diff --git a/.gitattributes b/.gitattributes index c4b674eff71a6..fcf9b296f32bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -.java text=auto -.xml text=auto +*.java text=auto +*.xml text=auto diff --git a/CODEOWNERS b/CODEOWNERS index 4a69d5e57644e..f24b124155899 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -38,6 +38,7 @@ /bundles/org.openhab.binding.boschindego/ @jofleck /bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho +/bundles/org.openhab.binding.broadlinkthermostat/ @flo-02-mu /bundles/org.openhab.binding.bsblan/ @hypetsch /bundles/org.openhab.binding.bticinosmarther/ @MrRonfo /bundles/org.openhab.binding.buienradar/ @gedejong @@ -51,12 +52,12 @@ /bundles/org.openhab.binding.daikin/ @caffineehacker /bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.darksky/ @cweitkamp -/bundles/org.openhab.binding.deconz/ @J-N-K +/bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor -/bundles/org.openhab.binding.dmx/ @J-N-K +/bundles/org.openhab.binding.dmx/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.doorbird/ @mhilbush /bundles/org.openhab.binding.draytonwiser/ @andrew-schofield /bundles/org.openhab.binding.dscalarm/ @RSStephens @@ -68,6 +69,7 @@ /bundles/org.openhab.binding.energenie/ @hmerk /bundles/org.openhab.binding.enigma2/ @gdolfen /bundles/org.openhab.binding.enocean/ @fruggy83 +/bundles/org.openhab.binding.enphase/ @Hilbrand /bundles/org.openhab.binding.enturno/ @klocsson /bundles/org.openhab.binding.epsonprojector/ @mlobstein /bundles/org.openhab.binding.etherrain/ @dfad1469 @@ -76,6 +78,7 @@ /bundles/org.openhab.binding.feed/ @svilenvul /bundles/org.openhab.binding.feican/ @Hilbrand /bundles/org.openhab.binding.fmiweather/ @ssalonen +/bundles/org.openhab.binding.folderwatcher/ @goopilot /bundles/org.openhab.binding.folding/ @fa2k /bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand /bundles/org.openhab.binding.freebox/ @lolodomo @@ -99,8 +102,9 @@ /bundles/org.openhab.binding.heliosventilation/ @ramack /bundles/org.openhab.binding.heos/ @Wire82 /bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s +/bundles/org.openhab.binding.homewizard/ @Daniel-42 /bundles/org.openhab.binding.hpprinter/ @cossey -/bundles/org.openhab.binding.http/ @J-N-K +/bundles/org.openhab.binding.http/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.hue/ @cweitkamp /bundles/org.openhab.binding.hydrawise/ @digitaldan /bundles/org.openhab.binding.hyperion/ @tavalin @@ -141,10 +145,12 @@ /bundles/org.openhab.binding.loxone/ @ppieczul /bundles/org.openhab.binding.luftdateninfo/ @weymann /bundles/org.openhab.binding.lutron/ @actong @bobadair +/bundles/org.openhab.binding.luxtronikheatpump/ @sgiehl /bundles/org.openhab.binding.magentatv/ @markus7017 -/bundles/org.openhab.binding.mail/ @J-N-K +/bundles/org.openhab.binding.mail/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.max/ @marcelrv /bundles/org.openhab.binding.mcp23017/ @aogorek +/bundles/org.openhab.binding.mecmeter/ @kaikreuzer /bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep /bundles/org.openhab.binding.meteoalerte/ @clinique /bundles/org.openhab.binding.meteoblue/ @9037568 @@ -169,6 +175,7 @@ /bundles/org.openhab.binding.mqtt.generic/ @davidgraeff /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff +/bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.mystrom/ @pail23 /bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn /bundles/org.openhab.binding.neato/ @jjlauterbach @@ -192,7 +199,7 @@ /bundles/org.openhab.binding.omnikinverter/ @hansbogert /bundles/org.openhab.binding.omnilink/ @ecdye /bundles/org.openhab.binding.onebusaway/ @sdwilsh -/bundles/org.openhab.binding.onewire/ @J-N-K +/bundles/org.openhab.binding.onewire/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.onewiregpio/ @aogorek /bundles/org.openhab.binding.onkyo/ @pail23 @paulianttila /bundles/org.openhab.binding.opengarage/ @psmedley @@ -206,6 +213,7 @@ /bundles/org.openhab.binding.paradoxalarm/ @theater /bundles/org.openhab.binding.pentair/ @jsjames /bundles/org.openhab.binding.phc/ @gnlpfjh +/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler /bundles/org.openhab.binding.pioneeravr/ @Stratehm /bundles/org.openhab.binding.pixometer/ @Confectrician /bundles/org.openhab.binding.pjlinkdevice/ @nils @@ -216,10 +224,12 @@ /bundles/org.openhab.binding.pulseaudio/ @peuter /bundles/org.openhab.binding.pushbullet/ @hakan42 /bundles/org.openhab.binding.pushover/ @cweitkamp +/bundles/org.openhab.binding.qbus/ @QbusKoen /bundles/org.openhab.binding.radiothermostat/ @mlobstein /bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.revogi/ @andibraeu /bundles/org.openhab.binding.remoteopenhab/ @lolodomo +/bundles/org.openhab.binding.resol/ @ramack /bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila /bundles/org.openhab.binding.rme/ @kgoderis /bundles/org.openhab.binding.robonect/ @reyem @@ -244,7 +254,7 @@ /bundles/org.openhab.binding.smartmeter/ @msteigenberger /bundles/org.openhab.binding.smartthings/ @BobRak /bundles/org.openhab.binding.smhi/ @pacive -/bundles/org.openhab.binding.snmp/ @J-N-K +/bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.solaredge/ @alexf2015 /bundles/org.openhab.binding.solarlog/ @johannrichard /bundles/org.openhab.binding.somfymylink/ @loungeflyz @@ -254,20 +264,21 @@ /bundles/org.openhab.binding.sonyprojector/ @lolodomo /bundles/org.openhab.binding.spotify/ @Hilbrand /bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush +/bundles/org.openhab.binding.surepetcare/ @renescherer @HerzScheisse /bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis /bundles/org.openhab.binding.tado/ @dfrommi /bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag /bundles/org.openhab.binding.telegram/ @ZzetT -/bundles/org.openhab.binding.teleinfo/ @Nokyyz +/bundles/org.openhab.binding.teleinfo/ @Nokyyz @olivierkeke /bundles/org.openhab.binding.tellstick/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.tesla/ @kgoderis /bundles/org.openhab.binding.tibber/ @kjoglum /bundles/org.openhab.binding.tivo/ @mlobstein /bundles/org.openhab.binding.touchwand/ @roieg /bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand -/bundles/org.openhab.binding.tr064/ @J-N-K +/bundles/org.openhab.binding.tr064/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer /bundles/org.openhab.binding.unifi/ @mgbowman /bundles/org.openhab.binding.unifiedremote/ @GiviMAD @@ -275,16 +286,19 @@ /bundles/org.openhab.binding.upnpcontrol/ @mherwege /bundles/org.openhab.binding.urtsi/ @OLibutzki /bundles/org.openhab.binding.valloxmv/ @bjoernbrings +/bundles/org.openhab.binding.vdr/ @MatthiasKlocke /bundles/org.openhab.binding.vektiva/ @octa22 /bundles/org.openhab.binding.velbus/ @cedricboon /bundles/org.openhab.binding.velux/ @gs4711 /bundles/org.openhab.binding.venstarthermostat/ @hww3 @digitaldan +/bundles/org.openhab.binding.ventaair/ @t2000 /bundles/org.openhab.binding.verisure/ @jannegpriv /bundles/org.openhab.binding.vigicrues/ @clinique /bundles/org.openhab.binding.vitotronic/ @steand /bundles/org.openhab.binding.volvooncall/ @clinique /bundles/org.openhab.binding.weathercompany/ @mhilbush /bundles/org.openhab.binding.weatherunderground/ @lolodomo +/bundles/org.openhab.binding.webthing/ @grro /bundles/org.openhab.binding.wemo/ @hmerk /bundles/org.openhab.binding.wifiled/ @rvt @xylo /bundles/org.openhab.binding.windcentrale/ @marcelrv @@ -300,9 +314,9 @@ /bundles/org.openhab.io.homekit/ @beowulfe @yfre /bundles/org.openhab.io.hueemulation/ @davidgraeff @digitaldan /bundles/org.openhab.io.imperihome/ @pdegeus +/bundles/org.openhab.io.metrics/ @pravussum /bundles/org.openhab.io.neeo/ @tmrobert8 /bundles/org.openhab.io.openhabcloud/ @kaikreuzer -/bundles/org.openhab.io.transport.modbus/ @ssalonen /bundles/org.openhab.persistence.dynamodb/ @ssalonen /bundles/org.openhab.persistence.influxdb/ @lujop /bundles/org.openhab.persistence.jdbc/ @openhab/add-ons-maintainers diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 8569f1c6e39e9..2b591c4aa9b58 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -176,6 +176,11 @@ org.openhab.binding.bosesoundtouch ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.broadlinkthermostat + ${project.version} + org.openhab.addons.bundles org.openhab.binding.bsblan @@ -326,6 +331,11 @@ org.openhab.binding.enocean ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.enphase + ${project.version} + org.openhab.addons.bundles org.openhab.binding.enturno @@ -366,6 +376,11 @@ org.openhab.binding.fmiweather ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.folderwatcher + ${project.version} + org.openhab.addons.bundles org.openhab.binding.folding @@ -481,6 +496,11 @@ org.openhab.binding.homematic ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.homewizard + ${project.version} + org.openhab.addons.bundles org.openhab.binding.hpprinter @@ -691,6 +711,11 @@ org.openhab.binding.lutron ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.luxtronikheatpump + ${project.version} + org.openhab.addons.bundles org.openhab.binding.magentatv @@ -711,6 +736,11 @@ org.openhab.binding.mcp23017 ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mecmeter + ${project.version} + org.openhab.addons.bundles org.openhab.binding.melcloud @@ -1016,6 +1046,11 @@ org.openhab.binding.phc ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.pilight + ${project.version} + org.openhab.addons.bundles org.openhab.binding.pioneeravr @@ -1086,6 +1121,11 @@ org.openhab.binding.remoteopenhab ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.resol + ${project.version} + org.openhab.addons.bundles org.openhab.binding.rfxcom @@ -1256,6 +1296,11 @@ org.openhab.binding.squeezebox ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.surepetcare + ${project.version} + org.openhab.addons.bundles org.openhab.binding.synopanalyzer @@ -1361,6 +1406,11 @@ org.openhab.binding.valloxmv ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.vdr + ${project.version} + org.openhab.addons.bundles org.openhab.binding.vektiva @@ -1381,6 +1431,11 @@ org.openhab.binding.venstarthermostat ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.ventaair + ${project.version} + org.openhab.addons.bundles org.openhab.binding.verisure @@ -1486,6 +1541,11 @@ org.openhab.io.imperihome ${project.version} + + org.openhab.addons.bundles + org.openhab.io.metrics + ${project.version} + org.openhab.addons.bundles org.openhab.io.neeo diff --git a/bundles/org.openhab.binding.airvisualnode/noEmbedDependencies.profile b/bundles/org.openhab.binding.airvisualnode/noEmbedDependencies.profile deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/bundles/org.openhab.binding.airvisualnode/pom.xml b/bundles/org.openhab.binding.airvisualnode/pom.xml index 6e89e6b57cf60..ac7f864a4e528 100644 --- a/bundles/org.openhab.binding.airvisualnode/pom.xml +++ b/bundles/org.openhab.binding.airvisualnode/pom.xml @@ -14,9 +14,15 @@ openHAB Add-ons :: Bundles :: AirVisual Node Air Quality Monitor Binding + + + !jcifs + + + - org.samba.jcifs + jcifs jcifs 1.3.17 compile diff --git a/bundles/org.openhab.binding.airvisualnode/src/main/feature/feature.xml b/bundles/org.openhab.binding.airvisualnode/src/main/feature/feature.xml index 66adc39bd0085..a01fcaa87ab3a 100644 --- a/bundles/org.openhab.binding.airvisualnode/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.airvisualnode/src/main/feature/feature.xml @@ -4,7 +4,6 @@ openhab-runtime-base - mvn:org.samba.jcifs/jcifs/1.3.17 mvn:org.openhab.addons.bundles/org.openhab.binding.airvisualnode/${project.version} diff --git a/bundles/org.openhab.binding.alarmdecoder/README.md b/bundles/org.openhab.binding.alarmdecoder/README.md index 86ef3fd2bdbd3..a7e72455c56b3 100644 --- a/bundles/org.openhab.binding.alarmdecoder/README.md +++ b/bundles/org.openhab.binding.alarmdecoder/README.md @@ -12,8 +12,6 @@ There are several versions of the adapter available: This binding allows openHAB to access the state of wired or wireless contacts and motion detectors connected to supported alarm panels, as well as the state of attached keypads and the messages send to attached LRR devices. Support is also available for sending keypad commands, including special/programmable keys supported by your panel. -For those upgrading from the OH1 version of the binding, the [original OH1 README](https://www.openhab.org/v2.5/addons/bindings/alarmdecoder1/) file is available for reference. - ## Supported Things The binding supports the following thing types: @@ -311,3 +309,7 @@ The alarmdecoder device cannot query the panel for the state of individual zones For this reason, the binding puts contacts into the "unknown" state (UNDEF), *until the panel goes into the READY state*. At that point, all contacts for which no update messages have arrived are presumed to be in the CLOSED state. In other words: to get to a clean slate after an openHAB restart, close all doors/windows such that the panel is READY. + +## Reference Information + +The protocol used to communicate with the Alarm Decoder is described [here](https://www.alarmdecoder.com/wiki/index.php/Protocol). diff --git a/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/binding/binding.xml index 84d93cead336a..aa161e3d8303a 100644 --- a/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/binding/binding.xml @@ -22,7 +22,6 @@ URL to use for playing audio streams, e.g. http://192.168.0.2:8080 - false diff --git a/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/thing/thing-types.xml index 33f8a72e3ed64..84874e6ed22ee 100644 --- a/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.allplay/src/main/resources/OH-INF/thing/thing-types.xml @@ -34,15 +34,13 @@ - + The device identifier identifies one certain speaker. - true The device name of the speaker. - false diff --git a/bundles/org.openhab.binding.amazonechocontrol/README.md b/bundles/org.openhab.binding.amazonechocontrol/README.md index c67ab108348f0..12b595f9bf769 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/README.md +++ b/bundles/org.openhab.binding.amazonechocontrol/README.md @@ -429,7 +429,7 @@ sitemap flashbriefings label="Flash Briefings" ## Smart Home Devices -Note: the cannels of smartHomeDevices and smartHomeDeviceGroup will be created dynamically based on the capabilities reported by the amazon server. This can take a little bit of time. +Note: the channels of smartHomeDevices and smartHomeDeviceGroup will be created dynamically based on the capabilities reported by the amazon server. This can take a little bit of time. The polling interval configured in the Account Thing to get the state is specified in minutes and has a minimum of 10. This means it takes up to 10 minutes to see the state of a channel. The reason for this low interval is, that the polling causes a big server load for the Smart Home Skills. #### Supported Things diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java index d69a041a56bae..8615ef193db9b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java @@ -14,14 +14,10 @@ import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; -import java.util.Date; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -180,7 +176,17 @@ synchronized void setSmartHomeDevices(List deviceList) { List aliases = shd.aliases; if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null && "SonarCloudService".equals(driverIdentity.identifier)) { - deviceName = "Alexa Guard on " + shd.friendlyName; + List<@Nullable String> interfaces = shd.getCapabilities().stream().map(c -> c.interfaceName) + .collect(Collectors.toList()); + if (interfaces.contains("Alexa.AcousticEventSensor")) { + deviceName = "Alexa Guard on " + shd.friendlyName; + } else if (interfaces.contains("Alexa.ColorController")) { + deviceName = "Alexa Color Controller on " + shd.friendlyName; + } else if (interfaces.contains("Alexa.PowerController")) { + deviceName = "Alexa Plug on " + shd.friendlyName; + } else { + deviceName = "Unknown Device on " + shd.friendlyName; + } } else if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null && "OnGuardSmartHomeBridgeService".equals(driverIdentity.identifier)) { deviceName = "Alexa Guard"; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java index baaafeb26db6d..fd8d21399e739 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java @@ -29,15 +29,32 @@ public static class SmartHomeCapability { public @Nullable String version; public @Nullable String interfaceName; public @Nullable Properties properties; + + @Override + public String toString() { + return "SmartHomeCapability{" + "capabilityType='" + capabilityType + '\'' + ", type='" + type + '\'' + + ", version='" + version + '\'' + ", interfaceName='" + interfaceName + '\'' + ", properties=" + + properties + '}'; + } } public static class Properties { public @Nullable List supported; + + @Override + public String toString() { + return "Properties{" + "supported=" + supported + '}'; + } } public static class Property { public @Nullable String name; } + @Override + public String toString() { + return "JsonSmartHomeCapabilities{" + "capabilites=" + capabilites + '}'; + } + public @Nullable List capabilites; } diff --git a/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherBridgeHandler.java b/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherBridgeHandler.java index 8461a3f95328a..1c86211334d22 100644 --- a/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherBridgeHandler.java +++ b/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherBridgeHandler.java @@ -18,7 +18,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.ambientweather.internal.config.BridgeConfig; @@ -128,7 +127,7 @@ public void initialize() { */ private boolean hasApplicationKey() { String configApplicationKey = getConfigAs(BridgeConfig.class).applicationKey; - if (StringUtils.isEmpty(configApplicationKey)) { + if (configApplicationKey == null || configApplicationKey.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing application key"); return false; } @@ -141,7 +140,7 @@ private boolean hasApplicationKey() { */ private boolean hasApiKey() { String configApiKey = getConfigAs(BridgeConfig.class).apiKey; - if (StringUtils.isEmpty(configApiKey)) { + if (configApiKey == null || configApiKey.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing API key"); return false; } diff --git a/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherEventListener.java b/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherEventListener.java index 27ceb2be23f61..39d54463ba909 100644 --- a/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherEventListener.java +++ b/bundles/org.openhab.binding.ambientweather/src/main/java/org/openhab/binding/ambientweather/internal/handler/AmbientWeatherEventListener.java @@ -18,7 +18,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.commons.lang.StringUtils; import org.json.JSONObject; import org.openhab.binding.ambientweather.internal.model.DeviceJson; import org.openhab.binding.ambientweather.internal.model.EventDataGenericJson; @@ -318,8 +317,9 @@ private synchronized void handleData(String jsonData) { logger.debug("Listener: Data: {}", jsonData); try { EventDataGenericJson data = gson.fromJson(jsonData, EventDataGenericJson.class); - if (StringUtils.isNotEmpty(data.macAddress)) { - sendWeatherDataToHandler(data.macAddress, jsonData); + String macAddress = data == null ? null : data.macAddress; + if (macAddress != null && !macAddress.isEmpty()) { + sendWeatherDataToHandler(macAddress, jsonData); } } catch (JsonSyntaxException e) { logger.info("Listener: Exception parsing subscribed event: {}", e.getMessage()); diff --git a/bundles/org.openhab.binding.androiddebugbridge/README.md b/bundles/org.openhab.binding.androiddebugbridge/README.md index 9b1ce5c0dace0..c2c159549783d 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/README.md +++ b/bundles/org.openhab.binding.androiddebugbridge/README.md @@ -6,7 +6,7 @@ If you are not familiar with adb I suggest you to search "How to enable adb over ## Supported Things -This binding was tested on the Fire TV Stick (android version 7.1.2, volume control not working) and Nexus5x (android version 8.1.0, everything works nice), please update this document if you tested it with other android versions to reflect the compatibility of the biding. +This binding was tested on the Fire TV Stick (android version 7.1.2, volume control not working) and Nexus5x (android version 8.1.0, everything works nice), please update this document if you tested it with other android versions to reflect the compatibility of the binding. ## Discovery diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java index de2d31d64d923..3e738d6a114fc 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java @@ -42,6 +42,7 @@ public class AndroidDebugBridgeBindingConstants { public static final String STOP_PACKAGE_CHANNEL = "stop-package"; public static final String STOP_CURRENT_PACKAGE_CHANNEL = "stop-current-package"; public static final String CURRENT_PACKAGE_CHANNEL = "current-package"; + public static final String AWAKE_STATE_CHANNEL = "awake-state"; public static final String WAKE_LOCK_CHANNEL = "wake-lock"; public static final String SCREEN_STATE_CHANNEL = "screen-state"; // List of all Parameters diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java index 8778fcfbbec42..1ddb6e642c197 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java @@ -117,7 +117,13 @@ public String getCurrentPackage() throws AndroidDebugBridgeDeviceException, Inte if (packageActivityName.contains("/")) return packageActivityName.split("/")[0]; } - throw new AndroidDebugBridgeDeviceReadException("can read package name"); + throw new AndroidDebugBridgeDeviceReadException("Unable to read package name"); + } + + public boolean isAwake() + throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException { + String devicesResp = runAdbShell("dumpsys", "activity", "|", "grep", "mWakefulness"); + return devicesResp.contains("mWakefulness=Awake"); } public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDeviceException, @@ -125,12 +131,13 @@ public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDevic String devicesResp = runAdbShell("dumpsys", "power", "|", "grep", "'Display Power'"); if (devicesResp.contains("=")) { try { - return devicesResp.split("=")[1].equals("ON"); + var state = devicesResp.split("=")[1].trim(); + return state.equals("ON"); } catch (NumberFormatException e) { logger.debug("Unable to parse device wake lock: {}", e.getMessage()); } } - throw new AndroidDebugBridgeDeviceReadException("can read screen state"); + throw new AndroidDebugBridgeDeviceReadException("Unable to read screen state"); } public boolean isPlayingMedia(String currentApp) @@ -168,12 +175,12 @@ public int getPowerWakeLock() throws InterruptedException, AndroidDebugBridgeDev String lockResp = runAdbShell("dumpsys", "power", "|", "grep", "Locks", "|", "grep", "'size='"); if (lockResp.contains("=")) { try { - return Integer.parseInt(lockResp.replace("\n", "").split("=")[1]); + return Integer.parseInt(lockResp.replace("\n", "").split("=")[1].trim()); } catch (NumberFormatException e) { logger.debug("Unable to parse device wake lock: {}", e.getMessage()); } } - throw new AndroidDebugBridgeDeviceReadException("can read wake lock"); + throw new AndroidDebugBridgeDeviceReadException("Unable to read wake lock"); } private void setVolume(int stream, int volume) diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java index 245e38b90ca37..410e4887e4e19 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java @@ -59,6 +59,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration(); private @Nullable ScheduledFuture connectionCheckerSchedule; private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null; + private boolean deviceAwake = false; public AndroidDebugBridgeHandler(Thing thing) { super(thing); @@ -135,6 +136,12 @@ private void handleCommandInternal(ChannelUID channelUID, Command command) updateState(channelUID, new DecimalType(lock)); } break; + case AWAKE_STATE_CHANNEL: + if (command instanceof RefreshType) { + boolean awakeState = adbConnection.isAwake(); + updateState(channelUID, OnOffType.from(awakeState)); + } + break; case SCREEN_STATE_CHANNEL: if (command instanceof RefreshType) { boolean screenState = adbConnection.isScreenOn(); @@ -277,6 +284,7 @@ public void checkConnection() { } catch (AndroidDebugBridgeDeviceException e) { logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(), e.getMessage()); + adbConnection.disconnect(); updateStatus(ThingStatus.OFFLINE); return; } @@ -294,6 +302,23 @@ public void checkConnection() { } private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException { + boolean awakeState; + boolean prevDeviceAwake = deviceAwake; + try { + awakeState = adbConnection.isAwake(); + deviceAwake = awakeState; + } catch (TimeoutException e) { + logger.warn("Unable to refresh awake state: Timeout"); + return; + } + var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL); + if (isLinked(awakeStateChannelUID)) { + updateState(awakeStateChannelUID, OnOffType.from(awakeState)); + } + if (!awakeState && !prevDeviceAwake) { + logger.debug("device {} is sleeping", config.ip); + return; + } try { handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH); } catch (AndroidDebugBridgeDeviceReadException e) { diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml index 98710f5459c74..77336ba900f98 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml @@ -18,6 +18,7 @@ + serial @@ -386,6 +387,13 @@ + + Switch + + Awake State + + + Switch diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/AstroThingHandler.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/AstroThingHandler.java index ad27bdb7f13c9..2e06df173cfc2 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/AstroThingHandler.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/AstroThingHandler.java @@ -34,7 +34,7 @@ import javax.measure.quantity.Angle; -import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang3.time.DateFormatUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.astro.internal.action.AstroActions; diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/Job.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/Job.java index 1ace958cb7da3..b4a5b8ee8ba7e 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/Job.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/Job.java @@ -16,7 +16,7 @@ import static java.util.Calendar.SECOND; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; -import static org.apache.commons.lang.time.DateUtils.truncatedEquals; +import static org.apache.commons.lang3.time.DateUtils.truncatedEquals; import static org.openhab.binding.astro.internal.AstroBindingConstants.*; import static org.openhab.binding.astro.internal.util.DateTimeUtils.*; diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/util/DateTimeUtils.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/util/DateTimeUtils.java index 140e8c154971d..6e18febb2c5d4 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/util/DateTimeUtils.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/util/DateTimeUtils.java @@ -16,7 +16,7 @@ import java.util.Date; import java.util.regex.Pattern; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.time.DateUtils; import org.openhab.binding.astro.internal.config.AstroChannelConfig; import org.openhab.binding.astro.internal.model.Range; import org.slf4j.Logger; diff --git a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java index 36745f55228a6..5bf66f38c64b4 100644 --- a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java +++ b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java @@ -1118,7 +1118,7 @@ public void responseReceived(String response) { } m = versionHdPattern.matcher(response); - if (m.matches()) { + if (!capabilities.isUHDModel() && m.matches()) { handleVersionResponse(m, response); return; } diff --git a/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/config/config.xml index ce4acc3aa69b3..d05e74d3a272c 100644 --- a/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/config/config.xml @@ -5,23 +5,20 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + network-address IP or Host name of Atlona Matrix Switch - true - + User Name to login with if Telnet Login is on - false true - + password Password to login with if Telnet Login is on - false true diff --git a/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/discovery/AutelisDiscoveryParticipant.java b/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/discovery/AutelisDiscoveryParticipant.java index 67a29b9a9fb5e..23c859c5ef95c 100644 --- a/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/discovery/AutelisDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/discovery/AutelisDiscoveryParticipant.java @@ -65,7 +65,7 @@ public Set getSupportedThingTypeUIDs() { properties.put("host", url.getHost()); properties.put("user", "admin"); properties.put("password", "admin"); - properties.put("port", new Integer(port)); + properties.put("port", Integer.valueOf(port)); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label) .build(); diff --git a/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/handler/AutelisHandler.java b/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/handler/AutelisHandler.java index 30e37717fa983..0766da21fc19c 100644 --- a/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/handler/AutelisHandler.java +++ b/bundles/org.openhab.binding.autelis/src/main/java/org/openhab/binding/autelis/internal/handler/AutelisHandler.java @@ -13,11 +13,15 @@ package org.openhab.binding.autelis.internal.handler; import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,14 +29,11 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.util.B64Code; -import org.eclipse.jetty.util.StringUtil; import org.openhab.binding.autelis.internal.AutelisBindingConstants; import org.openhab.binding.autelis.internal.config.AutelisConfiguration; import org.openhab.core.library.types.DecimalType; @@ -191,88 +192,94 @@ public void channelLinked(ChannelUID channelUID) { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.debug("handleCommand channel: {} command: {}", channelUID.getId(), command); - if (AutelisBindingConstants.CMD_LIGHTS.equals(channelUID.getId())) { - /* - * lighting command possible values, but we will let anything - * through. alloff, allon, csync, cset, cswim, party, romance, - * caribbean, american, sunset, royalty, blue, green, red, white, - * magenta, hold, recall - */ - getUrl(baseURL + "/lights.cgi?val=" + command.toString(), TIMEOUT_SECONDS); - } else if (AutelisBindingConstants.CMD_REBOOT.equals(channelUID.getId()) && command == OnOffType.ON) { - getUrl(baseURL + "/userreboot.cgi?do=true" + command.toString(), TIMEOUT_SECONDS); - updateState(channelUID, OnOffType.OFF); - } else { - String[] args = channelUID.getId().split("-"); - if (args.length < 2) { - logger.warn("Unown channel {} for command {}", channelUID, command); - return; - } - String type = args[0]; - String name = args[1]; - - if (AutelisBindingConstants.CMD_EQUIPMENT.equals(type)) { - String cmd = "value"; - int value; - if (command == OnOffType.OFF) { - value = 0; - } else if (command == OnOffType.ON) { - value = 1; - } else if (command instanceof DecimalType) { - value = ((DecimalType) command).intValue(); - if (!isJandy() && value >= 3) { - // this is a autelis dim type. not sure what 2 does - cmd = "dim"; - } - } else { - logger.error("command type {} is not supported", command); + try { + logger.debug("handleCommand channel: {} command: {}", channelUID.getId(), command); + if (AutelisBindingConstants.CMD_LIGHTS.equals(channelUID.getId())) { + /* + * lighting command possible values, but we will let anything + * through. alloff, allon, csync, cset, cswim, party, romance, + * caribbean, american, sunset, royalty, blue, green, red, white, + * magenta, hold, recall + */ + getUrl(baseURL + "/lights.cgi?val=" + command.toString(), TIMEOUT_SECONDS); + } else if (AutelisBindingConstants.CMD_REBOOT.equals(channelUID.getId()) && command == OnOffType.ON) { + getUrl(baseURL + "/userreboot.cgi?do=true" + command.toString(), TIMEOUT_SECONDS); + updateState(channelUID, OnOffType.OFF); + } else { + String[] args = channelUID.getId().split("-"); + if (args.length < 2) { + logger.warn("Unown channel {} for command {}", channelUID, command); return; } - String response = getUrl(baseURL + "/set.cgi?name=" + name + "&" + cmd + "=" + value, TIMEOUT_SECONDS); - logger.debug("equipment set {} {} {} : result {}", name, cmd, value, response); - } else if (AutelisBindingConstants.CMD_TEMP.equals(type)) { - String value; - if (command == IncreaseDecreaseType.INCREASE) { - value = "up"; - } else if (command == IncreaseDecreaseType.DECREASE) { - value = "down"; - } else if (command == OnOffType.OFF) { - value = "0"; - } else if (command == OnOffType.ON) { - value = "1"; - } else { - value = command.toString(); - } + String type = args[0]; + String name = args[1]; + + if (AutelisBindingConstants.CMD_EQUIPMENT.equals(type)) { + String cmd = "value"; + int value; + if (command == OnOffType.OFF) { + value = 0; + } else if (command == OnOffType.ON) { + value = 1; + } else if (command instanceof DecimalType) { + value = ((DecimalType) command).intValue(); + if (!isJandy() && value >= 3) { + // this is a autelis dim type. not sure what 2 does + cmd = "dim"; + } + } else { + logger.error("command type {} is not supported", command); + return; + } + String response = getUrl(baseURL + "/set.cgi?name=" + name + "&" + cmd + "=" + value, + TIMEOUT_SECONDS); + logger.debug("equipment set {} {} {} : result {}", name, cmd, value, response); + } else if (AutelisBindingConstants.CMD_TEMP.equals(type)) { + String value; + if (command == IncreaseDecreaseType.INCREASE) { + value = "up"; + } else if (command == IncreaseDecreaseType.DECREASE) { + value = "down"; + } else if (command == OnOffType.OFF) { + value = "0"; + } else if (command == OnOffType.ON) { + value = "1"; + } else { + value = command.toString(); + } - String cmd; - // name ending in sp are setpoints, ht are heater? - if (name.endsWith("sp")) { - cmd = "temp"; - } else if (name.endsWith("ht")) { - cmd = "hval"; + String cmd; + // name ending in sp are setpoints, ht are heater? + if (name.endsWith("sp")) { + cmd = "temp"; + } else if (name.endsWith("ht")) { + cmd = "hval"; + } else { + logger.error("Unknown temp type {}", name); + return; + } + String response = getUrl(baseURL + "/set.cgi?wait=1&name=" + name + "&" + cmd + "=" + value, + TIMEOUT_SECONDS); + logger.debug("temp set name:{} cmd:{} value:{} : result {}", name, cmd, value, response); + } else if (AutelisBindingConstants.CMD_CHEM.equals(type)) { + String response = getUrl(baseURL + "/set.cgi?name=" + name + "&chem=" + command.toString(), + TIMEOUT_SECONDS); + logger.debug("chlrp {} {}: result {}", name, command, response); + } else if (AutelisBindingConstants.CMD_PUMPS.equals(type)) { + String response = getUrl(baseURL + "/set.cgi?name=" + name + "&speed=" + command.toString(), + TIMEOUT_SECONDS); + logger.debug("pumps {} {}: result {}", name, command, response); } else { - logger.error("Unknown temp type {}", name); - return; + logger.error("Unsupported type {}", type); } - String response = getUrl(baseURL + "/set.cgi?wait=1&name=" + name + "&" + cmd + "=" + value, - TIMEOUT_SECONDS); - logger.debug("temp set name:{} cmd:{} value:{} : result {}", name, cmd, value, response); - } else if (AutelisBindingConstants.CMD_CHEM.equals(type)) { - String response = getUrl(baseURL + "/set.cgi?name=" + name + "&chem=" + command.toString(), - TIMEOUT_SECONDS); - logger.debug("chlrp {} {}: result {}", name, command, response); - } else if (AutelisBindingConstants.CMD_PUMPS.equals(type)) { - String response = getUrl(baseURL + "/set.cgi?name=" + name + "&speed=" + command.toString(), - TIMEOUT_SECONDS); - logger.debug("pumps {} {}: result {}", name, command, response); - } else { - logger.error("Unsupported type {}", type); } + clearState(true); + // reset the schedule for our next poll which at that time will reflect if our command was successful or + // not. + initPolling(COMMAND_UPDATE_TIME_SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - clearState(true); - // reset the schedule for our next poll which at that time will reflect if our command was successful or not. - initPolling(COMMAND_UPDATE_TIME_SECONDS); } /** @@ -288,17 +295,17 @@ private void configure() { String username = configuration.user; String password = configuration.password; - if (StringUtils.isBlank(username)) { + if (username == null || username.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "username must not be empty"); return; } - if (StringUtils.isBlank(password)) { + if (password == null || password.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "password must not be empty"); return; } - if (StringUtils.isBlank(host)) { + if (host == null || host.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "hostname must not be empty"); return; } @@ -314,7 +321,8 @@ private void configure() { } baseURL = "http://" + host + ":" + port; - basicAuthentication = "Basic " + B64Code.encode(username + ":" + password, StringUtil.__ISO_8859_1); + basicAuthentication = "Basic " + + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.ISO_8859_1)); logger.debug("Autelius binding configured with base url {} and refresh period of {}", baseURL, refresh); initPolling(0); @@ -349,7 +357,7 @@ private void clearPolling() { * Poll the Autelis controller for updates. This will retrieve various xml documents and update channel states from * its contents. */ - private void pollAutelisController() { + private void pollAutelisController() throws InterruptedException { logger.trace("Connecting to {}", baseURL); // clear our cached stated IF it is time. @@ -443,7 +451,7 @@ private void pollAutelisController() { } } - if (StringUtils.isEmpty((value))) { + if (value == null || value.isEmpty()) { continue; } @@ -466,16 +474,13 @@ private void pollAutelisController() { * @param timeout * @return */ - private synchronized String getUrl(String url, int timeout) { + private synchronized String getUrl(String url, int timeout) throws InterruptedException { // throttle commands for a very short time to avoid 'loosing' them long now = System.currentTimeMillis(); long nextReq = lastRequestTime + THROTTLE_TIME_MILLISECONDS; if (nextReq > now) { - try { - logger.trace("Throttling request for {} mills", nextReq - now); - Thread.sleep(nextReq - now); - } catch (InterruptedException ignored) { - } + logger.trace("Throttling request for {} mills", nextReq - now); + Thread.sleep(nextReq - now); } String getURL = url + (url.contains("?") ? "&" : "?") + "timestamp=" + System.currentTimeMillis(); logger.trace("Getting URL {} ", getURL); @@ -490,7 +495,7 @@ private synchronized String getUrl(String url, int timeout) { } lastRequestTime = System.currentTimeMillis(); return response.getContentAsString(); - } catch (Exception e) { + } catch (ExecutionException | TimeoutException e) { logger.debug("Could not make http connection", e); } return null; diff --git a/bundles/org.openhab.binding.automower/README.md b/bundles/org.openhab.binding.automower/README.md index 0ae12ecb2770d..70a3b0f59371a 100644 --- a/bundles/org.openhab.binding.automower/README.md +++ b/bundles/org.openhab.binding.automower/README.md @@ -1,26 +1,26 @@ # Automower Binding -This binding communicates to the Husqvarna Automower Connect API in order to send commands and query the state of Husqvarna Automower robots. +This is the binding for [Husqvarna Automower a robotic lawn mowers](https://www.husqvarna.com/uk/products/robotic-lawn-mowers/). +This binding allows you to integrate, view and control Automower lawn mowers in the openHAB environment. ## Supported Things -`bridge:` The bridge needs to be configured with credentials and an application key that allows communicating with the Automower Connect Api +`bridge:` The bridge needs to be configured with credentials and an application key that allows communicating with the Automower Connect API `automower:` A single Husqvarna Automower robot -Basically all Husqvarna Automower models with "Automower Connect" support should be supported. It was tested only with a Husqvarna Automower 450X +All Husqvarna Automower models with "Automower Connect" should be supported. It was tested only with a Husqvarna Automower 430X and 450X. ## Discovery -Once the bridge is created and configured, registered automowers will be discovered automatically - +Once the bridge is created and configured, OpenHab will automatically discover all Automowers registered on your account. ## Thing Configuration `bridge:` -- appKey (mandatory): The Application Key is required to communication with the Automower Connect Api. It can be obtained by registering an Application on the Husqvarna Website. This application also needs to be connected to the "Authentication API" and the "Automower Connect API" +- appKey (mandatory): The Application Key is required to communicate with the Automower Connect API. It can be obtained by registering an Application on [the Husqvarna Website](https://developer.husqvarnagroup.cloud/). This application also needs to be connected to the ["Authentication API" and the "Automower Connect API"](https://developer.husqvarnagroup.cloud/docs/getting-started) - userName (mandatory): The user name for which the application key has been issued - password (mandatory): The password for the given user - pollingInterval (optional): How often the bridge state should be queried in seconds. Default is 1h (3600s) @@ -34,23 +34,31 @@ With the default value of 1h this would mean ~720 requests per month for the bri - mowerId (mandatory): The Id of an automower as used by the Automower Connect Api to identify a mower. This is automatically filled when the thing is discovered - pollingInterval (optional): How often the current automower state should be polled in seconds. Default is 10min (600s) -Keep in mind that the status of the automowers should not be queried too often. -According to the Husqvarna documentation not more than 10000 requests per month and application key are allowed. -With the default value of 10min this would mean ~4300 requests per month per automower +Keep in mind that the status of the Automowers should not be queried too often. +According to the Husqvarna documentation, no more than 10000 requests per month and application key are allowed. +With the default value of 10min this would mean ~4300 requests per month per single Automower ## Channels -| channel | type | description | -|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | String | (readonly) The name of the Automower | -| mode | String | (readonly) The current mode (MAIN_AREA, SECONDARY_AREA, HOME, DEMO, UNKNOWN) | -| activity | String | (readonly) The current activity (UNKNOWN, NOT_APPLICABLE, MOWING, GOING_HOME, CHARGING, LEAVING, PARKED_IN_CS, STOPPED_IN_GARDEN) | -| state | String | (readonly) The current state (UNKNOWN, NOT_APPLICABLE, PAUSED, IN_OPERATION, WAIT_UPDATING, WAIT_POWER_UP, RESTRICTED, OFF, STOPPED, ERROR, FATAL_ERROR, ERROR_AT_POWER_UP) | -| last-update | DateTime | (readonly) The time when the automower updated its states | -| battery | Number | (readonly) The battery state of charge in percent | -| error-code | Number | (readonly) The current error code | -| error-timestamp | DateTime | (readonly) The timestamp when the current error occurred | +| channel | type | access mode | description | +|-------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| mower-status#mode | String | R | The current mode (MAIN_AREA, SECONDARY_AREA, HOME, DEMO, UNKNOWN) | +| mower-status#activity | String | R | The current activity (UNKNOWN, NOT_APPLICABLE, MOWING, GOING_HOME, CHARGING, LEAVING, PARKED_IN_CS, STOPPED_IN_GARDEN) | +| mower-status#state | String | R | The current state (UNKNOWN, NOT_APPLICABLE, PAUSED, IN_OPERATION, WAIT_UPDATING, WAIT_POWER_UP, RESTRICTED_NONE, RESTRICTED_WEEK_SCHEDULE, RESTRICTED_PARK_OVERRIDE, RESTRICTED_SENSOR, RESTRICTED_DAILY_LIMIT, OFF, STOPPED, ERROR, FATAL_ERROR, ERROR_AT_POWER_UP) | +| mower-status#last-update | DateTime | R | The time when the automower updated its states | +| mower-status#battery | Number | R | The battery state of charge in percent | +| mower-status#error-code | Number | R | The current error code | +| mower-status#error-timestamp | DateTime | R | The timestamp when the current error occurred | +| mower-status#planner-next-start | DateTime | R | The time for the next auto start. If the mower is charging then the value is the estimated time when it will be leaving the charging station. If the mower is about to start now, the value is NULL. | +| mower-status#planner-override-action | String | R | The action that overrides current planner operation. | +| mower-status#calendar-tasks | String | R | The JSON with the information about Automower planner. | +| mower#start | Number | W | Starts the automower for a duration | +| mower#resume_schedule | Switch | W | Resumes the Automower schedule | +| mower#pause | Switch | W | Pause the Automower | +| mower#park | Number | W | Park the Automower for a duration | +| mower#park_until_next_schedule | Switch | W | Park the Automower until next schedule | +| mower#park_until_further_notice | Switch | W | Park the Automower until further notice. | ## Actions @@ -79,19 +87,23 @@ The following actions are available for `automower`things: ### automower.items - String Automower_Name "Name" { channel="automower:automower:mybridge:myAutomower:name" } - String Automower_Mode "Mode" { channel="automower:automower:mybridge:myAutomower:mode" } - String Automower_Activity "Activity" { channel="automower:automower:mybridge:myAutomower:activity" } - String Automower_State "State" { channel="automower:automower:mybridge:myAutomower:state" } - DateTime Automower_Last_Update "Last Update" { channel="automower:automower:mybridge:myAutomower:last-update" } - Number Automower_Battery "Battery" { channel="automower:automower:mybridge:myAutomower:battery" } - Number Automower_Error_Code "Error Code" { channel="automower:automower:mybridge:myAutomower:error-code" } - DateTime Automower_Error_Time "Error Time" { channel="automower:automower:mybridge:myAutomower:error-timestamp" } - - - String Automower_Command "Command" { channel="automower:automower:mybridge:myAutomower:command" } - Number Automower_Command_Duration "Command Duration" { channel="automower:automower:mybridge:myAutomower:command-duration" } - String Automower_Command_Response "Command Response" { channel="automower:automower:mybridge:myAutomower:command-response" } + String Automower_Mode "Mode [%s]" { channel="automower:automower:mybridge:myAutomower:mower-status#mode" } + String Automower_Activity "Activity [%s]" { channel="automower:automower:mybridge:myAutomower:mower-status#activity" } + String Automower_State "State [%s]" { channel="automower:automower:mybridge:myAutomower:mower-status#state" } + DateTime Automower_Last_Update "Last Update" { channel="automower:automower:mybridge:myAutomower:mower-status#last-update" } + Number Automower_Battery "Battery [%d %%]" { channel="automower:automower:mybridge:myAutomower:mower-status#battery" } + Number Automower_Error_Code "Error Code [%d]" { channel="automower:automower:mybridge:myAutomower:mower-status#error-code" } + DateTime Automower_Error_Time "Error Time" { channel="automower:automower:mybridge:myAutomower:mower-status#error-timestamp" } + String Automower_Override_Action "Override Action [%s]" { channel="automower:automower:mybridge:myAutomower:mower-status#planner-override-action" } + DateTime Automower_Next_Start_Time "Next Start Time" { channel="automower:automower:mybridge:myAutomower:mower-status#planner-next-start" } + String Automower_Calendar_Tasks "Planned Tasks [%s]" { channel="automower:automower:mybridge:myAutomower:mower-status#calendar-tasks" } + + Number Automower_Command_Start "Start mowing for duration [%d min]" { channel="automower:automower:mybridge:myAutomower:mower#start" } + Switch Automower_Command_Resume "Resume the schedule" { channel="automower:automower:mybridge:myAutomower:mower#resume_schedule" } + Switch Automower_Command_Pause "Pause the automower" { channel="automower:automower:mybridge:myAutomower:mower#pause" } + Number Automower_Command_Park "Park for duration [%d min]" { channel="automower:automower:mybridge:myAutomower:mower#park" } + Switch Automower_Command_Park_Next_Schedule "Park until next schedule" { channel="automower:automower:mybridge:myAutomower:mower#park_until_next_schedule" } + Switch Automower_Command_Park_Notice "Park until further notice" { channel="automower:automower:mybridge:myAutomower:mower#park_until_further_notice" } ### automower.sitemap @@ -100,7 +112,6 @@ The following actions are available for `automower`things: sitemap demo label="Automower" { Frame { - Text item=Automower_Name Text item=Automower_Mode Text item=Automower_Activity Text item=Automower_State @@ -108,6 +119,9 @@ sitemap demo label="Automower" Text item=Automower_Battery Text item=Automower_Error_Code Text item=Automower_Error_Time + Text item=Automower_Override_Action + Text item=Automower_Next_Start_Time + Text item=Automower_Calendar_Tasks } } ``` diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerBindingConstants.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerBindingConstants.java index 4475662eec91d..14bebefa09d2e 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerBindingConstants.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerBindingConstants.java @@ -20,10 +20,10 @@ * used across the whole binding. * * @author Markus Pfleger - Initial contribution + * @author Marcin Czeczko - Added support for planner & calendar data */ @NonNullByDefault public class AutomowerBindingConstants { - private static final String BINDING_ID = "automower"; public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); @@ -32,7 +32,7 @@ public class AutomowerBindingConstants { public static final ThingTypeUID THING_TYPE_AUTOMOWER = new ThingTypeUID(BINDING_ID, "automower"); // List of all Channel ids - public static final String CHANNEL_MOWER_NAME = "name"; + public static final String CHANNEL_STATUS_NAME = "name"; public static final String CHANNEL_STATUS_MODE = "mode"; public static final String CHANNEL_STATUS_ACTIVITY = "activity"; public static final String CHANNEL_STATUS_STATE = "state"; @@ -40,6 +40,17 @@ public class AutomowerBindingConstants { public static final String CHANNEL_STATUS_BATTERY = "battery"; public static final String CHANNEL_STATUS_ERROR_CODE = "error-code"; public static final String CHANNEL_STATUS_ERROR_TIMESTAMP = "error-timestamp"; + public static final String CHANNEL_PLANNER_NEXT_START = "planner-next-start"; + public static final String CHANNEL_PLANNER_OVERRIDE_ACTION = "planner-override-action"; + public static final String CHANNEL_CALENDAR_TASKS = "calendar-tasks"; + + // Command channels + public static final String CHANNEL_COMMAND_START = "start"; + public static final String CHANNEL_COMMAND_RESUME_SCHEDULE = "resume_schedule"; + public static final String CHANNEL_COMMAND_PAUSE = "pause"; + public static final String CHANNEL_COMMAND_PARK = "park"; + public static final String CHANNEL_COMMAND_PARK_UNTIL_NEXT_SCHEDULE = "park_until_next_schedule"; + public static final String CHANNEL_COMMAND_PARK_UNTIL_NOTICE = "park_until_further_notice"; // Automower properties public static final String AUTOMOWER_ID = "mowerId"; diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerHandlerFactory.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerHandlerFactory.java index e084d5e3ddb2b..cfc59bd2eafba 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerHandlerFactory.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/AutomowerHandlerFactory.java @@ -27,6 +27,7 @@ import org.openhab.binding.automower.internal.things.AutomowerHandler; import org.openhab.core.auth.client.oauth2.OAuthFactory; import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -55,12 +56,14 @@ public class AutomowerHandlerFactory extends BaseThingHandlerFactory { private final OAuthFactory oAuthFactory; protected final @NonNullByDefault({}) HttpClient httpClient; private @Nullable ServiceRegistration automowerDiscoveryServiceRegistration; + private final TimeZoneProvider timeZoneProvider; @Activate - public AutomowerHandlerFactory(@Reference OAuthFactory oAuthFactory, - @Reference HttpClientFactory httpClientFactory) { + public AutomowerHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference HttpClientFactory httpClientFactory, + @Reference TimeZoneProvider timeZoneProvider) { this.oAuthFactory = oAuthFactory; this.httpClient = httpClientFactory.getCommonHttpClient(); + this.timeZoneProvider = timeZoneProvider; } @Override @@ -77,7 +80,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } if (AutomowerHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { - return new AutomowerHandler(thing); + return new AutomowerHandler(thing, timeZoneProvider); } return null; diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/bridge/AutomowerBridge.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/bridge/AutomowerBridge.java index cb053ddbbdc90..d24c6cdba1d26 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/bridge/AutomowerBridge.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/bridge/AutomowerBridge.java @@ -38,7 +38,6 @@ */ @NonNullByDefault public class AutomowerBridge { - private final OAuthClientService authService; private final String appKey; private final String userName; @@ -96,7 +95,6 @@ public Mower getAutomowerStatus(String id) throws AutomowerCommunicationExceptio */ public void sendAutomowerCommand(String id, AutomowerCommand command, long commandDuration) throws AutomowerCommunicationException { - MowerCommandAttributes attributes = new MowerCommandAttributes(); attributes.setDuration(commandDuration); diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/HusqvarnaApi.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/HusqvarnaApi.java index 252f1440dc20e..09c5f3487024a 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/HusqvarnaApi.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/HusqvarnaApi.java @@ -25,7 +25,6 @@ */ @NonNullByDefault public abstract class HusqvarnaApi { - private final HttpClient httpClient; protected final Gson gson; diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/AutomowerConnectApi.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/AutomowerConnectApi.java index f5806634900ae..64b00eab08959 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/AutomowerConnectApi.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/AutomowerConnectApi.java @@ -39,7 +39,6 @@ */ @NonNullByDefault public class AutomowerConnectApi extends HusqvarnaApi { - public AutomowerConnectApi(HttpClient httpClient) { super(httpClient); } @@ -81,7 +80,6 @@ public void sendCommand(String appKey, String token, String id, MowerCommandRequ private ContentResponse executeRequest(String appKey, String token, final Request request) throws AutomowerCommunicationException { - request.timeout(10, TimeUnit.SECONDS); request.header("Authorization-Provider", "husqvarna"); @@ -92,7 +90,10 @@ private ContentResponse executeRequest(String appKey, String token, final Reques ContentResponse response; try { response = request.send(); - } catch (InterruptedException | TimeoutException | ExecutionException e) { + } catch (TimeoutException | ExecutionException e) { + throw new AutomowerCommunicationException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new AutomowerCommunicationException(e); } return response; diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Calendar.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Calendar.java index a10250de5d512..66fbeda3e6ab6 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Calendar.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Calendar.java @@ -12,9 +12,17 @@ */ package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto; +import java.util.ArrayList; +import java.util.List; + /** * @author Markus Pfleger - Initial contribution + * @author Marcin Czeczko - Added support for planner & calendar data */ public class Calendar { + private List tasks = new ArrayList<>(); + public List getTasks() { + return tasks; + } } diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/CalendarTask.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/CalendarTask.java new file mode 100644 index 0000000000000..bfb03da4fa4c3 --- /dev/null +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/CalendarTask.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2021 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.automower.internal.rest.api.automowerconnect.dto; + +/** + * @author Marcin Czeczko - Initial contribution + */ +public class CalendarTask { + /** + * Start time expressed in minutes after midnight. + */ + private Integer start; + + /** + * Duration time expressed in minutes + */ + private Integer duration; + private Boolean monday; + private Boolean tuesday; + private Boolean wednesday; + private Boolean thursday; + private Boolean friday; + private Boolean saturday; + private Boolean sunday; + + public Integer getStart() { + return start; + } + + public Integer getDuration() { + return duration; + } + + public Boolean getMonday() { + return monday; + } + + public Boolean getTuesday() { + return tuesday; + } + + public Boolean getWednesday() { + return wednesday; + } + + public Boolean getThursday() { + return thursday; + } + + public Boolean getFriday() { + return friday; + } + + public Boolean getSaturday() { + return saturday; + } + + public Boolean getSunday() { + return sunday; + } +} diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Planner.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Planner.java index 35058167101aa..6dcb6730181cd 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Planner.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/Planner.java @@ -14,7 +14,37 @@ /** * @author Markus Pfleger - Initial contribution + * @author Marcin Czeczko - Added support for planner & calendar data */ public class Planner { + private long nextStartTimestamp; + private RestrictedReason restrictedReason; + private PlannerOverride override; + public long getNextStartTimestamp() { + return nextStartTimestamp; + } + + public Planner setNextStartTimestamp(long nextStartTimestamp) { + this.nextStartTimestamp = nextStartTimestamp; + return this; + } + + public RestrictedReason getRestrictedReason() { + return restrictedReason; + } + + public Planner setRestrictedReason(RestrictedReason restrictedReason) { + this.restrictedReason = restrictedReason; + return this; + } + + public PlannerOverride getOverride() { + return override; + } + + public Planner setOverride(PlannerOverride override) { + this.override = override; + return this; + } } diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/PlannerOverride.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/PlannerOverride.java new file mode 100644 index 0000000000000..32ec97ab6d2ca --- /dev/null +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/PlannerOverride.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.automower.internal.rest.api.automowerconnect.dto; + +/** + * @author Marcin Czeczko - Initial contribution + */ +public class PlannerOverride { + private String action; + + public String getAction() { + return action; + } + + public PlannerOverride setAction(String action) { + this.action = action; + return this; + } +} diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/RestrictedReason.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/RestrictedReason.java new file mode 100644 index 0000000000000..3e1c8b3a92a26 --- /dev/null +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/rest/api/automowerconnect/dto/RestrictedReason.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.automower.internal.rest.api.automowerconnect.dto; + +/** + * @author Marcin Czeczko - Initial Contribution + */ +public enum RestrictedReason { + NONE, + WEEK_SCHEDULE, + PARK_OVERRIDE, + SENSOR, + DAILY_LIMIT, + NOT_APPLICABLE +} diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerCommand.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerCommand.java index c8f8f19596be4..54dcfd6fa9b68 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerCommand.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerCommand.java @@ -12,25 +12,47 @@ */ package org.openhab.binding.automower.internal.things; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.openhab.core.thing.ChannelUID; + /** * @author Markus Pfleger - Initial contribution */ public enum AutomowerCommand { + START("Start", "mower#start"), + RESUME_SCHEDULE("ResumeSchedule", "mower#resume_schedule"), + PAUSE("Pause", "mower#pause"), + PARK("Park", "mower#park"), + PARK_UNTIL_NEXT_SCHEDULE("ParkUntilNextSchedule", "mower#park_until_next_schedule"), + PARK_UNTIL_FURTHER_NOTICE("ParkUntilFurtherNotice", "mower#park_until_further_notice"); - START("Start"), - RESUME_SCHEDULE("ResumeSchedule"), - PAUSE("Pause"), - PARK("Park"), - PARK_UNTIL_NEXT_SCHEDULE("ParkUntilNextSchedule"), - PARK_UNTIL_FURTHER_NOTICE("ParkUntilFurtherNotice"); + private static final Map CHANNEL_TO_CMD_MAP = new HashMap<>(); + + static { + EnumSet.allOf(AutomowerCommand.class).forEach(cmd -> CHANNEL_TO_CMD_MAP.put(cmd.getChannel(), cmd)); + } private final String command; + private final String channel; - private AutomowerCommand(String command) { + AutomowerCommand(String command, String channel) { this.command = command; + this.channel = channel; + } + + public static Optional fromChannelUID(ChannelUID channelUID) { + return Optional.ofNullable(CHANNEL_TO_CMD_MAP.get(channelUID.getId())); } public String getCommand() { return command; } + + public String getChannel() { + return channel; + } } diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerConfiguration.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerConfiguration.java index c2cadaaee02f6..734b8edd576d3 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerConfiguration.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerConfiguration.java @@ -22,7 +22,6 @@ */ @NonNullByDefault public class AutomowerConfiguration { - public @Nullable String mowerId; public @Nullable Integer pollingInterval; diff --git a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerHandler.java b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerHandler.java index 95c67128e8868..de705216ade68 100644 --- a/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerHandler.java +++ b/bundles/org.openhab.binding.automower/src/main/java/org/openhab/binding/automower/internal/things/AutomowerHandler.java @@ -14,12 +14,11 @@ import static org.openhab.binding.automower.internal.AutomowerBindingConstants.*; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -34,7 +33,10 @@ import org.openhab.binding.automower.internal.bridge.AutomowerBridge; import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler; import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower; +import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.RestrictedReason; +import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.State; import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; @@ -51,14 +53,19 @@ import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.Type; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; + /** * The {@link AutomowerHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Markus Pfleger - Initial contribution + * @author Marcin Czeczko - Added support for planner & calendar data */ @NonNullByDefault public class AutomowerHandler extends BaseThingHandler { @@ -68,12 +75,18 @@ public class AutomowerHandler extends BaseThingHandler { private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.MINUTES.toSeconds(10); private final Logger logger = LoggerFactory.getLogger(AutomowerHandler.class); + private final TimeZoneProvider timeZoneProvider; + private AtomicReference automowerId = new AtomicReference(NO_ID); private long lastQueryTimeMs = 0L; private @Nullable ScheduledFuture automowerPollingJob; private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1); + private @Nullable Mower mowerState; + + private Gson gson = new Gson(); + private Runnable automowerPollingRunnable = () -> { Bridge bridge = getBridge(); if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) { @@ -83,17 +96,32 @@ public class AutomowerHandler extends BaseThingHandler { } }; - public AutomowerHandler(Thing thing) { + public AutomowerHandler(Thing thing, TimeZoneProvider timeZoneProvider) { super(thing); + this.timeZoneProvider = timeZoneProvider; } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType) { + if (RefreshType.REFRESH == command) { + logger.debug("Refreshing channel '{}'", channelUID); refreshChannels(channelUID); + } else { + AutomowerCommand.fromChannelUID(channelUID).ifPresent(commandName -> { + logger.debug("Sending command '{}'", commandName); + getCommandValue(command).ifPresentOrElse(duration -> sendAutomowerCommand(commandName, duration), + () -> sendAutomowerCommand(commandName)); + }); } } + private Optional getCommandValue(Type type) { + if (type instanceof DecimalType) { + return Optional.of(((DecimalType) type).intValue()); + } + return Optional.empty(); + } + private void refreshChannels(ChannelUID channelUID) { updateAutomowerState(); } @@ -121,7 +149,6 @@ public void initialize() { automowerId.set(configMowerId); startAutomowerPolling(pollingIntervalS); } - } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); } @@ -163,49 +190,47 @@ private void stopAutomowerPolling() { } } - private boolean isValidResult(Mower mower) { - return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null + private boolean isValidResult(@Nullable Mower mower) { + return mower != null && mower.getAttributes() != null && mower.getAttributes().getMetadata() != null && mower.getAttributes().getBattery() != null && mower.getAttributes().getSystem() != null; } - private boolean isConnected(Mower mower) { - return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null + private boolean isConnected(@Nullable Mower mower) { + return mower != null && mower.getAttributes() != null && mower.getAttributes().getMetadata() != null && mower.getAttributes().getMetadata().isConnected(); } private synchronized void updateAutomowerState() { - if (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos) { - lastQueryTimeMs = System.nanoTime(); - String id = automowerId.get(); - try { - AutomowerBridge automowerBridge = getAutomowerBridge(); - if (automowerBridge != null) { - Mower mower = automowerBridge.getAutomowerStatus(id); - - if (isValidResult(mower)) { - initializeProperties(mower); - - updateChannelState(mower); - - if (isConnected(mower)) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/comm-error-mower-not-connected-to-cloud"); - } + String id = automowerId.get(); + try { + AutomowerBridge automowerBridge = getAutomowerBridge(); + if (automowerBridge != null) { + if (mowerState == null || (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos)) { + lastQueryTimeMs = System.nanoTime(); + mowerState = automowerBridge.getAutomowerStatus(id); + } + if (isValidResult(mowerState)) { + initializeProperties(mowerState); + + updateChannelState(mowerState); + + if (isConnected(mowerState)) { + updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/comm-error-query-mower-failed"); + "@text/comm-error-mower-not-connected-to-cloud"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/conf-error-no-bridge"); + "@text/comm-error-query-mower-failed"); } - } catch (AutomowerCommunicationException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/comm-error-query-mower-failed"); - logger.warn("Unable to query automower status for: {}. Error: {}", id, e.getMessage()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge"); } + } catch (AutomowerCommunicationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/comm-error-query-mower-failed"); + logger.warn("Unable to query automower status for: {}. Error: {}", id, e.getMessage()); } } @@ -228,6 +253,7 @@ public void sendAutomowerCommand(AutomowerCommand command) { * "Park" commands */ public void sendAutomowerCommand(AutomowerCommand command, long commandDurationMinutes) { + logger.debug("Sending command '{} {}'", command.getCommand(), commandDurationMinutes); String id = automowerId.get(); try { AutomowerBridge automowerBridge = getAutomowerBridge(); @@ -243,30 +269,53 @@ public void sendAutomowerCommand(AutomowerCommand command, long commandDurationM updateAutomowerState(); } - private void updateChannelState(Mower mower) { - if (isValidResult(mower)) { - updateState(CHANNEL_MOWER_NAME, new StringType(mower.getAttributes().getSystem().getName())); + private String restrictedState(RestrictedReason reason) { + return "RESTRICTED_" + reason.name(); + } + private void updateChannelState(@Nullable Mower mower) { + if (isValidResult(mower)) { + updateState(CHANNEL_STATUS_NAME, new StringType(mower.getAttributes().getSystem().getName())); updateState(CHANNEL_STATUS_MODE, new StringType(mower.getAttributes().getMower().getMode().name())); updateState(CHANNEL_STATUS_ACTIVITY, new StringType(mower.getAttributes().getMower().getActivity().name())); - updateState(CHANNEL_STATUS_STATE, new StringType(mower.getAttributes().getMower().getState().name())); - Instant statusTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMetadata().getStatusTimestamp()); + if (mower.getAttributes().getMower().getState() != State.RESTRICTED) { + updateState(CHANNEL_STATUS_STATE, new StringType(mower.getAttributes().getMower().getState().name())); + } else { + updateState(CHANNEL_STATUS_STATE, + new StringType(restrictedState(mower.getAttributes().getPlanner().getRestrictedReason()))); + } + updateState(CHANNEL_STATUS_LAST_UPDATE, - new DateTimeType(ZonedDateTime.ofInstant(statusTimestamp, ZoneId.systemDefault()))); + new DateTimeType(toZonedDateTime(mower.getAttributes().getMetadata().getStatusTimestamp()))); updateState(CHANNEL_STATUS_BATTERY, new QuantityType( mower.getAttributes().getBattery().getBatteryPercent(), Units.PERCENT)); updateState(CHANNEL_STATUS_ERROR_CODE, new DecimalType(mower.getAttributes().getMower().getErrorCode())); - Instant errorCodeTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMower().getErrorCodeTimestamp()); - updateState(CHANNEL_STATUS_ERROR_TIMESTAMP, - new DateTimeType(ZonedDateTime.ofInstant(errorCodeTimestamp, ZoneId.systemDefault()))); + long errorCodeTimestamp = mower.getAttributes().getMower().getErrorCodeTimestamp(); + if (errorCodeTimestamp == 0L) { + updateState(CHANNEL_STATUS_ERROR_TIMESTAMP, UnDefType.NULL); + } else { + updateState(CHANNEL_STATUS_ERROR_TIMESTAMP, new DateTimeType(toZonedDateTime(errorCodeTimestamp))); + } + + long nextStartTimestamp = mower.getAttributes().getPlanner().getNextStartTimestamp(); + // If next start timestamp is 0 it means the mower should start now, so using current timestamp + if (nextStartTimestamp == 0L) { + updateState(CHANNEL_PLANNER_NEXT_START, UnDefType.NULL); + } else { + updateState(CHANNEL_PLANNER_NEXT_START, new DateTimeType(toZonedDateTime(nextStartTimestamp))); + } + updateState(CHANNEL_PLANNER_OVERRIDE_ACTION, + new StringType(mower.getAttributes().getPlanner().getOverride().getAction())); + updateState(CHANNEL_CALENDAR_TASKS, + new StringType(gson.toJson(mower.getAttributes().getCalendar().getTasks()))); } } - private void initializeProperties(Mower mower) { + private void initializeProperties(@Nullable Mower mower) { Map properties = editProperties(); properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId()); @@ -280,4 +329,18 @@ private void initializeProperties(Mower mower) { updateProperties(properties); } + + /** + * Converts timestamp returned by the Automower API into local time-zone. + * Timestamp returned by the API doesn't have offset and it always in the current time zone - it can be treated as + * UTC. + * Method builds a ZonedDateTime with same hour value but in the current system timezone. + * + * @param timestamp - Automower API timestamp + * @return ZonedDateTime in system timezone + */ + private ZonedDateTime toZonedDateTime(long timestamp) { + Instant timestampInstant = Instant.ofEpochMilli(timestamp); + return ZonedDateTime.ofInstant(timestampInstant, timeZoneProvider.getTimeZone()); + } } diff --git a/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/i18n/automower.properties b/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/i18n/automower.properties index 1f42b1aacae4f..67ca840657ef3 100644 --- a/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/i18n/automower.properties +++ b/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/i18n/automower.properties @@ -13,8 +13,6 @@ thing-type.config.automower.automower.timestamp.label = Last State Update thing-type.config.automower.automower.batteryPct.label = Battery Percentage # channel types -channel-type.automower.name.label = Name -channel-type.automower.name.description = Automower name channel-type.automower.mode.label = Mode channel-type.automower.mode.description = Mode channel-type.automower.activity.label = Activity @@ -23,7 +21,14 @@ channel-type.automower.state.label = State channel-type.automower.state.description = State channel-type.automower.last-update.label = Last Update channel-type.automower.last-update.description = Last Update - +channel-type.automower.planner-next-start.label= Next planned start +channel-type.automower.planner-next-start.description= Next planned start +channel-type.automower.planner-override-action.label = Planner override action +channel-type.automower.planner-override-action.description = Planner override action +channel-type.automower.planner-restricted-reason.label = Planner restriction +channel-type.automower.planner-restricted-reason.description = Planner restriction +channel-type.automower.calendar-tasks.label = The information about the planner as JSON +channel-type.automower.calendar-tasks.description = The information about the planner as JSON conf-error-no-app-key = Cannot connect to Automower bridge as no app key is available in the configuration conf-error-no-username = Cannot connect to Automower bridge as no username is available in the configuration diff --git a/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/thing/thing-types.xml index 07a3d3be6729d..5172a21455bdc 100644 --- a/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.automower/src/main/resources/OH-INF/thing/thing-types.xml @@ -12,8 +12,10 @@ - The Application Key is required to communication with the Automower Connect Api. It can be obtained by - registering an Application on the Husqvarna Website. This application also needs to be connected to the + The Application Key is required to communication with the Automower Connect Api at + https://developer.husqvarnagroup.cloud/. It can be obtained by + registering an Application on the Husqvarna Website. + This application also needs to be connected to the "Authentication API" and the "Automower Connect API" @@ -21,7 +23,7 @@ The user name for which the application key has been issued - Password + password The password for the given user @@ -48,9 +50,18 @@ - + + + + + + + + + + @@ -126,7 +137,11 @@ - + + + + + @@ -164,4 +179,64 @@ + + DateTime + + The channel providing the time for the next auto start. If the mower is charging then the value is the + estimated time when it will be leaving the charging station. If the mower is about to start now, the value is NULL. + + + + + String + + The channel providing an action that overrides current planner operation. + + + + + String + + The channel providing a JSON with the information about the planner tasks. + + + + + Number + + Start for a duration in minutes + + + + + Switch + + Resume schedule + + + + Switch + + Pause the mower now until manual resume + + + + Number + + Park for a duration in minutes + + + + + Switch + + Park until next schedule + + + + Switch + + Park and pause the mower schedule until manual resume + + diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/FritzAhaWebInterface.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/FritzAhaWebInterface.java index cb298b542a8a0..8eb2854acb8ac 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/FritzAhaWebInterface.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/FritzAhaWebInterface.java @@ -242,9 +242,13 @@ public String addSID(String path) { String content = contentResponse.getContentAsString(); logger.debug("GET response complete: {}", content); return content; - } catch (ExecutionException | InterruptedException | TimeoutException e) { + } catch (ExecutionException | TimeoutException e) { logger.debug("response failed: {}", e.getLocalizedMessage(), e); return null; + } catch (InterruptedException e) { + logger.debug("response interrupted: {}", e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + return null; } } diff --git a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/BigAssFanConfig.java b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/BigAssFanConfig.java index 647572c288039..74b1dc7a70465 100644 --- a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/BigAssFanConfig.java +++ b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/BigAssFanConfig.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.bigassfan.internal; -import org.apache.commons.lang.StringUtils; - /** * The {@link BigAssFanConfig} is responsible for storing the BigAssFan thing configuration. * @@ -60,13 +58,13 @@ public void setMacAddress(String macAddress) { } public boolean isValid() { - if (StringUtils.isBlank(label)) { + if (label == null || label.isBlank()) { return false; } - if (StringUtils.isBlank(ipAddress)) { + if (ipAddress == null || ipAddress.isBlank()) { return false; } - if (StringUtils.isBlank(macAddress)) { + if (macAddress == null || macAddress.isBlank()) { return false; } return true; diff --git a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java index 4c2bf94b760a4..eacc428f217d3 100644 --- a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java +++ b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java @@ -40,7 +40,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.bigassfan.internal.BigAssFanConfig; import org.openhab.binding.bigassfan.internal.utils.BigAssFanConverter; import org.openhab.core.common.ThreadPoolManager; @@ -668,7 +667,7 @@ private String readMessage() { } private void processMessage(String incomingMessage) { - if (StringUtils.isEmpty(incomingMessage)) { + if (incomingMessage == null || incomingMessage.isEmpty()) { return; } @@ -742,11 +741,11 @@ private void processMessage(String incomingMessage) { private boolean isMe(String idFromDevice) { // Check match on MAC address - if (StringUtils.equalsIgnoreCase(idFromDevice, macAddress)) { + if (macAddress.equalsIgnoreCase(idFromDevice)) { return true; } // Didn't match MAC address, check match for label - if (StringUtils.equalsIgnoreCase(idFromDevice, label)) { + if (label.equalsIgnoreCase(idFromDevice)) { return true; } return false; diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java index f31aa533676f8..4cac3551856c4 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java @@ -24,8 +24,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BeaconBluetoothHandler; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; +import org.openhab.binding.bluetooth.BluetoothUtils; import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SIUnits; @@ -156,8 +156,35 @@ private void read() { case IDLE: logger.debug("Read data from device {}...", address); BluetoothCharacteristic characteristic = device.getCharacteristic(uuid); - if (characteristic != null && device.readCharacteristic(characteristic)) { + + if (characteristic != null) { readState = ReadState.READING; + device.readCharacteristic(characteristic).whenComplete((data, ex) -> { + try { + if (data != null) { + logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), + address, data); + updateStatus(ThingStatus.ONLINE); + sinceLastReadSec.set(0); + try { + updateChannels( + new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data))); + } catch (AirthingsParserException e) { + logger.warn( + "Data parsing error occured, when parsing data from device {}, cause {}", + address, e.getMessage(), e); + } + } else { + logger.debug("Characteristic {} from device {} failed: {}", + characteristic.getUuid(), address, ex.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + ex.getMessage()); + } + } finally { + readState = ReadState.IDLE; + disconnect(); + } + }); } else { logger.debug("Read data from device {} failed", address); disconnect(); @@ -205,30 +232,6 @@ public void onConnectionStateChange(BluetoothConnectionStatusNotification connec execute(); } - @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - try { - if (status == BluetoothCompletionStatus.SUCCESS) { - logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address, - characteristic.getValue()); - updateStatus(ThingStatus.ONLINE); - sinceLastReadSec.set(0); - try { - updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue())); - } catch (AirthingsParserException e) { - logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address, - e.getMessage(), e); - } - } else { - logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device"); - } - } finally { - readState = ReadState.IDLE; - disconnect(); - } - } - private void updateChannels(AirthingsWavePlusDataParser parser) { logger.debug("Parsed data: {}", parser); updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT)); diff --git a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java index 2588dd0620eac..c0be052e3c5ee 100644 --- a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java +++ b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.bluetooth.am43.internal; -import java.util.Arrays; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -24,7 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; import org.openhab.binding.bluetooth.ConnectedBluetoothHandler; import org.openhab.binding.bluetooth.am43.internal.command.AM43Command; @@ -164,7 +162,7 @@ private void processCommand(AM43Command command) { command.setState(AM43Command.State.FAILED); return; } - if (!resolved) { + if (!device.isServicesDiscovered()) { logger.debug("Unable to send command {} to device {}: services not resolved", command, device.getAddress()); command.setState(AM43Command.State.FAILED); @@ -180,9 +178,15 @@ private void processCommand(AM43Command command) { // there is no consequence to calling this as much as we like device.enableNotifications(characteristic); - characteristic.setValue(command.getRequest()); command.setState(AM43Command.State.ENQUEUED); - device.writeCharacteristic(characteristic); + device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> { + if (t != null) { + logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage()); + command.setState(AM43Command.State.FAILED); + } else { + command.setState(AM43Command.State.SENT); + } + }); if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS, AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) { @@ -197,39 +201,8 @@ private void processCommand(AM43Command command) { } @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - super.onCharacteristicWriteComplete(characteristic, status); - - byte[] request = characteristic.getByteValue(); - - AM43Command command = currentCommand; - - if (command != null) { - if (!Arrays.equals(request, command.getRequest())) { - logger.debug("Write completed for unknown command"); - return; - } - switch (status) { - case SUCCESS: - command.setState(AM43Command.State.SENT); - break; - case ERROR: - command.setState(AM43Command.State.FAILED); - break; - } - } else { - if (logger.isDebugEnabled()) { - logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request)); - } - } - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); - - byte[] response = characteristic.getByteValue(); + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) { + super.onCharacteristicUpdate(characteristic, response); AM43Command command = currentCommand; if (command == null) { diff --git a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/command/AM43Command.java b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/command/AM43Command.java index 92ce6cc0e2cf7..1204e11b6fd78 100644 --- a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/command/AM43Command.java +++ b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/command/AM43Command.java @@ -18,7 +18,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java index afa8ba4843b83..43206c9b1c37f 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java @@ -27,7 +27,7 @@ */ public class BlueGigaBluetoothCharacteristic extends BluetoothCharacteristic { - private boolean notificationEnabled; + private boolean notifying; public BlueGigaBluetoothCharacteristic(int handle) { super(null, handle); @@ -45,11 +45,11 @@ public void setUUID(UUID uuid) { this.uuid = uuid; } - public boolean isNotificationEnabled() { - return notificationEnabled; + public boolean isNotifying() { + return notifying; } - public void setNotificationEnabled(boolean enable) { - this.notificationEnabled = enable; + public void setNotifying(boolean enable) { + this.notifying = enable; } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java index c2cb969e8fe4e..3fce5a0e451db 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java @@ -19,9 +19,11 @@ import java.util.NavigableMap; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -29,10 +31,11 @@ import org.openhab.binding.bluetooth.BluetoothAddress; import org.openhab.binding.bluetooth.BluetoothBindingConstants; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDescriptor; import org.openhab.binding.bluetooth.BluetoothDevice; +import org.openhab.binding.bluetooth.BluetoothException; import org.openhab.binding.bluetooth.BluetoothService; +import org.openhab.binding.bluetooth.BluetoothUtils; import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler; import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener; import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse; @@ -66,6 +69,14 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class); + private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE); + private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure( + BlueGigaProcedure.Type.GET_SERVICES); + private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure( + BlueGigaProcedure.Type.GET_CHARACTERISTICS); + private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure( + BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL); + private Map handleToUUID = new HashMap<>(); private NavigableMap handleToCharacteristic = new TreeMap<>(); @@ -75,22 +86,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue // The dongle handler private final BlueGigaBridgeHandler bgHandler; - // An enum to use in the state machine for interacting with the device - private enum BlueGigaProcedure { - NONE, - GET_SERVICES, - GET_CHARACTERISTICS, - READ_CHARACTERISTIC_DECL, - CHARACTERISTIC_READ, - CHARACTERISTIC_WRITE, - NOTIFICATION_ENABLE, - NOTIFICATION_DISABLE - } - - private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE; - - // Somewhere to remember what characteristic we're working on - private @Nullable BluetoothCharacteristic procedureCharacteristic; + private BlueGigaProcedure currentProcedure = PROCEDURE_NONE; // The connection handle if the device is connected private int connection = -1; @@ -113,9 +109,24 @@ public void run() { private Runnable procedureTimeoutTask = new Runnable() { @Override public void run() { - logger.debug("Procedure {} timeout for device {}", procedureProgress, address); - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; + BlueGigaProcedure procedure = currentProcedure; + logger.debug("Procedure {} timeout for device {}", procedure.type, address); + switch (procedure.type) { + case CHARACTERISTIC_READ: + ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure; + readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic " + + readProcedure.characteristic.getUuid() + " timeout for device " + address)); + break; + case CHARACTERISTIC_WRITE: + WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure; + writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic " + + writeProcedure.characteristic.getUuid() + " timeout for device " + address)); + break; + default: + break; + } + + currentProcedure = PROCEDURE_NONE; } }; @@ -174,7 +185,7 @@ public boolean disconnect() { @Override public boolean discoverServices() { - if (procedureProgress != BlueGigaProcedure.NONE) { + if (currentProcedure != PROCEDURE_NONE) { return false; } @@ -184,49 +195,45 @@ public boolean discoverServices() { } procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.GET_SERVICES; + currentProcedure = PROCEDURE_GET_SERVICES; return true; } @Override - public boolean enableNotifications(BluetoothCharacteristic characteristic) { + public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) { if (connection == -1) { - logger.debug("Cannot enable notifications, device not connected {}", this); - return false; + return CompletableFuture.failedFuture(new BluetoothException("Not connected")); } BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic; - if (ch.isNotificationEnabled()) { - return true; + if (ch.isNotifying()) { + return CompletableFuture.completedFuture(null); } BluetoothDescriptor descriptor = ch .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID()); if (descriptor == null || descriptor.getHandle() == 0) { - logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid()); - return false; + return CompletableFuture.failedFuture( + new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]")); } - if (procedureProgress != BlueGigaProcedure.NONE) { - logger.debug("Procedure already in progress {}", procedureProgress); - return false; + if (currentProcedure != PROCEDURE_NONE) { + return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress")); } int[] value = { 1, 0 }; - byte[] bvalue = toBytes(value); - descriptor.setValue(bvalue); cancelTimer(procedureTimer); if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) { - logger.debug("bgWriteCharacteristic returned false"); - return false; + return CompletableFuture.failedFuture(new BluetoothException( + "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]")); } procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE; - procedureCharacteristic = characteristic; - + WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch, + BlueGigaProcedure.Type.NOTIFICATION_ENABLE); + currentProcedure = notifyProcedure; try { // we intentionally sleep here in order to give this procedure a chance to complete. // ideally we would use locks/conditions to make this wait until completiong but @@ -235,57 +242,52 @@ public boolean enableNotifications(BluetoothCharacteristic characteristic) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - return true; + return notifyProcedure.writeFuture; } @Override - public boolean disableNotifications(BluetoothCharacteristic characteristic) { + public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) { if (connection == -1) { - logger.debug("Cannot enable notifications, device not connected {}", this); - return false; + return CompletableFuture.failedFuture(new BluetoothException("Not connected")); } BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic; - if (ch.isNotificationEnabled()) { - return true; + if (!ch.isNotifying()) { + return CompletableFuture.completedFuture(null); } BluetoothDescriptor descriptor = ch .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID()); if (descriptor == null || descriptor.getHandle() == 0) { - logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid()); - return false; + return CompletableFuture.failedFuture( + new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]")); } - if (procedureProgress != BlueGigaProcedure.NONE) { - logger.debug("Procedure already in progress {}", procedureProgress); - return false; + if (currentProcedure != PROCEDURE_NONE) { + return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress")); } int[] value = { 0, 0 }; - byte[] bvalue = toBytes(value); - descriptor.setValue(bvalue); cancelTimer(procedureTimer); if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) { - logger.debug("bgWriteCharacteristic returned false"); - return false; + return CompletableFuture.failedFuture(new BluetoothException( + "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]")); } procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE; - procedureCharacteristic = characteristic; + WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch, + BlueGigaProcedure.Type.NOTIFICATION_DISABLE); + currentProcedure = notifyProcedure; - try { - // we intentionally sleep here in order to give this procedure a chance to complete. - // ideally we would use locks/conditions to make this wait until completiong but - // I have a better solution planned for later. - Connor Petty - Thread.sleep(500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return true; + return notifyProcedure.writeFuture; + } + + @Override + public boolean isNotifying(BluetoothCharacteristic characteristic) { + BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic; + return ch.isNotifying(); } @Override @@ -301,52 +303,56 @@ public boolean disableNotifications(BluetoothDescriptor descriptor) { } @Override - public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) { - if (characteristic == null || characteristic.getHandle() == 0) { - return false; + public CompletableFuture readCharacteristic(BluetoothCharacteristic characteristic) { + if (characteristic.getHandle() == 0) { + return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle")); } if (connection == -1) { - return false; + return CompletableFuture.failedFuture(new BluetoothException("Not connected")); } - if (procedureProgress != BlueGigaProcedure.NONE) { - return false; + if (currentProcedure != PROCEDURE_NONE) { + return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress")); } cancelTimer(procedureTimer); if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) { - return false; + return CompletableFuture.failedFuture( + new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]")); } procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ; - procedureCharacteristic = characteristic; + ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic); + currentProcedure = readProcedure; - return true; + return readProcedure.readFuture; } @Override - public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) { - if (characteristic == null || characteristic.getHandle() == 0) { - return false; + public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) { + if (characteristic.getHandle() == 0) { + return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle")); } if (connection == -1) { - return false; + return CompletableFuture.failedFuture(new BluetoothException("Not connected")); } - if (procedureProgress != BlueGigaProcedure.NONE) { - return false; + if (currentProcedure != PROCEDURE_NONE) { + return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress")); } cancelTimer(procedureTimer); - if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) { - return false; + if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), + BluetoothUtils.toIntArray(value))) { + return CompletableFuture.failedFuture( + new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]")); } procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE; - procedureCharacteristic = characteristic; + WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure( + (BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE); + currentProcedure = writeProcedure; - return true; + return writeProcedure.writeFuture; } @Override @@ -558,7 +564,7 @@ private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event return; } - if (procedureProgress == BlueGigaProcedure.NONE) { + if (currentProcedure == PROCEDURE_NONE) { logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}", connection, address); return; @@ -568,63 +574,73 @@ private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event updateLastSeenTime(); // The current procedure is now complete - move on... - switch (procedureProgress) { + switch (currentProcedure.type) { case GET_SERVICES: // We've downloaded all services, now get the characteristics if (bgHandler.bgFindCharacteristics(connection)) { procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS; + currentProcedure = PROCEDURE_GET_CHARACTERISTICS; } else { - procedureProgress = BlueGigaProcedure.NONE; + currentProcedure = PROCEDURE_NONE; } break; case GET_CHARACTERISTICS: // We've downloaded all attributes, now read the characteristic declarations if (bgHandler.bgReadCharacteristicDeclarations(connection)) { procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC); - procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL; + currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL; } else { - procedureProgress = BlueGigaProcedure.NONE; + currentProcedure = PROCEDURE_NONE; } break; case READ_CHARACTERISTIC_DECL: // We've downloaded read all the declarations, we are done now - procedureProgress = BlueGigaProcedure.NONE; + currentProcedure = PROCEDURE_NONE; notifyListeners(BluetoothEventType.SERVICES_DISCOVERED); break; case CHARACTERISTIC_READ: // The read failed - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic, - BluetoothCompletionStatus.ERROR); - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; + ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure; + readProcedure.readFuture.completeExceptionally(new BluetoothException( + "Read characteristic failed: " + readProcedure.characteristic.getUuid())); + currentProcedure = PROCEDURE_NONE; break; case CHARACTERISTIC_WRITE: // The write completed - failure or success - BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS - ? BluetoothCompletionStatus.SUCCESS - : BluetoothCompletionStatus.ERROR; - notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result); - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; + WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure; + if (event.getResult() == BgApiResponse.SUCCESS) { + writeProcedure.writeFuture.complete(null); + } else { + writeProcedure.writeFuture.completeExceptionally(new BluetoothException( + "Write characteristic failed: " + writeProcedure.characteristic.getUuid())); + } + currentProcedure = PROCEDURE_NONE; break; case NOTIFICATION_ENABLE: + WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure; boolean success = event.getResult() == BgApiResponse.SUCCESS; - if (!success) { - logger.debug("write to descriptor failed"); + if (success) { + notifyEnableProcedure.writeFuture.complete(null); + } else { + notifyEnableProcedure.writeFuture + .completeExceptionally(new BluetoothException("Enable characteristic notification failed: " + + notifyEnableProcedure.characteristic.getUuid())); } - ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(success); - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; + notifyEnableProcedure.characteristic.setNotifying(success); + currentProcedure = PROCEDURE_NONE; break; case NOTIFICATION_DISABLE: + WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure; success = event.getResult() == BgApiResponse.SUCCESS; - if (!success) { - logger.debug("write to descriptor failed"); + if (success) { + notifyDisableProcedure.writeFuture.complete(null); + } else { + notifyDisableProcedure.writeFuture + .completeExceptionally(new BluetoothException("Disable characteristic notification failed: " + + notifyDisableProcedure.characteristic.getUuid())); } - ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(!success); - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; + notifyDisableProcedure.characteristic.setNotifying(!success); + currentProcedure = PROCEDURE_NONE; break; default: break; @@ -656,13 +672,29 @@ private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) { } for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) { - ch.setNotificationEnabled(false); + ch.setNotifying(false); } cancelTimer(procedureTimer); connectionState = ConnectionState.DISCONNECTED; connection = -1; - procedureProgress = BlueGigaProcedure.NONE; + + BlueGigaProcedure procedure = currentProcedure; + switch (procedure.type) { + case CHARACTERISTIC_READ: + ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure; + readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic " + + readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address)); + break; + case CHARACTERISTIC_WRITE: + WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure; + writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic " + + writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address)); + break; + default: + break; + } + currentProcedure = PROCEDURE_NONE; notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(connectionState)); @@ -701,42 +733,30 @@ private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) { return; } if (handle == characteristic.getHandle()) { - characteristic.setValue(event.getValue().clone()); - + byte[] value = BluetoothUtils.toByteArray(event.getValue()); + BlueGigaProcedure procedure = currentProcedure; // If this is the characteristic we were reading, then send a read completion - if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null - && procedureCharacteristic.getHandle() == event.getAttHandle()) { - procedureProgress = BlueGigaProcedure.NONE; - procedureCharacteristic = null; - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, - BluetoothCompletionStatus.SUCCESS); - return; + if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) { + ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure; + if (readProcedure.characteristic.getHandle() == event.getAttHandle()) { + readProcedure.readFuture.complete(value); + currentProcedure = PROCEDURE_NONE; + return; + } } - // Notify the user of the updated value - notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic); + notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value); } else { // it must be one of the descriptors we need to update UUID attUUID = handleToUUID.get(handle); BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID); - descriptor.setValue(toBytes(event.getValue())); - notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor); - } - } - - private static byte @Nullable [] toBytes(int @Nullable [] value) { - if (value == null) { - return null; - } - byte[] ret = new byte[value.length]; - for (int i = 0; i < value.length; i++) { - ret[i] = (byte) value[i]; + notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor, + BluetoothUtils.toByteArray(event.getValue())); } - return ret; } private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) { - ByteBuffer buffer = ByteBuffer.wrap(toBytes(value)); + ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value)); buffer.order(ByteOrder.LITTLE_ENDIAN); ch.setProperties(Byte.toUnsignedInt(buffer.get())); @@ -773,7 +793,7 @@ public void dispose() { cancelTimer(connectTimer); cancelTimer(procedureTimer); bgHandler.removeEventListener(this); - procedureProgress = BlueGigaProcedure.NONE; + currentProcedure = PROCEDURE_NONE; connectionState = ConnectionState.DISCOVERING; connection = -1; } @@ -787,4 +807,48 @@ private void cancelTimer(@Nullable ScheduledFuture task) { private ScheduledFuture startTimer(Runnable command, long timeout) { return scheduler.schedule(command, timeout, TimeUnit.SECONDS); } + + private static class BlueGigaProcedure { + private final Type type; + + public BlueGigaProcedure(Type type) { + this.type = type; + } + + // An enum to use in the state machine for interacting with the device + enum Type { + NONE, + GET_SERVICES, + GET_CHARACTERISTICS, + READ_CHARACTERISTIC_DECL, + CHARACTERISTIC_READ, + CHARACTERISTIC_WRITE, + NOTIFICATION_ENABLE, + NOTIFICATION_DISABLE + } + } + + private static class ReadCharacteristicProcedure extends BlueGigaProcedure { + + private final BluetoothCharacteristic characteristic; + + private final CompletableFuture readFuture = new CompletableFuture<>(); + + public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) { + super(Type.CHARACTERISTIC_READ); + this.characteristic = characteristic; + } + } + + private static class WriteCharacteristicProcedure extends BlueGigaProcedure { + + private final BlueGigaBluetoothCharacteristic characteristic; + + private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>(); + + public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) { + super(type); + this.characteristic = characteristic; + } + } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java index e57e3d56a5d40..7bd7cdb168237 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java @@ -24,6 +24,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -158,6 +159,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler removeInactiveDevicesTask; private @Nullable ScheduledFuture discoveryTask; + private @Nullable ScheduledFuture initTask; private @Nullable Future passiveScanIdleTimer; @@ -168,15 +170,40 @@ public BlueGigaBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) @Override public void initialize() { - logger.info("Initializing BlueGiga"); super.initialize(); - Optional cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class)); updateStatus(ThingStatus.UNKNOWN); + if (initTask == null) { + initTask = scheduler.scheduleWithFixedDelay(this::checkInit, 0, 10, TimeUnit.SECONDS); + } + } + + protected void checkInit() { + boolean init = false; + try { + if (!serialHandler.get().isAlive()) { + logger.debug("BLE serial handler seems to be dead, reinitilize"); + stop(); + init = true; + } + } catch (InterruptedException e) { + return; + } catch (ExecutionException e) { + init = true; + } + + if (init) { + logger.debug("Initialize BlueGiga"); + start(); + } + } + + private void start() { + Optional cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class)); if (cfg.isPresent()) { + initComplete = false; configuration = cfg.get(); serialPortFuture = RetryFuture.callWithRetry(() -> { var localFuture = serialPortFuture; - logger.debug("Initialize BlueGiga"); logger.debug("Using configuration: {}", configuration); String serialPortName = configuration.port; @@ -288,13 +315,16 @@ public void initialize() { @Override public void dispose() { - logger.info("Disposing BlueGiga"); + if (initTask != null) { + initTask.cancel(true); + initTask = null; + } stop(); - stopScheduledTasks(); super.dispose(); } private void stop() { + logger.info("Stop BlueGiga"); transactionManager.thenAccept(tman -> { tman.removeEventListener(this); tman.close(); @@ -309,6 +339,7 @@ private void stop() { serialPortFuture.thenAccept(this::closeSerialPort); serialPortFuture.cancel(false); + stopScheduledTasks(); } private void schedulePassiveScan() { @@ -764,8 +795,7 @@ public void bluegigaEventReceived(@Nullable BlueGigaResponse event) { @Override public void bluegigaClosed(Exception reason) { - logger.debug("BlueGiga connection closed, request reinitialization"); + logger.debug("BlueGiga connection closed, request reinitialization, reason: {}", reason.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason.getMessage()); - initComplete = false; } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaResponsePackets.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaResponsePackets.java index 01f61fabf7616..56fe26fc413b1 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaResponsePackets.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaResponsePackets.java @@ -199,7 +199,7 @@ public static BlueGigaResponse getPacket(int[] data) { return bleFrame; } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - logger.error("Error instantiating BLE class", e); + logger.debug("Error instantiating BLE class", e); } return null; diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java index c08d70ce3f7e2..be2fad17b61bf 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java @@ -67,6 +67,8 @@ public BlueGigaSerialHandler(final String uid, final InputStream inputStream, fi parserThread = createBlueGigaBLEHandler(uid); parserThread.setUncaughtExceptionHandler((t, th) -> { logger.warn("BluegigaSerialHandler terminating due to unhandled error", th); + notifyEventListeners(new BlueGigaException( + "BluegigaSerialHandler terminating due to unhandled error, reason " + th.getMessage())); }); parserThread.setDaemon(true); parserThread.start(); @@ -238,7 +240,6 @@ private void checkIfAlive() { private void inboundMessageHandlerLoop() { final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 }; - int exceptionCnt = 0; logger.trace("BlueGiga BLE thread started"); int[] inputBuffer = new int[BLE_MAX_LENGTH]; int inputCount = 0; @@ -260,9 +261,8 @@ private void inboundMessageHandlerLoop() { } if (inputCount < 4) { - // The BGAPI protocol has no packet framing, and no error detection, so we do a few - // sanity checks on the header to try and allow resyncronisation should there be an - // error. + // The BGAPI protocol has no packet framing and no error detection, so we do a few + // sanity checks on the header to try and allow resynchronisation. // Byte 0: Check technology type is bluetooth and high length is 0 // Byte 1: Check length is less than 64 bytes // Byte 2: Check class ID is less than 8 @@ -278,21 +278,22 @@ private void inboundMessageHandlerLoop() { } else if (inputCount == 4) { // Process the header to get the length inputLength = inputBuffer[1] + (inputBuffer[0] & 0x02 << 8) + 4; - if (inputLength > 64) { - logger.debug("BLE length larger than 64 bytes ({})", inputLength); + if (inputLength > BLE_MAX_LENGTH) { + logger.debug("Received illegal BLE packet, length larger than max {} bytes ({})", + BLE_MAX_LENGTH, inputLength); if (inputStream.markSupported()) { inputStream.reset(); } inputCount = 0; + inputLength = 0; continue; } - } - if (inputCount == inputLength) { + } else if (inputCount == inputLength) { + // End of packet reached - process if (logger.isTraceEnabled()) { logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength)); } - // End of packet reached - process BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer); if (logger.isTraceEnabled()) { @@ -300,24 +301,16 @@ private void inboundMessageHandlerLoop() { } if (responsePacket != null) { notifyEventListeners(responsePacket); + } else { + logger.debug("Unknown packet received: {}", printHex(inputBuffer, inputLength)); } inputCount = 0; - exceptionCnt = 0; - } - - } catch (IOException e) { - logger.debug("BlueGiga BLE IOException: ", e); - - if (exceptionCnt++ > 10) { - logger.error("BlueGiga BLE exception count exceeded, closing handler"); - close = true; - notifyEventListeners(e); } } catch (Exception e) { - logger.debug("BlueGiga BLE Exception, closing handler", e); + logger.trace("BlueGiga BLE Exception: ", e); close = true; - notifyEventListeners(e); + notifyEventListeners(new BlueGigaException("BlueGiga BLE Exception, reason " + e.getMessage(), e)); } } logger.debug("BlueGiga BLE exited."); diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java index f4d78ab591e64..4f865323a26d4 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java @@ -15,10 +15,10 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.bluez.exceptions.BluezFailedException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.freedesktop.dbus.errors.NoReply; @@ -28,7 +28,6 @@ import org.openhab.binding.bluetooth.BaseBluetoothDevice; import org.openhab.binding.bluetooth.BluetoothAddress; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDescriptor; import org.openhab.binding.bluetooth.BluetoothService; import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent; @@ -42,6 +41,8 @@ import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent; import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; import org.openhab.binding.bluetooth.notification.BluetoothScanNotification; +import org.openhab.binding.bluetooth.util.RetryException; +import org.openhab.binding.bluetooth.util.RetryFuture; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.util.HexUtils; import org.slf4j.Logger; @@ -57,6 +58,7 @@ * * @author Kai Kreuzer - Initial contribution and API * @author Benjamin Lafois - Replaced tinyB with bluezDbus + * @author Peter Rosenberg - Improve notifications and properties support * */ @NonNullByDefault @@ -256,57 +258,65 @@ private void ensureConnected() { } @Override - public boolean enableNotifications(BluetoothCharacteristic characteristic) { - ensureConnected(); + public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) { + BluetoothDevice dev = device; + if (dev == null || !dev.isConnected()) { + return CompletableFuture + .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected")); + } BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); - if (c != null) { + if (c == null) { + logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); + return CompletableFuture.failedFuture( + new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device")); + } + return RetryFuture.callWithRetry(() -> { try { c.startNotify(); } catch (DBusException e) { if (e.getMessage().contains("Already notifying")) { - return false; + return null; } else if (e.getMessage().contains("In Progress")) { - // let's retry in 10 seconds - scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS); + // let's retry in half a second + throw new RetryException(500, TimeUnit.MILLISECONDS); } else { logger.warn("Exception occurred while activating notifications on '{}'", address, e); + throw e; } } - return true; - } else { - logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); - return false; - } + return null; + }, scheduler); } @Override - public boolean writeCharacteristic(BluetoothCharacteristic characteristic) { + public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) { logger.debug("writeCharacteristic()"); - ensureConnected(); + BluetoothDevice dev = device; + if (dev == null || !dev.isConnected()) { + return CompletableFuture + .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected")); + } BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); if (c == null) { logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); - return false; + return CompletableFuture.failedFuture( + new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device")); } - scheduler.submit(() -> { + return RetryFuture.callWithRetry(() -> { try { - c.writeValue(characteristic.getByteValue(), null); - notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic, - BluetoothCompletionStatus.SUCCESS); - + c.writeValue(value, null); + return null; } catch (DBusException e) { logger.debug("Exception occurred when trying to write characteristic '{}': {}", characteristic.getUuid(), e.getMessage()); - notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic, - BluetoothCompletionStatus.ERROR); + throw e; } - }); - return true; + }, scheduler); } @Override @@ -363,10 +373,7 @@ public void onCharacteristicNotify(CharacteristicUpdateEvent event) { } BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid())); if (c != null) { - synchronized (c) { - c.setValue(event.getData()); - notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS); - } + notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData()); } } @@ -399,6 +406,7 @@ public boolean discoverServices() { for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) { BluetoothCharacteristic characteristic = new BluetoothCharacteristic( UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0); + convertCharacteristicProperties(dBusBlueZCharacteristic, characteristic); for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) { BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic, @@ -414,49 +422,111 @@ public boolean discoverServices() { return true; } + /** + * Convert the flags of BluetoothGattCharacteristic to the int bitset used by BluetoothCharacteristic. + * + * @param dBusBlueZCharacteristic source characteristic to read the flags from + * @param characteristic destination characteristic to write to properties to + */ + private void convertCharacteristicProperties(BluetoothGattCharacteristic dBusBlueZCharacteristic, + BluetoothCharacteristic characteristic) { + int properties = 0; + + for (String property : dBusBlueZCharacteristic.getFlags()) { + switch (property) { + case "broadcast": + properties |= BluetoothCharacteristic.PROPERTY_BROADCAST; + break; + case "read": + properties |= BluetoothCharacteristic.PROPERTY_READ; + break; + case "write-without-response": + properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE; + break; + case "write": + properties |= BluetoothCharacteristic.PROPERTY_WRITE; + break; + case "notify": + properties |= BluetoothCharacteristic.PROPERTY_NOTIFY; + break; + case "indicate": + properties |= BluetoothCharacteristic.PROPERTY_INDICATE; + break; + } + } + + characteristic.setProperties(properties); + } + @Override - public boolean readCharacteristic(BluetoothCharacteristic characteristic) { + public CompletableFuture readCharacteristic(BluetoothCharacteristic characteristic) { + BluetoothDevice dev = device; + if (dev == null || !dev.isConnected()) { + return CompletableFuture + .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected")); + } + BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); if (c == null) { logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); - return false; + return CompletableFuture.failedFuture( + new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device")); } - scheduler.submit(() -> { + return RetryFuture.callWithRetry(() -> { try { - byte[] value = c.readValue(null); - characteristic.setValue(value); - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, - BluetoothCompletionStatus.SUCCESS); - } catch (DBusException e) { + return c.readValue(null); + } catch (DBusException | DBusExecutionException e) { + // DBusExecutionException is thrown if the value cannot be read logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(), e.getMessage()); - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, - BluetoothCompletionStatus.ERROR); + throw e; } - }); - return true; + }, scheduler); } @Override - public boolean disableNotifications(BluetoothCharacteristic characteristic) { + public boolean isNotifying(BluetoothCharacteristic characteristic) { BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); if (c != null) { + Boolean isNotifying = c.isNotifying(); + return Objects.requireNonNullElse(isNotifying, false); + } else { + logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); + return false; + } + } + + @Override + public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) { + BluetoothDevice dev = device; + if (dev == null || !dev.isConnected()) { + return CompletableFuture + .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected")); + } + BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); + if (c == null) { + logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); + return CompletableFuture.failedFuture( + new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device")); + } + + return RetryFuture.callWithRetry(() -> { try { c.stopNotify(); - } catch (BluezFailedException e) { - if (e.getMessage().contains("In Progress")) { - // let's retry in 10 seconds - scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS); + } catch (DBusException e) { + if (e.getMessage().contains("Already notifying")) { + return null; + } else if (e.getMessage().contains("In Progress")) { + // let's retry in half a second + throw new RetryException(500, TimeUnit.MILLISECONDS); } else { - logger.warn("Exception occurred while activating notifications on '{}'", address, e); + logger.warn("Exception occurred while deactivating notifications on '{}'", address, e); + throw e; } } - return true; - } else { - logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); - return false; - } + return null; + }, scheduler); } @Override diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java index 75a8b73cbc176..0abeade1f80ff 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java @@ -51,7 +51,7 @@ public class DeviceManagerFactory { private final BlueZPropertiesChangedHandler changeHandler = new BlueZPropertiesChangedHandler(); - private @Nullable CompletableFuture deviceManagerFuture; + private @Nullable CompletableFuture<@Nullable DeviceManager> deviceManagerFuture; private @Nullable CompletableFuture deviceManagerWrapperFuture; public BlueZPropertiesChangedHandler getPropertiesChangedHandler() { @@ -79,7 +79,13 @@ public void initialize() { // Experimental - seems reuse does not work } catch (IllegalStateException e) { // Exception caused by first call to the library - return DeviceManager.createInstance(false); + try { + return DeviceManager.createInstance(false); + } catch (DBusException ex) { + // we might be on a system without DBus, such as macOS or Windows + logger.debug("Failed to initialize DeviceManager: {}", ex.getMessage()); + return null; + } } }, scheduler); @@ -90,8 +96,10 @@ public void initialize() { int count = tryCount.incrementAndGet(); try { logger.debug("Registering property handler attempt: {}", count); - devManager.registerPropertyHandler(changeHandler); - logger.debug("Successfully registered property handler"); + if (devManager != null) { + devManager.registerPropertyHandler(changeHandler); + logger.debug("Successfully registered property handler"); + } return new DeviceManagerWrapper(devManager); } catch (DBusException e) { if (count < 3) { @@ -103,7 +111,12 @@ public void initialize() { }, scheduler); }).whenComplete((devManagerWrapper, th) -> { if (th != null) { - logger.warn("Failed to initialize DeviceManager: {}", th.getMessage()); + if (th.getCause() instanceof DBusException) { + // we might be on a system without DBus, such as macOS or Windows + logger.debug("Failed to initialize DeviceManager: {}", th.getMessage()); + } else { + logger.warn("Failed to initialize DeviceManager: {}", th.getMessage()); + } } }); } @@ -114,7 +127,11 @@ public void dispose() { if (stage1 != null) { if (!stage1.cancel(true)) { // a failure to cancel means that the stage completed normally - stage1.thenAccept(DeviceManager::closeConnection); + stage1.thenAccept(devManager -> { + if (devManager != null) { + devManager.closeConnection(); + } + }); } } this.deviceManagerFuture = null; diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java index 93453d99d8348..cbfeeae06207b 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.List; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -32,25 +33,32 @@ @NonNullByDefault public class DeviceManagerWrapper { - private DeviceManager deviceManager; + private @Nullable DeviceManager deviceManager; - public DeviceManagerWrapper(DeviceManager deviceManager) { + public DeviceManagerWrapper(@Nullable DeviceManager deviceManager) { this.deviceManager = deviceManager; } public synchronized Collection scanForBluetoothAdapters() { - return deviceManager.scanForBluetoothAdapters(); + if (deviceManager != null) { + return deviceManager.scanForBluetoothAdapters(); + } else { + return Set.of(); + } } public synchronized @Nullable BluetoothAdapter getAdapter(BluetoothAddress address) { - // we don't use `deviceManager.getAdapter` here since it might perform a scan if the adapter is missing. - String addr = address.toString(); - List adapters = deviceManager.getAdapters(); - if (adapters != null) { - for (BluetoothAdapter btAdapter : adapters) { - String btAddr = btAdapter.getAddress(); - if (addr.equalsIgnoreCase(btAddr)) { - return btAdapter; + DeviceManager devMgr = deviceManager; + if (devMgr != null) { + // we don't use `deviceManager.getAdapter` here since it might perform a scan if the adapter is missing. + String addr = address.toString(); + List adapters = devMgr.getAdapters(); + if (adapters != null) { + for (BluetoothAdapter btAdapter : adapters) { + String btAddr = btAdapter.getAddress(); + if (addr.equalsIgnoreCase(btAddr)) { + return btAdapter; + } } } } @@ -58,6 +66,10 @@ public synchronized Collection scanForBluetoothAdapters() { } public synchronized List getDevices(BluetoothAdapter adapter) { - return deviceManager.getDevices(adapter.getAddress(), true); + if (deviceManager != null) { + return deviceManager.getDevices(adapter.getAddress(), true); + } else { + return List.of(); + } } } diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java index e8169113b7bee..d1a2ba510010c 100644 --- a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java @@ -12,13 +12,14 @@ */ package org.openhab.binding.bluetooth.daikinmadoka.handler; -import java.util.Arrays; import java.util.Random; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.measure.quantity.Temperature; import javax.measure.quantity.Time; @@ -27,7 +28,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; import org.openhab.binding.bluetooth.ConnectedBluetoothHandler; import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants; @@ -350,12 +350,12 @@ private void resetCleanFilterIndicator() { } @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] msgBytes) { if (logger.isDebugEnabled()) { logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(), - HexUtils.bytesToHex(characteristic.getByteValue())); + HexUtils.bytesToHex(msgBytes)); } - super.onCharacteristicUpdate(characteristic); + super.onCharacteristicUpdate(characteristic, msgBytes); // Check that arguments are valid. if (characteristic.getUuid() == null) { @@ -367,9 +367,8 @@ public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { return; } - // A message cannot be null or have a 0-byte length - byte[] msgBytes = characteristic.getByteValue(); - if (msgBytes == null || msgBytes.length == 0) { + // A message cannot have a 0-byte length + if (msgBytes.length == 0) { return; } @@ -398,7 +397,7 @@ private void processCommand(BRC1HCommand command) { return; } - if (!resolved) { + if (!device.isServicesDiscovered()) { logger.debug("Unable to send command {} to device {}: services not resolved", command.getClass().getSimpleName(), device.getAddress()); command.setState(BRC1HCommand.State.FAILED); @@ -424,17 +423,23 @@ private void processCommand(BRC1HCommand command) { // Commands can be composed of multiple chunks for (byte[] chunk : command.getRequest()) { - charWrite.setValue(chunk); command.setState(BRC1HCommand.State.ENQUEUED); for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) { - if (device.writeCharacteristic(charWrite)) { - command.setState(BRC1HCommand.State.SENT); - synchronized (command) { - command.wait(100); - } - break; + try { + device.writeCharacteristic(charWrite, chunk).get(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + return; + } catch (ExecutionException ex) { + logger.debug("Error while writing message {}: {}", command.getClass().getSimpleName(), + ex.getMessage()); + Thread.sleep(100); + continue; + } catch (TimeoutException ex) { + Thread.sleep(100); + continue; } - Thread.sleep(100); + command.setState(BRC1HCommand.State.SENT); + break; } } @@ -459,39 +464,6 @@ private void processCommand(BRC1HCommand command) { } } - @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - super.onCharacteristicWriteComplete(characteristic, status); - - byte[] request = characteristic.getByteValue(); - BRC1HCommand command = currentCommand; - - if (command != null) { - // last chunk: - byte[] lastChunk = command.getRequest()[command.getRequest().length - 1]; - if (!Arrays.equals(request, lastChunk)) { - logger.debug("Write completed for a chunk, but not a complete command."); - synchronized (command) { - command.notify(); - } - return; - } - switch (status) { - case SUCCESS: - command.setState(BRC1HCommand.State.SENT); - break; - case ERROR: - command.setState(BRC1HCommand.State.FAILED); - break; - } - } else { - if (logger.isDebugEnabled()) { - logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request)); - } - } - } - /** * When the method is triggered, it means that all message chunks have been received, re-assembled in the right * order and that the payload is ready to be processed. diff --git a/bundles/org.openhab.binding.bluetooth.enoceanble/src/main/java/org/openhab/binding/bluetooth/enoceanble/internal/EnoceanBleDiscoveryParticipant.java b/bundles/org.openhab.binding.bluetooth.enoceanble/src/main/java/org/openhab/binding/bluetooth/enoceanble/internal/EnoceanBleDiscoveryParticipant.java index cfc1711db168e..6e7bd0d40f6b1 100644 --- a/bundles/org.openhab.binding.bluetooth.enoceanble/src/main/java/org/openhab/binding/bluetooth/enoceanble/internal/EnoceanBleDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.bluetooth.enoceanble/src/main/java/org/openhab/binding/bluetooth/enoceanble/internal/EnoceanBleDiscoveryParticipant.java @@ -52,7 +52,7 @@ public Set getSupportedThingTypeUIDs() { @Override public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) { Integer manufacturerId = device.getManufacturerId(); - logger.warn("Discovered device {} with manufacturerId {} and name {}", device.getAddress(), manufacturerId, + logger.debug("Discovered device {} with manufacturerId {} and name {}", device.getAddress(), manufacturerId, device.getName()); if (manufacturerId != null && manufacturerId == ENOCEAN_COMPANY_ID) { return new ThingUID(EnoceanBleBindingConstants.THING_TYPE_PTM215B, device.getAdapter().getUID(), diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java index 5717b08210729..37a594e4d2d37 100644 --- a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java @@ -27,7 +27,6 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothBindingConstants; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; import org.openhab.binding.bluetooth.ConnectedBluetoothHandler; import org.openhab.core.library.types.StringType; @@ -58,6 +57,7 @@ * channels based off of a bluetooth device's GATT characteristics. * * @author Connor Petty - Initial contribution + * @author Peter Rosenberg - Use notifications * */ @NonNullByDefault @@ -68,6 +68,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler { private final Map channelHandlers = new ConcurrentHashMap<>(); private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault(); private final CharacteristicChannelTypeProvider channelTypeProvider; + private final Map> handlerToChannels = new ConcurrentHashMap<>(); private @Nullable ScheduledFuture readCharacteristicJob = null; @@ -83,13 +84,15 @@ public void initialize() { GenericBindingConfiguration config = getConfigAs(GenericBindingConfiguration.class); readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> { if (device.getConnectionState() == ConnectionState.CONNECTED) { - if (resolved) { - for (CharacteristicHandler charHandler : charHandlers.values()) { - if (charHandler.canRead()) { + if (device.isServicesDiscovered()) { + handlerToChannels.forEach((charHandler, channelUids) -> { + // Only read the value manually if notification is not on. + // Also read it the first time before we activate notifications below. + if (!device.isNotifying(charHandler.characteristic) && charHandler.canRead()) { device.readCharacteristic(charHandler.characteristic); try { // TODO the ideal solution would be to use locks/conditions and timeouts - // between this code and `onCharacteristicReadComplete` but + // Kbetween this code and `onCharacteristicReadComplete` but // that would overcomplicate the code a bit and I plan // on implementing a better more generalized solution later Thread.sleep(50); @@ -97,7 +100,20 @@ public void initialize() { return; } } - } + if (charHandler.characteristic.canNotify()) { + // Enabled/Disable notifications dependent on if the channel is linked. + // TODO check why isLinked() is true for not linked channels + if (channelUids.stream().anyMatch(this::isLinked)) { + if (!device.isNotifying(charHandler.characteristic)) { + device.enableNotifications(charHandler.characteristic); + } + } else { + if (device.isNotifying(charHandler.characteristic)) { + device.disableNotifications(charHandler.characteristic); + } + } + } + }); } else { // if we are connected and still haven't been able to resolve the services, try disconnecting and // then connecting again @@ -117,15 +133,14 @@ public void dispose() { charHandlers.clear(); channelHandlers.clear(); + handlerToChannels.clear(); } @Override public void onServicesDiscovered() { - if (!resolved) { - resolved = true; - logger.trace("Service discovery completed for '{}'", address); - updateThingChannels(); - } + super.onServicesDiscovered(); + logger.trace("Service discovery completed for '{}'", address); + updateThingChannels(); } @Override @@ -139,19 +154,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - super.onCharacteristicReadComplete(characteristic, status); - if (status == BluetoothCompletionStatus.SUCCESS) { - byte[] data = characteristic.getByteValue(); - getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data); - } - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); - byte[] data = characteristic.getByteValue(); - getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data); + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { + super.onCharacteristicUpdate(characteristic, value); + getCharacteristicHandler(characteristic).handleCharacteristicUpdate(value); } private void updateThingChannels() { @@ -161,9 +166,11 @@ private void updateThingChannels() { logger.trace("{} processing characteristic {}", address, characteristic.getUuid()); CharacteristicHandler handler = getCharacteristicHandler(characteristic); List chans = handler.buildChannels(); - for (Channel channel : chans) { - channelHandlers.put(channel.getUID(), handler); + List chanUids = chans.stream().map(Channel::getUID).collect(Collectors.toList()); + for (ChannelUID channel : chanUids) { + channelHandlers.put(channel, handler); } + handlerToChannels.put(handler, chanUids); return chans.stream(); })// .collect(Collectors.toList()); @@ -187,13 +194,28 @@ private CharacteristicHandler getCharacteristicHandler(BluetoothCharacteristic c return Objects.requireNonNull(charHandlers.computeIfAbsent(characteristic, CharacteristicHandler::new)); } - private boolean readCharacteristic(BluetoothCharacteristic characteristic) { - return device.readCharacteristic(characteristic); + private void readCharacteristic(BluetoothCharacteristic characteristic) { + readCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid()).whenComplete((data, th) -> { + if (th != null) { + logger.warn("Could not read data from characteristic {} of device {}: {}", characteristic.getUuid(), + address, th.getMessage()); + return; + } + if (data != null) { + getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data); + } + }); } - private boolean writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) { - characteristic.setValue(data); - return device.writeCharacteristic(characteristic); + private void writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) { + writeCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid(), data, false) + .whenComplete((r, th) -> { + if (th != null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Could not write data to characteristic " + characteristic.getUuid() + ": " + + th.getMessage()); + } + }); } private class CharacteristicHandler { @@ -233,10 +255,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (state instanceof StringType) { // unknown characteristic byte[] data = HexUtils.hexToBytes(state.toString()); - if (!writeCharacteristic(characteristic, data)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not write data to characteristic: " + characteristicUUID); - } + writeCharacteristic(characteristic, data); } } catch (RuntimeException ex) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -255,10 +274,7 @@ private void updateCharacteristic(String fieldName, State state) { BluetoothChannelUtils.updateHolder(gattParser, request, fieldName, state); byte[] data = gattParser.serialize(request); - if (!writeCharacteristic(characteristic, data)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not write data to characteristic: " + characteristicUUID); - } + writeCharacteristic(characteristic, data); } catch (NumberFormatException ex) { logger.warn("Could not parse characteristic value: {} : {}", characteristicUUID, state, ex); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -341,8 +357,7 @@ public boolean canRead() { if (gattParser.isKnownCharacteristic(charUUID)) { return gattParser.isValidForRead(charUUID); } - // TODO: need to evaluate this from characteristic properties, but such properties aren't support yet - return true; + return characteristic.canRead(); } public boolean canWrite() { @@ -350,8 +365,7 @@ public boolean canWrite() { if (gattParser.isKnownCharacteristic(charUUID)) { return gattParser.isValidForWrite(charUUID); } - // TODO: need to evaluate this from characteristic properties, but such properties aren't support yet - return true; + return characteristic.canWrite(); } private boolean isAdvanced() { diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml index 0f51494318fc4..a4f76990c780c 100644 --- a/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml @@ -5,6 +5,12 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> + + + + + + A generic bluetooth device that supports GATT characteristics @@ -19,6 +25,11 @@ The frequency at which readable characteristics refreshed 30 + + + If enabled, will automatically connect to the device and reconnect if connection is lost. + true + diff --git a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java deleted file mode 100644 index a6f3bdbe69a11..0000000000000 --- a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java +++ /dev/null @@ -1,472 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.bluetooth.govee.internal; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.Condition; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.bluetooth.BeaconBluetoothHandler; -import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; -import org.openhab.binding.bluetooth.BluetoothDescriptor; -import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; -import org.openhab.binding.bluetooth.BluetoothService; -import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; -import org.openhab.core.common.NamedThreadFactory; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.util.HexUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices. - * - * @author Kai Kreuzer - Initial contribution and API - * @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted - */ -@Deprecated -@NonNullByDefault -public class ConnectedBluetoothHandler extends BeaconBluetoothHandler { - - private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class); - - private final Condition connectionCondition = deviceLock.newCondition(); - private final Condition serviceDiscoveryCondition = deviceLock.newCondition(); - private final Condition charCompleteCondition = deviceLock.newCondition(); - - private @Nullable Future reconnectJob; - private @Nullable Future pendingDisconnect; - private @Nullable BluetoothCharacteristic ongoingCharacteristic; - private @Nullable BluetoothCompletionStatus completeStatus; - - private boolean connectOnDemand; - private int idleDisconnectDelayMs = 1000; - - protected @Nullable ScheduledExecutorService connectionTaskExecutor; - private volatile boolean servicesDiscovered; - - public ConnectedBluetoothHandler(Thing thing) { - super(thing); - } - - @Override - public void initialize() { - - // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize - // the connectionTaskExecutor first - ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, - new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true)); - executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); - executor.setRemoveOnCancelPolicy(true); - connectionTaskExecutor = executor; - - super.initialize(); - - connectOnDemand = true; - - Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay"); - idleDisconnectDelayMs = 1000; - if (idleDisconnectDelayRaw instanceof Number) { - idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue(); - } - - if (!connectOnDemand) { - reconnectJob = executor.scheduleWithFixedDelay(() -> { - try { - if (device.getConnectionState() != ConnectionState.CONNECTED) { - device.connect(); - // we do not set the Thing status here, because we will anyhow receive a call to - // onConnectionStateChange - } else { - // just in case it was already connected to begin with - updateStatus(ThingStatus.ONLINE); - if (!servicesDiscovered && !device.discoverServices()) { - logger.debug("Error while discovering services"); - } - } - } catch (RuntimeException ex) { - logger.warn("Unexpected error occurred", ex); - } - }, 0, 30, TimeUnit.SECONDS); - } - } - - @Override - public void dispose() { - cancel(reconnectJob); - reconnectJob = null; - cancel(pendingDisconnect); - pendingDisconnect = null; - - super.dispose(); - - shutdown(connectionTaskExecutor); - connectionTaskExecutor = null; - } - - private static void cancel(@Nullable Future future) { - if (future != null) { - future.cancel(true); - } - } - - private void shutdown(@Nullable ScheduledExecutorService executor) { - if (executor != null) { - executor.shutdownNow(); - } - } - - private ScheduledExecutorService getConnectionTaskExecutor() { - var executor = connectionTaskExecutor; - if (executor == null) { - throw new IllegalStateException("characteristicScheduler has not been initialized"); - } - return executor; - } - - private void scheduleDisconnect() { - cancel(pendingDisconnect); - pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs, - TimeUnit.MILLISECONDS); - } - - private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException { - if (device.getConnectionState() == ConnectionState.CONNECTED) { - return; - } - if (device.getConnectionState() != ConnectionState.CONNECTING) { - if (!device.connect()) { - throw new ConnectionException("Failed to start connecting"); - } - } - logger.debug("waiting for connection"); - if (!awaitConnection(1, TimeUnit.SECONDS)) { - throw new TimeoutException("Connection attempt timeout."); - } - logger.debug("connection successful"); - if (!servicesDiscovered) { - logger.debug("discovering services"); - device.discoverServices(); - if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) { - throw new TimeoutException("Service discovery timeout"); - } - logger.debug("service discovery successful"); - } - } - - private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException { - deviceLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (device.getConnectionState() != ConnectionState.CONNECTED) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = connectionCondition.awaitNanos(nanosTimeout); - } - } finally { - deviceLock.unlock(); - } - return true; - } - - private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException { - deviceLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (ongoingCharacteristic != null) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout); - } - } finally { - deviceLock.unlock(); - } - return true; - } - - private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException { - deviceLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (!servicesDiscovered) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout); - } - } finally { - deviceLock.unlock(); - } - return true; - } - - private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID) - throws BluetoothException, TimeoutException, InterruptedException { - connectAndWait(); - BluetoothService service = device.getServices(serviceUUID); - if (service == null) { - throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found"); - } - BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID); - if (characteristic == null) { - throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found"); - } - return characteristic; - } - - private CompletableFuture executeWithConnection(UUID serviceUUID, UUID characteristicUUID, - CallableFunction callable) { - CompletableFuture future = new CompletableFuture<>(); - var executor = connectionTaskExecutor; - if (executor != null) { - executor.execute(() -> { - cancel(pendingDisconnect); - try { - BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID, - characteristicUUID); - future.complete(callable.call(characteristic)); - } catch (InterruptedException e) { - future.completeExceptionally(e); - return;// we don't want to schedule anything if we receive an interrupt - } catch (TimeoutException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - future.completeExceptionally(e); - } catch (Exception e) { - future.completeExceptionally(e); - } - if (connectOnDemand) { - scheduleDisconnect(); - } - }); - } else { - future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized")); - } - return future; - } - - public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) { - return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> { - if (!device.enableNotifications(characteristic)) { - throw new BluetoothException( - "Failed to start notifications for characteristic: " + characteristic.getUuid()); - } - return null; - }); - } - - public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data, - boolean enableNotification) { - return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> { - if (enableNotification) { - if (!device.enableNotifications(characteristic)) { - throw new BluetoothException( - "Failed to start characteristic notification" + characteristic.getUuid()); - } - } - // now block for completion - characteristic.setValue(data); - ongoingCharacteristic = characteristic; - if (!device.writeCharacteristic(characteristic)) { - throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid()); - } - if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) { - ongoingCharacteristic = null; - throw new TimeoutException( - "Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish"); - } - if (completeStatus == BluetoothCompletionStatus.ERROR) { - throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid()); - } - logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data), - characteristic.getUuid(), address); - return null; - }); - } - - public CompletableFuture readCharacteristic(UUID serviceUUID, UUID characteristicUUID) { - return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> { - // now block for completion - ongoingCharacteristic = characteristic; - if (!device.readCharacteristic(characteristic)) { - throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid()); - } - if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) { - ongoingCharacteristic = null; - throw new TimeoutException( - "Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish"); - } - if (completeStatus == BluetoothCompletionStatus.ERROR) { - throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid()); - } - byte[] data = characteristic.getByteValue(); - logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address, - HexUtils.bytesToHex(data)); - return data; - }); - } - - @Override - protected void updateStatusBasedOnRssi(boolean receivedSignal) { - // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether - // we are connected. - if (receivedSignal) { - if (device.getConnectionState() == ConnectionState.CONNECTED) { - updateStatus(ThingStatus.ONLINE); - } else { - if (!connectOnDemand) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected."); - } - } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } - } - - @Override - public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) { - super.onConnectionStateChange(connectionNotification); - switch (connectionNotification.getConnectionState()) { - case DISCOVERED: - // The device is now known on the Bluetooth network, so we can do something... - if (!connectOnDemand) { - getConnectionTaskExecutor().submit(() -> { - if (device.getConnectionState() != ConnectionState.CONNECTED) { - if (!device.connect()) { - logger.debug("Error connecting to device after discovery."); - } - } - }); - } - break; - case CONNECTED: - deviceLock.lock(); - try { - connectionCondition.signal(); - } finally { - deviceLock.unlock(); - } - if (!connectOnDemand) { - getConnectionTaskExecutor().submit(() -> { - if (!servicesDiscovered && !device.discoverServices()) { - logger.debug("Error while discovering services"); - } - }); - } - break; - case DISCONNECTED: - var future = pendingDisconnect; - if (future != null) { - future.cancel(false); - } - if (!connectOnDemand) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } - break; - default: - break; - } - } - - @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - super.onCharacteristicReadComplete(characteristic, status); - deviceLock.lock(); - try { - if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) { - completeStatus = status; - ongoingCharacteristic = null; - charCompleteCondition.signal(); - } - } finally { - deviceLock.unlock(); - } - } - - @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - super.onCharacteristicWriteComplete(characteristic, status); - deviceLock.lock(); - try { - if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) { - completeStatus = status; - ongoingCharacteristic = null; - charCompleteCondition.signal(); - } - } finally { - deviceLock.unlock(); - } - } - - @Override - public void onServicesDiscovered() { - super.onServicesDiscovered(); - deviceLock.lock(); - try { - this.servicesDiscovered = true; - serviceDiscoveryCondition.signal(); - } finally { - deviceLock.unlock(); - } - logger.debug("Service discovery completed for '{}'", address); - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); - if (logger.isDebugEnabled()) { - logger.debug("Recieved update {} to characteristic {} of device {}", - HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address); - } - } - - @Override - public void onDescriptorUpdate(BluetoothDescriptor descriptor) { - super.onDescriptorUpdate(descriptor); - if (logger.isDebugEnabled()) { - logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()), - descriptor.getUuid(), address); - } - } - - public static class BluetoothException extends Exception { - - public BluetoothException(String message) { - super(message); - } - } - - public static class ConnectionException extends BluetoothException { - - public ConnectionException(String message) { - super(message); - } - } - - @FunctionalInterface - public static interface CallableFunction { - public R call(U arg) throws Exception; - } -} diff --git a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java index 21caa88cfff3f..6072c3a6dbaa5 100644 --- a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java +++ b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java @@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothCharacteristic; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; +import org.openhab.binding.bluetooth.ConnectedBluetoothHandler; import org.openhab.binding.bluetooth.gattserial.MessageServicer; import org.openhab.binding.bluetooth.gattserial.SimpleGattSocket; import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetBatteryCommand; @@ -93,6 +94,11 @@ public GoveeHygrometerHandler(Thing thing) { @Override public void initialize() { super.initialize(); + if (thing.getStatus() == ThingStatus.OFFLINE) { + // something went wrong in super.initialize() so we shouldn't initialize further here either + return; + } + config = getConfigAs(GoveeHygrometerConfiguration.class); Map properties = thing.getProperties(); @@ -117,14 +123,14 @@ public void initialize() { logger.debug("refreshing temperature, humidity, and battery"); refreshBattery().join(); refreshTemperatureAndHumidity().join(); - connectionTaskExecutor.execute(device::disconnect); + disconnect(); updateStatus(ThingStatus.ONLINE); } } catch (RuntimeException ex) { logger.warn("unable to refresh", ex); } }, 0, config.refreshInterval, TimeUnit.SECONDS); - keepAliveJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> { + keepAliveJob = scheduler.scheduleWithFixedDelay(() -> { if (device.getConnectionState() == ConnectionState.CONNECTED) { try { GoveeMessage message = new GoveeMessage((byte) 0xAA, (byte) 1, null); @@ -393,9 +399,9 @@ private void updateTemHumBattery(short tem, int hum, int battery, int wifiLevel) } @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); - commandSocket.receivePacket(characteristic.getByteValue()); + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { + super.onCharacteristicUpdate(characteristic, value); + commandSocket.receivePacket(value); } private class CommandSocket extends SimpleGattSocket { diff --git a/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java index 92f3af7b7a46f..79cbcfd937c87 100644 --- a/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java @@ -25,7 +25,6 @@ import org.openhab.binding.bluetooth.BluetoothAdapter; import org.openhab.binding.bluetooth.BluetoothAddress; import org.openhab.binding.bluetooth.BluetoothCharacteristic; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothDescriptor; import org.openhab.binding.bluetooth.BluetoothDevice; import org.openhab.binding.bluetooth.BluetoothDeviceListener; @@ -135,32 +134,16 @@ public void onServicesDiscovered() { } @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { if (device == getDelegate()) { - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, status); + notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value); } } @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { + public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) { if (device == getDelegate()) { - notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic); - } - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - if (device == getDelegate()) { - notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic); - } - } - - @Override - public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) { - if (device == getDelegate()) { - notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor); + notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor, value); } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java index 210d43213001d..c6422cc746289 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java @@ -19,6 +19,10 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -80,6 +84,12 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice { */ private final Set eventListeners = new CopyOnWriteArraySet<>(); + private final Lock deviceLock = new ReentrantLock(); + private final Condition connectionCondition = deviceLock.newCondition(); + private final Condition serviceDiscoveryCondition = deviceLock.newCondition(); + + private volatile boolean servicesDiscovered = false; + /** * Construct a Bluetooth device taking the Bluetooth address * @@ -258,6 +268,45 @@ protected Collection getListeners() { protected void dispose() { } + @Override + public boolean isServicesDiscovered() { + return servicesDiscovered; + } + + @Override + public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException { + deviceLock.lock(); + try { + long nanosTimeout = unit.toNanos(timeout); + while (getConnectionState() != ConnectionState.CONNECTED) { + if (nanosTimeout <= 0L) { + return false; + } + nanosTimeout = connectionCondition.awaitNanos(nanosTimeout); + } + } finally { + deviceLock.unlock(); + } + return true; + } + + @Override + public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException { + deviceLock.lock(); + try { + long nanosTimeout = unit.toNanos(timeout); + while (!servicesDiscovered) { + if (nanosTimeout <= 0L) { + return false; + } + nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout); + } + } finally { + deviceLock.unlock(); + } + return true; + } + @Override protected void notifyListeners(BluetoothEventType event, Object... args) { switch (event) { @@ -270,6 +319,27 @@ protected void notifyListeners(BluetoothEventType event, Object... args) { default: break; } + switch (event) { + case SERVICES_DISCOVERED: + deviceLock.lock(); + try { + servicesDiscovered = true; + serviceDiscoveryCondition.signal(); + } finally { + deviceLock.unlock(); + } + break; + case CONNECTION_STATE: + deviceLock.lock(); + try { + connectionCondition.signal(); + } finally { + deviceLock.unlock(); + } + break; + default: + break; + } super.notifyListeners(event, args); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java index deb1cce3279a8..ac39941c87cb4 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java @@ -19,7 +19,6 @@ import javax.measure.quantity.Power; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; @@ -193,7 +192,7 @@ protected void updateAdapterLocation() { if (device != null) { BluetoothAdapter adapter = device.getAdapter(); String location = adapter.getLocation(); - if (location != null || StringUtils.isBlank(location)) { + if (location != null && !location.isBlank()) { updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, new StringType(location)); } else { updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, UnDefType.NULL); @@ -215,7 +214,7 @@ protected void updateStatusBasedOnRssi(boolean receivedSignal) { } } - private void onActivity() { + protected void onActivity() { this.lastActivityTime = ZonedDateTime.now(); } @@ -242,27 +241,12 @@ public void onServicesDiscovered() { } @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - if (status == BluetoothCompletionStatus.SUCCESS) { - onActivity(); - } - } - - @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - if (status == BluetoothCompletionStatus.SUCCESS) { - onActivity(); - } - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { onActivity(); } @Override - public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) { + public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) { onActivity(); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java index 9e8a591bcad02..77509f1551166 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java @@ -43,6 +43,8 @@ public class BluetoothBindingConstants { public static final String CONFIGURATION_ADDRESS = "address"; public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery"; + public static final String CONFIGURATION_ALWAYS_CONNECTED = "alwaysConnected"; + public static final String CONFIGURATION_IDLE_DISCONNECT_DELAY = "idleDisconnectDelay"; public static final long BLUETOOTH_BASE_UUID = 0x800000805f9b34fbL; diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java index 0a014ecb36e93..b452997df5308 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.bluetooth; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -31,17 +30,9 @@ * * @author Chris Jackson - Initial contribution * @author Kai Kreuzer - Cleaned up code + * @author Peter Rosenberg - Improve properties support */ public class BluetoothCharacteristic { - public static final int FORMAT_UINT8 = 0x11; - public static final int FORMAT_UINT16 = 0x12; - public static final int FORMAT_UINT32 = 0x14; - public static final int FORMAT_SINT8 = 0x21; - public static final int FORMAT_SINT16 = 0x22; - public static final int FORMAT_SINT32 = 0x24; - public static final int FORMAT_SFLOAT = 0x32; - public static final int FORMAT_FLOAT = 0x34; - public static final int PROPERTY_BROADCAST = 0x01; public static final int PROPERTY_READ = 0x02; public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04; @@ -85,11 +76,6 @@ public class BluetoothCharacteristic { protected int permissions; protected int writeType; - /** - * The raw data value for this characteristic - */ - protected int[] value = new int[0]; - /** * The {@link BluetoothService} to which this characteristic belongs */ @@ -142,6 +128,16 @@ public int getInstanceId() { return instance; } + /** + * Set the raw properties. The individual properties are represented as bits inside + * of this int value. + * + * @param properties of this Characteristic + */ + public void setProperties(int properties) { + this.properties = properties; + } + /** * Returns the properties of this characteristic. * @@ -152,6 +148,46 @@ public int getProperties() { return properties; } + /** + * Returns if the given characteristics property is enabled or not. + * + * @param property one of the Constants BluetoothCharacteristic.PROPERTY_XX + * @return true if this characteristic has the given property enabled, false if properties not set or + * the given property is not enabled. + */ + public boolean hasPropertyEnabled(int property) { + return (properties & property) != 0; + } + + /** + * Returns if notifications can be enabled on this characteristic. + * + * @return true if notifications can be enabled, false if notifications are not supported, characteristic is missing + * on device or notifications are not supported. + */ + public boolean canNotify() { + return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY); + } + + /** + * Returns if the value can be read on this characteristic. + * + * @return true if the value can be read, false otherwise. + */ + public boolean canRead() { + return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ); + } + + /** + * Returns if the value can be written on this characteristic. + * + * @return true if the value can be written with of without a response, false otherwise. + */ + public boolean canWrite() { + return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE) + || hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE); + } + /** * Returns the permissions for this characteristic. */ @@ -263,299 +299,6 @@ public boolean equals(Object obj) { return true; } - /** - * Get the stored value for this characteristic. - * - */ - public int[] getValue() { - return value; - } - - /** - * Get the stored value for this characteristic. - * - */ - public byte[] getByteValue() { - byte[] byteValue = new byte[value.length]; - for (int cnt = 0; cnt < value.length; cnt++) { - byteValue[cnt] = (byte) (value[cnt] & 0xFF); - } - return byteValue; - } - - /** - * Return the stored value of this characteristic. - * - */ - public Integer getIntValue(int formatType, int offset) { - if ((offset + getTypeLen(formatType)) > value.length) { - return null; - } - - switch (formatType) { - case FORMAT_UINT8: - return unsignedByteToInt(value[offset]); - - case FORMAT_UINT16: - return unsignedBytesToInt(value[offset], value[offset + 1]); - - case FORMAT_UINT32: - return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]); - - case FORMAT_SINT8: - return unsignedToSigned(unsignedByteToInt(value[offset]), 8); - - case FORMAT_SINT16: - return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16); - - case FORMAT_SINT32: - return unsignedToSigned( - unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32); - default: - logger.error("Unknown format type {} - no int value can be provided for it.", formatType); - } - - return null; - } - - /** - * Return the stored value of this characteristic. This doesn't read the remote data. - * - */ - public Float getFloatValue(int formatType, int offset) { - if ((offset + getTypeLen(formatType)) > value.length) { - return null; - } - - switch (formatType) { - case FORMAT_SFLOAT: - return bytesToFloat(value[offset], value[offset + 1]); - case FORMAT_FLOAT: - return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]); - default: - logger.error("Unknown format type {} - no float value can be provided for it.", formatType); - } - - return null; - } - - /** - * Return the stored value of this characteristic. This doesn't read the remote data. - * - */ - public String getStringValue(int offset) { - if (value == null || offset > value.length) { - return null; - } - byte[] strBytes = new byte[value.length - offset]; - for (int i = 0; i < (value.length - offset); ++i) { - strBytes[i] = (byte) value[offset + i]; - } - return new String(strBytes, StandardCharsets.UTF_8); - } - - /** - * Updates the locally stored value of this characteristic. - * - * @param value the value to set - * @return true, if it has been set successfully - */ - public boolean setValue(int[] value) { - this.value = value; - return true; - } - - /** - * Set the local value of this characteristic. - * - * @param value the value to set - * @param formatType the format of the value (as one of the FORMAT_* constants in this class) - * @param offset the offset to use when interpreting the value - * @return true, if it has been set successfully - */ - public boolean setValue(int value, int formatType, int offset) { - int len = offset + getTypeLen(formatType); - if (this.value == null) { - this.value = new int[len]; - } - if (len > this.value.length) { - return false; - } - int val = value; - switch (formatType) { - case FORMAT_SINT8: - val = intToSignedBits(value, 8); - // Fall-through intended - case FORMAT_UINT8: - this.value[offset] = (byte) (val & 0xFF); - break; - - case FORMAT_SINT16: - val = intToSignedBits(value, 16); - // Fall-through intended - case FORMAT_UINT16: - this.value[offset] = (byte) (val & 0xFF); - this.value[offset + 1] = (byte) ((val >> 8) & 0xFF); - break; - - case FORMAT_SINT32: - val = intToSignedBits(value, 32); - // Fall-through intended - case FORMAT_UINT32: - this.value[offset] = (byte) (val & 0xFF); - this.value[offset + 1] = (byte) ((val >> 8) & 0xFF); - this.value[offset + 2] = (byte) ((val >> 16) & 0xFF); - this.value[offset + 2] = (byte) ((val >> 24) & 0xFF); - break; - - default: - return false; - } - return true; - } - - /** - * Set the local value of this characteristic. - * - * @param mantissa the mantissa of the value - * @param exponent the exponent of the value - * @param formatType the format of the value (as one of the FORMAT_* constants in this class) - * @param offset the offset to use when interpreting the value - * @return true, if it has been set successfully - * - */ - public boolean setValue(int mantissa, int exponent, int formatType, int offset) { - int len = offset + getTypeLen(formatType); - if (value == null) { - value = new int[len]; - } - if (len > value.length) { - return false; - } - - switch (formatType) { - case FORMAT_SFLOAT: - int m = intToSignedBits(mantissa, 12); - int exp = intToSignedBits(exponent, 4); - value[offset] = (byte) (m & 0xFF); - value[offset + 1] = (byte) ((m >> 8) & 0x0F); - value[offset + 1] += (byte) ((exp & 0x0F) << 4); - break; - - case FORMAT_FLOAT: - m = intToSignedBits(mantissa, 24); - exp = intToSignedBits(exponent, 8); - value[offset] = (byte) (m & 0xFF); - value[offset + 1] = (byte) ((m >> 8) & 0xFF); - value[offset + 2] = (byte) ((m >> 16) & 0xFF); - value[offset + 2] += (byte) (exp & 0xFF); - break; - - default: - return false; - } - - return true; - } - - /** - * Set the local value of this characteristic. - * - * @param value the value to set - * @return true, if it has been set successfully - */ - public boolean setValue(byte[] value) { - this.value = new int[value.length]; - int cnt = 0; - for (byte val : value) { - this.value[cnt++] = val; - } - return true; - } - - /** - * Set the local value of this characteristic. - * - * @param value the value to set - * @return true, if it has been set successfully - */ - public boolean setValue(String value) { - this.value = new int[value.getBytes().length]; - int cnt = 0; - for (byte val : value.getBytes()) { - this.value[cnt++] = val; - } - return true; - } - - /** - * Returns the size of the requested value type. - */ - private int getTypeLen(int formatType) { - return formatType & 0xF; - } - - /** - * Convert a signed byte to an unsigned int. - */ - private int unsignedByteToInt(int value) { - return value & 0xFF; - } - - /** - * Convert signed bytes to a 16-bit unsigned int. - */ - private int unsignedBytesToInt(int value1, int value2) { - return value1 + (value2 << 8); - } - - /** - * Convert signed bytes to a 32-bit unsigned int. - */ - private int unsignedBytesToInt(int value1, int value2, int value3, int value4) { - return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24); - } - - /** - * Convert signed bytes to a 16-bit short float value. - */ - private float bytesToFloat(int value1, int value2) { - int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12); - int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4); - return (float) (mantissa * Math.pow(10, exponent)); - } - - /** - * Convert signed bytes to a 32-bit short float value. - */ - private float bytesToFloat(int value1, int value2, int value3, int value4) { - int mantissa = unsignedToSigned( - unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24); - return (float) (mantissa * Math.pow(10, value4)); - } - - /** - * Convert an unsigned integer to a two's-complement signed value. - */ - private int unsignedToSigned(int unsigned, int size) { - if ((unsigned & (1 << size - 1)) != 0) { - return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1))); - } else { - return unsigned; - } - } - - /** - * Convert an integer into the signed bits of the specified length. - */ - private int intToSignedBits(int i, int size) { - if (i < 0) { - return (1 << size - 1) + (i & ((1 << size - 1) - 1)); - } else { - return i; - } - } - public GattCharacteristic getGattCharacteristic() { return GattCharacteristic.getCharacteristic(uuid); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java index ec6c131caad45..c3afcc08bc210 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java @@ -30,8 +30,8 @@ public class BluetoothDescriptor { protected final BluetoothCharacteristic characteristic; protected final UUID uuid; + protected final int handle; - protected byte[] value; /** * The main constructor @@ -81,24 +81,6 @@ public int getHandle() { return handle; } - /** - * Returns the stored value for this descriptor. It doesn't read remote data. - * - * @return the value of the descriptor - */ - public byte[] getValue() { - return value; - } - - /** - * Sets the stored value for this descriptor. It doesn't update remote data. - * - * @param value the value for this descriptor instance - */ - public void setValue(byte[] value) { - this.value = value; - } - public GattDescriptor getDescriptor() { return GattDescriptor.getDescriptor(uuid); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java index f9dfde0862b5d..a1616f9ae344a 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java @@ -14,6 +14,8 @@ import java.util.Collection; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -28,6 +30,7 @@ * @author Chris Jackson - Initial contribution * @author Kai Kreuzer - Refactored class to use Integer instead of int, fixed bugs, diverse improvements * @author Connor Petty - Made most of the methods abstract + * @author Peter Rosenberg - Improve notifications */ @NonNullByDefault public abstract class BluetoothDevice { @@ -68,8 +71,6 @@ public enum ConnectionState { protected enum BluetoothEventType { CONNECTION_STATE, SCAN_RECORD, - CHARACTERISTIC_READ_COMPLETE, - CHARACTERISTIC_WRITE_COMPLETE, CHARACTERISTIC_UPDATED, DESCRIPTOR_UPDATED, SERVICES_DISCOVERED, @@ -226,28 +227,34 @@ public BluetoothAdapter getAdapter() { * Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an * operation when one is already in progress will result in subsequent calls returning false. *

- * This is an asynchronous method. Once the read is complete - * {@link BluetoothDeviceListener.onCharacteristicReadComplete} - * method will be called with the completion state. - *

- * Note that {@link BluetoothDeviceListener.onCharacteristicUpdate} will be called when the read value is received. + * This is an asynchronous method. Once the read is complete the returned future will be updated with the result. * * @param characteristic the {@link BluetoothCharacteristic} to read. - * @return true if the characteristic read is started successfully + * @return a future that returns the read data is successful, otherwise throws an exception */ - public abstract boolean readCharacteristic(BluetoothCharacteristic characteristic); + public abstract CompletableFuture readCharacteristic(BluetoothCharacteristic characteristic); /** * Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an * operation when one is already in progress will result in subsequent calls returning false. *

- * This is an asynchronous method. Once the write is complete - * {@link BluetoothDeviceListener.onCharacteristicWriteComplete} method will be called with the completion state. + * This is an asynchronous method. Once the write is complete the returned future will be updated with the result. * - * @param characteristic the {@link BluetoothCharacteristic} to read. - * @return true if the characteristic write is started successfully + * @param characteristic the {@link BluetoothCharacteristic} to write. + * @param value the data to write + * @return a future that returns null upon a successful write, otherwise throws an exception + */ + public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, + byte[] value); + + /** + * Returns if notification is enabled for the given characteristic. + * + * @param characteristic the {@link BluetoothCharacteristic} to check if notifications are enabled. + * @return true if notification is enabled, false if notification is disabled, characteristic is missing on device + * or notifications are not supported. */ - public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic); + public abstract boolean isNotifying(BluetoothCharacteristic characteristic); /** * Enables notifications for a characteristic. Only a single read or write operation can be requested at once. @@ -259,7 +266,7 @@ public BluetoothAdapter getAdapter() { * @param characteristic the {@link BluetoothCharacteristic} to receive notifications for. * @return true if the characteristic notification is started successfully */ - public abstract boolean enableNotifications(BluetoothCharacteristic characteristic); + public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic); /** * Disables notifications for a characteristic. Only a single read or write operation can be requested at once. @@ -269,7 +276,7 @@ public BluetoothAdapter getAdapter() { * @param characteristic the {@link BluetoothCharacteristic} to disable notifications for. * @return true if the characteristic notification is stopped successfully */ - public abstract boolean disableNotifications(BluetoothCharacteristic characteristic); + public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic); /** * Enables notifications for a descriptor. Only a single read or write operation can be requested at once. @@ -366,6 +373,12 @@ public final boolean hasListeners() { protected abstract Collection getListeners(); + public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException; + + public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException; + + public abstract boolean isServicesDiscovered(); + /** * Notify the listeners of an event * @@ -385,19 +398,11 @@ protected void notifyListeners(BluetoothEventType event, Object... args) { case SERVICES_DISCOVERED: listener.onServicesDiscovered(); break; - case CHARACTERISTIC_READ_COMPLETE: - listener.onCharacteristicReadComplete((BluetoothCharacteristic) args[0], - (BluetoothCompletionStatus) args[1]); - break; - case CHARACTERISTIC_WRITE_COMPLETE: - listener.onCharacteristicWriteComplete((BluetoothCharacteristic) args[0], - (BluetoothCompletionStatus) args[1]); - break; case CHARACTERISTIC_UPDATED: - listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0]); + listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]); break; case DESCRIPTOR_UPDATED: - listener.onDescriptorUpdate((BluetoothDescriptor) args[0]); + listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]); break; case ADAPTER_CHANGED: listener.onAdapterChanged((BluetoothAdapter) args[0]); diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java index 858ac0ef2c77c..4220cd8e1358a 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java @@ -46,37 +46,23 @@ public interface BluetoothDeviceListener { */ void onServicesDiscovered(); - /** - * Called when a read request completes - * - * @param characteristic the {@link BluetoothCharacteristic} that has completed the read request - * @param status the {@link BluetoothCompletionStatus} of the read request - */ - void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status); - - /** - * Called when a write request completes - * - * @param characteristic the {@link BluetoothCharacteristic} that has completed the write request - * @param status the {@link BluetoothCompletionStatus} of the write request - */ - void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status); - /** * Called when a characteristic value is received. Implementations should call this whenever a value * is received from the BLE device even if there is no change to the value. * * @param characteristic the updated {@link BluetoothCharacteristic} + * @param value the update value */ - void onCharacteristicUpdate(BluetoothCharacteristic characteristic); + void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value); /** * Called when a descriptor value is received. Implementations should call this whenever a value * is received from the BLE device even if there is no change to the value. * - * @param characteristic the updated {@link BluetoothCharacteristic} + * @param bluetoothDescriptor the updated {@link BluetoothDescriptor} + * @param value the update value */ - void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor); + void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value); /** * Called when the BluetoothAdapter for this BluetoothDevice changes. diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java new file mode 100644 index 0000000000000..d4977a1c58c96 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2021 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.bluetooth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class encompasses exceptions that occur within the bluetooth api. This can be subclassed for more specific + * exceptions in api implementations. + * + * @author Connor Petty - Initial contribution + * + */ +@NonNullByDefault +public class BluetoothException extends Exception { + + private static final long serialVersionUID = -2557298438595050148L; + + public BluetoothException() { + super(); + } + + public BluetoothException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public BluetoothException(String message, Throwable cause) { + super(message, cause); + } + + public BluetoothException(String message) { + super(message); + } + + public BluetoothException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java new file mode 100644 index 0000000000000..454a56b35b624 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2010-2021 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.bluetooth; + +import java.nio.charset.StandardCharsets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a utility class for parsing or formatting bluetooth characteristic data. + * + * @author Connor Petty - Initial Contribution + * + */ +public class BluetoothUtils { + + public static final Logger logger = LoggerFactory.getLogger(BluetoothUtils.class); + + public static final int FORMAT_UINT8 = 0x11; + public static final int FORMAT_UINT16 = 0x12; + public static final int FORMAT_UINT32 = 0x14; + public static final int FORMAT_SINT8 = 0x21; + public static final int FORMAT_SINT16 = 0x22; + public static final int FORMAT_SINT32 = 0x24; + public static final int FORMAT_SFLOAT = 0x32; + public static final int FORMAT_FLOAT = 0x34; + + /** + * Converts a byte array to an int array + * + * @param value + * @return + */ + public static int[] toIntArray(byte[] value) { + if (value == null) { + return null; + } + int[] ret = new int[value.length]; + for (int i = 0; i < value.length; i++) { + ret[i] = value[i]; + } + return ret; + } + + public static byte[] toByteArray(int[] value) { + if (value == null) { + return null; + } + byte[] ret = new byte[value.length]; + for (int i = 0; i < value.length; i++) { + ret[i] = (byte) (value[i] & 0xFF); + } + return ret; + } + + /** + * Return the stored value of this characteristic. + * + */ + public static Integer getIntegerValue(byte[] value, int formatType, int offset) { + if ((offset + getTypeLen(formatType)) > value.length) { + return null; + } + + switch (formatType) { + case FORMAT_UINT8: + return unsignedByteToInt(value[offset]); + + case FORMAT_UINT16: + return unsignedBytesToInt(value[offset], value[offset + 1]); + + case FORMAT_UINT32: + return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]); + + case FORMAT_SINT8: + return unsignedToSigned(unsignedByteToInt(value[offset]), 8); + + case FORMAT_SINT16: + return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16); + + case FORMAT_SINT32: + return unsignedToSigned( + unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32); + default: + logger.error("Unknown format type {} - no int value can be provided for it.", formatType); + } + + return null; + } + + /** + * Return the stored value of this characteristic. This doesn't read the remote data. + * + */ + public static Float getFloatValue(byte[] value, int formatType, int offset) { + if ((offset + getTypeLen(formatType)) > value.length) { + return null; + } + + switch (formatType) { + case FORMAT_SFLOAT: + return bytesToFloat(value[offset], value[offset + 1]); + case FORMAT_FLOAT: + return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]); + default: + logger.error("Unknown format type {} - no float value can be provided for it.", formatType); + } + + return null; + } + + /** + * Return the stored value of this characteristic. This doesn't read the remote data. + * + */ + public static String getStringValue(byte[] value, int offset) { + if (value == null || offset > value.length) { + return null; + } + byte[] strBytes = new byte[value.length - offset]; + for (int i = 0; i < (value.length - offset); ++i) { + strBytes[i] = value[offset + i]; + } + return new String(strBytes, StandardCharsets.UTF_8); + } + + /** + * Set the local value of this characteristic. + * + * @param value the value to set + * @param formatType the format of the value (as one of the FORMAT_* constants in this class) + * @param offset the offset to use when interpreting the value + * @return true, if it has been set successfully + */ + public static boolean setValue(byte[] dest, int value, int formatType, int offset) { + int len = offset + getTypeLen(formatType); + if (dest == null || len > dest.length) { + return false; + } + int val = value; + switch (formatType) { + case FORMAT_SINT8: + val = intToSignedBits(value, 8); + // Fall-through intended + case FORMAT_UINT8: + dest[offset] = (byte) (val & 0xFF); + break; + + case FORMAT_SINT16: + val = intToSignedBits(value, 16); + // Fall-through intended + case FORMAT_UINT16: + dest[offset] = (byte) (val & 0xFF); + dest[offset + 1] = (byte) ((val >> 8) & 0xFF); + break; + + case FORMAT_SINT32: + val = intToSignedBits(value, 32); + // Fall-through intended + case FORMAT_UINT32: + dest[offset] = (byte) (val & 0xFF); + dest[offset + 1] = (byte) ((val >> 8) & 0xFF); + dest[offset + 2] = (byte) ((val >> 16) & 0xFF); + dest[offset + 2] = (byte) ((val >> 24) & 0xFF); + break; + + default: + return false; + } + return true; + } + + /** + * Set the local value of this characteristic. + * + * @param mantissa the mantissa of the value + * @param exponent the exponent of the value + * @param formatType the format of the value (as one of the FORMAT_* constants in this class) + * @param offset the offset to use when interpreting the value + * @return true, if it has been set successfully + * + */ + public static boolean setValue(byte[] dest, int mantissa, int exponent, int formatType, int offset) { + int len = offset + getTypeLen(formatType); + if (dest == null || len > dest.length) { + return false; + } + + switch (formatType) { + case FORMAT_SFLOAT: + int m = intToSignedBits(mantissa, 12); + int exp = intToSignedBits(exponent, 4); + dest[offset] = (byte) (m & 0xFF); + dest[offset + 1] = (byte) ((m >> 8) & 0x0F); + dest[offset + 1] += (byte) ((exp & 0x0F) << 4); + break; + + case FORMAT_FLOAT: + m = intToSignedBits(mantissa, 24); + exp = intToSignedBits(exponent, 8); + dest[offset] = (byte) (m & 0xFF); + dest[offset + 1] = (byte) ((m >> 8) & 0xFF); + dest[offset + 2] = (byte) ((m >> 16) & 0xFF); + dest[offset + 2] += (byte) (exp & 0xFF); + break; + + default: + return false; + } + + return true; + } + + /** + * Returns the size of the requested value type. + */ + private static int getTypeLen(int formatType) { + return formatType & 0xF; + } + + /** + * Convert a signed byte to an unsigned int. + */ + private static int unsignedByteToInt(int value) { + return value & 0xFF; + } + + /** + * Convert signed bytes to a 16-bit unsigned int. + */ + private static int unsignedBytesToInt(int value1, int value2) { + return value1 + (value2 << 8); + } + + /** + * Convert signed bytes to a 32-bit unsigned int. + */ + private static int unsignedBytesToInt(int value1, int value2, int value3, int value4) { + return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24); + } + + /** + * Convert signed bytes to a 16-bit short float value. + */ + private static float bytesToFloat(int value1, int value2) { + int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12); + int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4); + return (float) (mantissa * Math.pow(10, exponent)); + } + + /** + * Convert signed bytes to a 32-bit short float value. + */ + private static float bytesToFloat(int value1, int value2, int value3, int value4) { + int mantissa = unsignedToSigned( + unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24); + return (float) (mantissa * Math.pow(10, value4)); + } + + /** + * Convert an unsigned integer to a two's-complement signed value. + */ + private static int unsignedToSigned(int unsigned, int size) { + if ((unsigned & (1 << size - 1)) != 0) { + return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1))); + } else { + return unsigned; + } + } + + /** + * Convert an integer into the signed bits of the specified length. + */ + private static int intToSignedBits(int i, int size) { + if (i < 0) { + return (1 << size - 1) + (i & ((1 << size - 1) - 1)); + } else { + return i; + } + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java index 12ea302d56cbd..556b23ee2f11b 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java @@ -12,21 +12,26 @@ */ package org.openhab.binding.bluetooth; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ScheduledFuture; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; -import org.eclipse.jdt.annotation.DefaultLocation; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; -import org.openhab.core.thing.ChannelUID; +import org.openhab.binding.bluetooth.util.RetryFuture; +import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,17 +41,18 @@ * * @author Kai Kreuzer - Initial contribution and API */ -@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS, - DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER }) +@NonNullByDefault public class ConnectedBluetoothHandler extends BeaconBluetoothHandler { private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class); - private ScheduledFuture connectionJob; + private @Nullable Future reconnectJob; + private @Nullable Future pendingDisconnect; - // internal flag for the service resolution status - protected volatile boolean resolved = false; + private boolean alwaysConnected; + private int idleDisconnectDelay = 1000; - protected final Set deviceCharacteristics = new CopyOnWriteArraySet<>(); + // we initially set the to scheduler so that we can keep this field non-null + private ScheduledExecutorService connectionTaskExecutor = scheduler; public ConnectedBluetoothHandler(Thing thing) { super(thing); @@ -54,55 +60,198 @@ public ConnectedBluetoothHandler(Thing thing) { @Override public void initialize() { + + // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize + // the connectionTaskExecutor first + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, + new NamedThreadFactory("bluetooth-connection" + thing.getThingTypeUID(), true)); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRemoveOnCancelPolicy(true); + connectionTaskExecutor = executor; + super.initialize(); - connectionJob = scheduler.scheduleWithFixedDelay(() -> { - try { - if (device.getConnectionState() != ConnectionState.CONNECTED) { - device.connect(); - // we do not set the Thing status here, because we will anyhow receive a call to - // onConnectionStateChange - } else { - // just in case it was already connected to begin with - updateStatus(ThingStatus.ONLINE); - if (!resolved && !device.discoverServices()) { - logger.debug("Error while discovering services"); + if (thing.getStatus() == ThingStatus.OFFLINE) { + // something went wrong in super.initialize() so we shouldn't initialize further here either + return; + } + + Object alwaysConnectRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_ALWAYS_CONNECTED); + alwaysConnected = !Boolean.FALSE.equals(alwaysConnectRaw); + + Object idleDisconnectDelayRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_IDLE_DISCONNECT_DELAY); + idleDisconnectDelay = 1000; + if (idleDisconnectDelayRaw instanceof Number) { + idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue(); + } + + if (alwaysConnected) { + reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> { + try { + if (device.getConnectionState() != ConnectionState.CONNECTED) { + if (!device.connect()) { + logger.debug("Failed to connect to {}", address); + } + // we do not set the Thing status here, because we will anyhow receive a call to + // onConnectionStateChange + } else { + // just in case it was already connected to begin with + updateStatus(ThingStatus.ONLINE); + if (!device.isServicesDiscovered() && !device.discoverServices()) { + logger.debug("Error while discovering services"); + } } + } catch (RuntimeException ex) { + logger.warn("Unexpected error occurred", ex); } - } catch (RuntimeException ex) { - logger.warn("Unexpected error occurred", ex); - } - }, 0, 30, TimeUnit.SECONDS); + }, 0, 30, TimeUnit.SECONDS); + } } @Override public void dispose() { - if (connectionJob != null) { - connectionJob.cancel(true); - connectionJob = null; - } + cancel(reconnectJob, true); + reconnectJob = null; + cancel(pendingDisconnect, true); + pendingDisconnect = null; + super.dispose(); + + // just in case something goes really wrong in the core and it tries to dispose a handler before initializing it + if (scheduler != connectionTaskExecutor) { + connectionTaskExecutor.shutdownNow(); + } } - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - super.handleCommand(channelUID, command); - - // Handle REFRESH - if (command == RefreshType.REFRESH) { - for (BluetoothCharacteristic characteristic : deviceCharacteristics) { - if (characteristic.getGattCharacteristic() != null - && channelUID.getId().equals(characteristic.getGattCharacteristic().name())) { - device.readCharacteristic(characteristic); - break; - } + private static void cancel(@Nullable Future future, boolean interrupt) { + if (future != null) { + future.cancel(interrupt); + } + } + + public void connect() { + connectionTaskExecutor.execute(() -> { + if (!device.connect()) { + logger.debug("Failed to connect to {}", address); + } + }); + } + + public void disconnect() { + connectionTaskExecutor.execute(device::disconnect); + } + + private void scheduleDisconnect() { + cancel(pendingDisconnect, false); + pendingDisconnect = connectionTaskExecutor.schedule(device::disconnect, idleDisconnectDelay, + TimeUnit.MILLISECONDS); + } + + private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException { + if (device.getConnectionState() == ConnectionState.CONNECTED) { + return; + } + if (device.getConnectionState() != ConnectionState.CONNECTING) { + if (!device.connect()) { + throw new ConnectionException("Failed to start connecting"); + } + } + if (!device.awaitConnection(1, TimeUnit.SECONDS)) { + throw new TimeoutException("Connection attempt timeout."); + } + if (!device.isServicesDiscovered()) { + device.discoverServices(); + if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) { + throw new TimeoutException("Service discovery timeout"); } } } - @Override - public void channelLinked(ChannelUID channelUID) { - super.channelLinked(channelUID); + private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID) + throws BluetoothException, TimeoutException, InterruptedException { + connectAndWait(); + BluetoothService service = device.getServices(serviceUUID); + if (service == null) { + throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found"); + } + BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID); + if (characteristic == null) { + throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found"); + } + return characteristic; + } + + private CompletableFuture executeWithConnection(UUID serviceUUID, UUID characteristicUUID, + Function> callable) { + if (connectionTaskExecutor == scheduler) { + return CompletableFuture + .failedFuture(new IllegalStateException("connectionTaskExecutor has not been initialized")); + } + if (connectionTaskExecutor.isShutdown()) { + return CompletableFuture.failedFuture(new IllegalStateException("connectionTaskExecutor is shut down")); + } + // we use a RetryFuture because it supports running Callable instances + return RetryFuture.callWithRetry(() -> { + // we block for completion here so that we keep the lock on the connectionTaskExecutor active. + return callable.apply(connectAndGetCharacteristic(serviceUUID, characteristicUUID)).get(); + }, connectionTaskExecutor)// we make this completion async so that operations chained off the returned future + // will not run on the connectionTaskExecutor + .whenCompleteAsync((r, th) -> { + // we us a while loop here in case the exceptions get nested + while (th instanceof CompletionException || th instanceof ExecutionException) { + th = th.getCause(); + } + if (th instanceof InterruptedException) { + // we don't want to schedule anything if we receive an interrupt + return; + } + if (th instanceof TimeoutException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, th.getMessage()); + } + if (!alwaysConnected) { + scheduleDisconnect(); + } + }, scheduler); + } + + public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) { + return executeWithConnection(serviceUUID, characteristicUUID, device::enableNotifications); + } + + public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data, + boolean enableNotification) { + var future = executeWithConnection(serviceUUID, characteristicUUID, characteristic -> { + if (enableNotification) { + return device.enableNotifications(characteristic) + .thenCompose((v) -> device.writeCharacteristic(characteristic, data)); + } else { + return device.writeCharacteristic(characteristic, data); + } + }); + if (logger.isDebugEnabled()) { + future = future.whenComplete((v, t) -> { + if (t == null) { + logger.debug("Characteristic {} from {} has written value {}", characteristicUUID, address, + HexUtils.bytesToHex(data)); + } + }); + } + return future; + } + + public CompletableFuture readCharacteristic(UUID serviceUUID, UUID characteristicUUID) { + var future = executeWithConnection(serviceUUID, characteristicUUID, device::readCharacteristic); + if (logger.isDebugEnabled()) { + future = future.whenComplete((data, t) -> { + if (t == null) { + if (logger.isDebugEnabled()) { + logger.debug("Characteristic {} from {} has been read - value {}", characteristicUUID, address, + HexUtils.bytesToHex(data)); + } + } + }); + } + return future; } @Override @@ -110,10 +259,12 @@ protected void updateStatusBasedOnRssi(boolean receivedSignal) { // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether // we are connected. if (receivedSignal) { - if (device.getConnectionState() == ConnectionState.CONNECTED) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected."); + if (alwaysConnected) { + if (device.getConnectionState() == ConnectionState.CONNECTED) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected."); + } } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); @@ -126,24 +277,30 @@ public void onConnectionStateChange(BluetoothConnectionStatusNotification connec switch (connectionNotification.getConnectionState()) { case DISCOVERED: // The device is now known on the Bluetooth network, so we can do something... - scheduler.submit(() -> { - if (device.getConnectionState() != ConnectionState.CONNECTED) { - if (!device.connect()) { - logger.debug("Error connecting to device after discovery."); + if (alwaysConnected) { + connectionTaskExecutor.submit(() -> { + if (device.getConnectionState() != ConnectionState.CONNECTED) { + if (!device.connect()) { + logger.debug("Error connecting to device after discovery."); + } } - } - }); + }); + } break; case CONNECTED: - updateStatus(ThingStatus.ONLINE); - scheduler.submit(() -> { - if (!resolved && !device.discoverServices()) { - logger.debug("Error while discovering services"); - } - }); + if (alwaysConnected) { + connectionTaskExecutor.submit(() -> { + if (!device.isServicesDiscovered() && !device.discoverServices()) { + logger.debug("Error while discovering services"); + } + }); + } break; case DISCONNECTED: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + cancel(pendingDisconnect, false); + if (alwaysConnected) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } break; default: break; @@ -151,51 +308,19 @@ public void onConnectionStateChange(BluetoothConnectionStatusNotification connec } @Override - public void onServicesDiscovered() { - super.onServicesDiscovered(); - if (!resolved) { - resolved = true; - logger.debug("Service discovery completed for '{}'", address); - } - } - - @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - super.onCharacteristicReadComplete(characteristic, status); - if (status == BluetoothCompletionStatus.SUCCESS) { - if (logger.isDebugEnabled()) { - logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address, - HexUtils.bytesToHex(characteristic.getByteValue())); - } - } else { - logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address); - } - } - - @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - super.onCharacteristicWriteComplete(characteristic, status); - if (logger.isDebugEnabled()) { - logger.debug("Wrote {} to characteristic {} of device {}: {}", - HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status); - } - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { + super.onCharacteristicUpdate(characteristic, value); if (logger.isDebugEnabled()) { - logger.debug("Recieved update {} to characteristic {} of device {}", - HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address); + logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value), + characteristic.getUuid(), address); } } @Override - public void onDescriptorUpdate(BluetoothDescriptor descriptor) { - super.onDescriptorUpdate(descriptor); + public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) { + super.onDescriptorUpdate(descriptor, value); if (logger.isDebugEnabled()) { - logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()), + logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(value), descriptor.getUuid(), address); } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java new file mode 100644 index 0000000000000..7bf8c17b51d17 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 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.bluetooth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This is thrown when some kind of connection issue occurs as part of a bluetooth api call that expects a connection. + * + * @author Connor Petty - Initial contribution + * + */ +@NonNullByDefault +public class ConnectionException extends BluetoothException { + + private static final long serialVersionUID = 2966261738506666653L; + + public ConnectionException() { + super(); + } + + public ConnectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ConnectionException(String message, Throwable cause) { + super(message, cause); + } + + public ConnectionException(String message) { + super(message); + } + + public ConnectionException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java index bb4c37029bf1b..921e61e1879c5 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java @@ -15,6 +15,8 @@ import java.util.Collection; import java.util.Collections; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -101,27 +103,45 @@ public boolean discoverServices() { } @Override - public boolean readCharacteristic(BluetoothCharacteristic characteristic) { + public CompletableFuture readCharacteristic(BluetoothCharacteristic characteristic) { BluetoothDevice delegate = getDelegate(); - return delegate != null && delegate.readCharacteristic(characteristic); + if (delegate == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null")); + } + return delegate.readCharacteristic(characteristic); + } + + @Override + public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) { + BluetoothDevice delegate = getDelegate(); + if (delegate == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null")); + } + return delegate.writeCharacteristic(characteristic, value); } @Override - public boolean writeCharacteristic(BluetoothCharacteristic characteristic) { + public boolean isNotifying(BluetoothCharacteristic characteristic) { BluetoothDevice delegate = getDelegate(); - return delegate != null && delegate.writeCharacteristic(characteristic); + return delegate != null ? delegate.isNotifying(characteristic) : false; } @Override - public boolean enableNotifications(BluetoothCharacteristic characteristic) { + public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) { BluetoothDevice delegate = getDelegate(); - return delegate != null ? delegate.enableNotifications(characteristic) : false; + if (delegate == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null")); + } + return delegate.enableNotifications(characteristic); } @Override - public boolean disableNotifications(BluetoothCharacteristic characteristic) { + public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) { BluetoothDevice delegate = getDelegate(); - return delegate != null ? delegate.disableNotifications(characteristic) : false; + if (delegate == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null")); + } + return delegate.disableNotifications(characteristic); } @Override @@ -154,6 +174,24 @@ protected Collection getListeners() { return delegate != null ? delegate.getCharacteristic(uuid) : null; } + @Override + public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException { + BluetoothDevice delegate = getDelegate(); + return delegate != null ? delegate.awaitConnection(timeout, unit) : false; + } + + @Override + public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException { + BluetoothDevice delegate = getDelegate(); + return delegate != null ? delegate.awaitServiceDiscovery(timeout, unit) : false; + } + + @Override + public boolean isServicesDiscovered() { + BluetoothDevice delegate = getDelegate(); + return delegate != null ? delegate.isServicesDiscovered() : false; + } + @Override protected void dispose() { BluetoothDevice delegate = getDelegate(); diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java index c6031e770c494..7de03394adf43 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java @@ -20,10 +20,10 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import java.util.function.Supplier; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -34,13 +34,9 @@ import org.openhab.binding.bluetooth.BluetoothCharacteristic; import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic; import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers; -import org.openhab.binding.bluetooth.BluetoothCompletionStatus; -import org.openhab.binding.bluetooth.BluetoothDescriptor; import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; -import org.openhab.binding.bluetooth.BluetoothDeviceListener; +import org.openhab.binding.bluetooth.BluetoothUtils; import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant; -import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; -import org.openhab.binding.bluetooth.notification.BluetoothScanNotification; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.thing.Thing; @@ -56,28 +52,16 @@ * @author Connor Petty - Initial Contribution */ @NonNullByDefault -public class BluetoothDiscoveryProcess implements Supplier, BluetoothDeviceListener { +public class BluetoothDiscoveryProcess implements Supplier { private static final int DISCOVERY_TTL = 300; private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class); - private final Lock serviceDiscoveryLock = new ReentrantLock(); - private final Condition connectionCondition = serviceDiscoveryLock.newCondition(); - private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition(); - private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition(); - private final BluetoothDeviceSnapshot device; private final Collection participants; private final Set adapters; - private volatile boolean servicesDiscovered = false; - - /** - * Contains characteristic which reading is ongoing or null if no ongoing readings. - */ - private volatile @Nullable GattCharacteristic ongoingGattCharacteristic; - public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device, Collection participants, Set adapters) { this.participants = participants; @@ -166,41 +150,31 @@ private DiscoveryResult createDefaultResult() { .withBridge(device.getAdapter().getUID()).withLabel(label).build(); } - // this is really just a special return type for `ensureConnected` - private static class ConnectionException extends Exception { - - } - - private void ensureConnected() throws ConnectionException, InterruptedException { - if (device.getConnectionState() != ConnectionState.CONNECTED) { - if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) { - logger.debug("Connection attempt failed to start for device {}", device.getAddress()); - // something failed, so we abandon connection discovery - throw new ConnectionException(); - } - if (!awaitConnection(10, TimeUnit.SECONDS)) { - logger.debug("Connection to device {} timed out", device.getAddress()); - throw new ConnectionException(); - } - if (!servicesDiscovered) { - device.discoverServices(); - if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) { - logger.debug("Service discovery for device {} timed out", device.getAddress()); - // something failed, so we abandon connection discovery - throw new ConnectionException(); - } - } - readDeviceInformationIfMissing(); - logger.debug("Device information fetched from the device: {}", device); - } - } - private @Nullable DiscoveryResult findConnectionResult(List connectionParticipants) { try { - device.addListener(this); for (BluetoothDiscoveryParticipant participant : connectionParticipants) { - // we call this every time just in case a participant somehow closes the connection - ensureConnected(); + if (device.getConnectionState() != ConnectionState.CONNECTED) { + if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) { + logger.debug("Connection attempt failed to start for device {}", device.getAddress()); + // something failed, so we abandon connection discovery + return null; + } + if (!device.awaitConnection(1, TimeUnit.SECONDS)) { + logger.debug("Connection to device {} timed out", device.getAddress()); + return null; + } + if (!device.isServicesDiscovered()) { + device.discoverServices(); + if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) { + logger.debug("Service discovery for device {} timed out", device.getAddress()); + // something failed, so we abandon connection discovery + return null; + } + } + readDeviceInformationIfMissing(); + logger.debug("Device information fetched from the device: {}", device); + } + try { DiscoveryResult result = participant.createResult(device); if (result != null) { @@ -210,180 +184,49 @@ private void ensureConnected() throws ConnectionException, InterruptedException logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e); } } - } catch (InterruptedException | ConnectionException e) { + } catch (InterruptedException e) { // do nothing - } finally { - device.removeListener(this); } return null; } - @Override - public void onScanRecordReceived(BluetoothScanNotification scanNotification) { - } - - @Override - public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) { - if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) { - serviceDiscoveryLock.lock(); - try { - connectionCondition.signal(); - } finally { - serviceDiscoveryLock.unlock(); - } - } - } - private void readDeviceInformationIfMissing() throws InterruptedException { if (device.getName() == null) { - fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME); + fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName); } if (device.getModel() == null) { - fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING); + fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel); } if (device.getSerialNumber() == null) { - fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING); + fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl); } if (device.getHardwareRevision() == null) { - fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING); + fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision); } if (device.getFirmwareRevision() == null) { - fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING); + fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision); } if (device.getSoftwareRevision() == null) { - fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING); + fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision); } } - private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException { + private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic, Consumer consumer) + throws InterruptedException { UUID uuid = gattCharacteristic.getUUID(); BluetoothCharacteristic characteristic = device.getCharacteristic(uuid); if (characteristic == null) { logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid); return; } - if (!device.readCharacteristic(characteristic)) { - logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress()); - return; - } - ongoingGattCharacteristic = gattCharacteristic; - if (!awaitInfoResponse(1, TimeUnit.SECONDS)) { - logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress()); - ongoingGattCharacteristic = null; - } - } - - private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException { - serviceDiscoveryLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (device.getConnectionState() != ConnectionState.CONNECTED) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = connectionCondition.awaitNanos(nanosTimeout); - } - } finally { - serviceDiscoveryLock.unlock(); - } - return true; - } - - private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException { - serviceDiscoveryLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (ongoingGattCharacteristic != null) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout); - } - } finally { - serviceDiscoveryLock.unlock(); - } - return true; - } - - private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException { - serviceDiscoveryLock.lock(); - try { - long nanosTimeout = unit.toNanos(timeout); - while (!servicesDiscovered) { - if (nanosTimeout <= 0L) { - return false; - } - nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout); - } - } finally { - serviceDiscoveryLock.unlock(); - } - return true; - } - - @Override - public void onServicesDiscovered() { - serviceDiscoveryLock.lock(); - try { - servicesDiscovered = true; - serviceDiscoveryCondition.signal(); - } finally { - serviceDiscoveryLock.unlock(); - } - } - - @Override - public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) { - serviceDiscoveryLock.lock(); try { - if (status == BluetoothCompletionStatus.SUCCESS) { - switch (characteristic.getGattCharacteristic()) { - case DEVICE_NAME: - device.setName(characteristic.getStringValue(0)); - break; - case MODEL_NUMBER_STRING: - device.setModel(characteristic.getStringValue(0)); - break; - case SERIAL_NUMBER_STRING: - device.setSerialNumberl(characteristic.getStringValue(0)); - break; - case HARDWARE_REVISION_STRING: - device.setHardwareRevision(characteristic.getStringValue(0)); - break; - case FIRMWARE_REVISION_STRING: - device.setFirmwareRevision(characteristic.getStringValue(0)); - break; - case SOFTWARE_REVISION_STRING: - device.setSoftwareRevision(characteristic.getStringValue(0)); - break; - default: - break; - } - } - - if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) { - ongoingGattCharacteristic = null; - infoDiscoveryCondition.signal(); - } - } finally { - serviceDiscoveryLock.unlock(); + byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS); + consumer.accept(BluetoothUtils.getStringValue(value, 0)); + } catch (ExecutionException e) { + logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage()); + } catch (TimeoutException e) { + logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(), + e.getMessage()); } } - - @Override - public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, - BluetoothCompletionStatus status) { - } - - @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - } - - @Override - public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) { - } - - @Override - public void onAdapterChanged(BluetoothAdapter adapter) { - } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryService.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryService.java index b1f4010b4b2e8..1f096e0fb7408 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryService.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryService.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.bluetooth.discovery.internal; +import java.time.Duration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -30,6 +31,7 @@ import org.openhab.binding.bluetooth.BluetoothDevice; import org.openhab.binding.bluetooth.BluetoothDiscoveryListener; import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant; +import org.openhab.core.cache.ExpiringCache; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; @@ -131,6 +133,13 @@ public void stopScan() { for (BluetoothAdapter adapter : adapters) { adapter.scanStop(); } + + // The method `removeOlderResults()` removes the Things from listeners like `Inbox`. + // We therefore need to reset `latestSnapshot` so that the Things are notified again next time. + // Results newer than `getTimestampOfLastScan()` will also be notified again but do not lead to duplicates. + discoveryCaches.values().forEach(discoveryCache -> { + discoveryCache.latestSnapshot.putValue(null); + }); removeOlderResults(getTimestampOfLastScan()); } @@ -153,10 +162,6 @@ private static ThingUID createThingUIDWithBridge(DiscoveryResult result, Bluetoo private static DiscoveryResult copyWithNewBridge(DiscoveryResult result, BluetoothAdapter adapter) { String label = result.getLabel(); - String adapterLabel = adapter.getLabel(); - if (adapterLabel != null) { - label = adapterLabel + " - " + label; - } return DiscoveryResultBuilder.create(createThingUIDWithBridge(result, adapter))// .withBridge(adapter.getUID())// @@ -172,7 +177,8 @@ private class DiscoveryCache { private final Map discoveryFutures = new HashMap<>(); private final Map> discoveryResults = new ConcurrentHashMap<>(); - private @Nullable BluetoothDeviceSnapshot latestSnapshot; + private ExpiringCache latestSnapshot = new ExpiringCache<>(Duration.ofMinutes(1), + () -> null); /** * This is meant to be used as part of a Map.compute function @@ -209,7 +215,7 @@ private synchronized void createDiscoveryFuture(BluetoothDevice device) { CompletableFuture future = null; BluetoothDeviceSnapshot snapshot = new BluetoothDeviceSnapshot(device); - BluetoothDeviceSnapshot latestSnapshot = this.latestSnapshot; + BluetoothDeviceSnapshot latestSnapshot = this.latestSnapshot.getValue(); if (latestSnapshot != null) { snapshot.merge(latestSnapshot); @@ -236,7 +242,7 @@ private synchronized void createDiscoveryFuture(BluetoothDevice device) { } } } - this.latestSnapshot = snapshot; + this.latestSnapshot.putValue(snapshot); if (future == null) { // we pass in the snapshot since it acts as a delegate for the device. It will also retain any new @@ -291,6 +297,12 @@ private void publishDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult re discoveryResults.put(adapter, results); } + /** + * Called when a new discovery is published and thus requires the old discovery to be removed first. + * + * @param adapter to get the results to be removed + * @param result unused + */ private void retractDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult result) { Set results = discoveryResults.remove(adapter); if (results != null) { diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/CharacteristicPropertiesTest.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/CharacteristicPropertiesTest.java new file mode 100644 index 0000000000000..3d40537f6d550 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/CharacteristicPropertiesTest.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 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.bluetooth; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link BluetoothCharacteristic}. + * + * @author Peter Rosenberg - Initial contribution + */ +public class CharacteristicPropertiesTest { + private BluetoothCharacteristic characteristic = new BluetoothCharacteristic(UUID.randomUUID(), 0); + + @Test + public void testAllSupportedProperties() { + // given + // when + int properties = 0; + properties |= BluetoothCharacteristic.PROPERTY_BROADCAST; + properties |= BluetoothCharacteristic.PROPERTY_READ; + properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE; + properties |= BluetoothCharacteristic.PROPERTY_WRITE; + properties |= BluetoothCharacteristic.PROPERTY_NOTIFY; + properties |= BluetoothCharacteristic.PROPERTY_INDICATE; + characteristic.setProperties(properties); + + // then + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST), "Broastcast not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE), + "Write not response not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE), + "Signed write set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS), + "Extended props set"); + } + + @Test + public void testNoProperties() { + // given + // when + int properties = 0; + characteristic.setProperties(properties); + + // then + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST), + "Broastcast not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE), + "Write not response not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE), + "Signed write set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS), + "Extended props set"); + } + + @Test + public void testSomeSupportedProperties() { + // given + // when + int properties = 0; + properties |= BluetoothCharacteristic.PROPERTY_READ; + properties |= BluetoothCharacteristic.PROPERTY_NOTIFY; + characteristic.setProperties(properties); + + // then + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST), + "Broastcast not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE), + "Write not response not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set"); + assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE), + "Signed write set"); + assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS), + "Extended props set"); + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java index d5170cd071df5..2cf91dbe0e520 100644 --- a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.bluetooth; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -73,14 +74,16 @@ public boolean disconnect() { } @Override - public boolean readCharacteristic(BluetoothCharacteristic characteristic) { + public CompletableFuture readCharacteristic(BluetoothCharacteristic characteristic) { if (characteristic.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME) { - characteristic.setValue(deviceName); - notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, - BluetoothCompletionStatus.SUCCESS); - return true; + String name = deviceName; + if (name != null) { + return CompletableFuture.completedFuture(name.getBytes(StandardCharsets.UTF_8)); + } else { + return CompletableFuture.completedFuture(new byte[0]); + } } - return false; + return CompletableFuture.failedFuture(new UnsupportedOperationException()); } public void setDeviceName(String deviceName) { @@ -93,20 +96,25 @@ protected void notifyListeners(BluetoothEventType event, Object... args) { } @Override - public boolean writeCharacteristic(BluetoothCharacteristic characteristic) { - return false; + public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); } @Override - public boolean enableNotifications(BluetoothCharacteristic characteristic) { - return false; + public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); } @Override - public boolean disableNotifications(BluetoothCharacteristic characteristic) { + public boolean isNotifying(BluetoothCharacteristic characteristic) { return false; } + @Override + public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); + } + @Override public boolean enableNotifications(BluetoothDescriptor descriptor) { return false; diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/TestUtils.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/TestUtils.java index fc2bbd8fc5818..c87ebec3a6788 100644 --- a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/TestUtils.java +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/TestUtils.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.bluetooth; -import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingUID; diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryServiceTest.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryServiceTest.java index 141146d4911d8..b05b6e4e46eca 100644 --- a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryServiceTest.java @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java index 9ee06fb31582f..380c14bba72fa 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java @@ -56,7 +56,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler { class DeviceService { /** * Constructor. - * + * * @param service Service which belongs to the device. * @param affectedChannels Channels which are affected by the state of this service. */ @@ -99,7 +99,7 @@ public BoschSHCHandler(Thing thing) { /** * Returns the unique id of the Bosch device. - * + * * @return Unique id of the Bosch device. */ public @Nullable String getBoschID() { @@ -150,7 +150,7 @@ public void initialize() { /** * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update * states of services). - * + * * @param channelUID {@link ChannelUID} of the channel to which the command was sent * @param command {@link Command} */ @@ -162,10 +162,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) { try { deviceService.service.refreshState(); - } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) { + } catch (TimeoutException | ExecutionException | BoschSHCException e) { this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format("Error when trying to refresh state from service %s: %s", deviceService.service.getServiceName(), e.getMessage())); + } catch (InterruptedException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Interrupted refresh state from service %s: %s", + deviceService.service.getServiceName(), e.getMessage())); + Thread.currentThread().interrupt(); } } } @@ -174,7 +179,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Processes an update which is received from the bridge. - * + * * @param serviceName Name of service the update came from. * @param stateData Current state of device service. Serialized as JSON. */ @@ -196,7 +201,7 @@ protected void initializeServices() throws BoschSHCException { /** * Returns the bridge handler for this thing handler. - * + * * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration. * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set. */ @@ -214,7 +219,7 @@ protected BridgeHandler getBridgeHandler() throws BoschSHCException { /** * Query the Bosch Smart Home Controller for the state of the service with the specified name. - * + * * @note Use services instead of directly requesting a state. * * @param stateName Name of the service to query @@ -228,16 +233,21 @@ protected BridgeHandler getBridgeHandler() throws BoschSHCException { try { BridgeHandler bridgeHandler = this.getBridgeHandler(); return bridgeHandler.getState(deviceId, stateName, classOfT); - } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) { + } catch (TimeoutException | ExecutionException | BoschSHCException e) { this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage())); return null; + } catch (InterruptedException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage())); + Thread.currentThread().interrupt(); + return null; } } /** * Creates and registers a new service for this device. - * + * * @param Type of service. * @param Type of service state. * @param newService Supplier function to create a new instance of the service. @@ -258,7 +268,7 @@ protected , TState extends BoschSHCServ /** * Registers a service for this device. - * + * * @param Type of service. * @param Type of service state. * @param service Service to register. @@ -287,7 +297,7 @@ protected , TState extends BoschSHCServ /** * Updates the state of a device service. * Sets the status of the device to offline if setting the state fails. - * + * * @param Type of service. * @param Type of service state. * @param service Service to set state for. @@ -297,15 +307,19 @@ protected , TState extends BoschSHCServ TService service, TState state) { try { service.setState(state); - } catch (InterruptedException | TimeoutException | ExecutionException e) { + } catch (TimeoutException | ExecutionException e) { this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format( "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage())); + } catch (InterruptedException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String + .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage())); + Thread.currentThread().interrupt(); } } /** * Registers a service of this device. - * + * * @param service Service which belongs to this device * @param affectedChannels Channels which are affected by the state of this * service diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java index d3d860e4e04a7..f13ae38ee55b9 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java @@ -12,8 +12,7 @@ */ package org.openhab.binding.boschshc.internal.devices.bridge; -import static org.eclipse.jetty.http.HttpMethod.GET; -import static org.eclipse.jetty.http.HttpMethod.PUT; +import static org.eclipse.jetty.http.HttpMethod.*; import java.lang.reflect.Type; import java.util.ArrayList; @@ -30,7 +29,10 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; -import org.openhab.binding.boschshc.internal.devices.bridge.dto.*; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; @@ -233,12 +235,13 @@ private void initialAccess(BoschHttpClient httpClient) { } catch (InterruptedException e) { this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted"); + Thread.currentThread().interrupt(); } } /** * Get a list of connected devices from the Smart-Home Controller - * + * * @throws InterruptedException in case bridge is stopped */ private boolean getDevices() throws InterruptedException { @@ -354,7 +357,7 @@ private void handleLongPollFailure(Throwable e) { /** * Get a list of rooms from the Smart-Home controller - * + * * @throws InterruptedException in case bridge is stopped */ private boolean getRooms() throws InterruptedException { @@ -479,11 +482,11 @@ public Device getDeviceInfo(String deviceId) /** * Sends a state change for a device to the controller - * + * * @param deviceId Id of device to change state for * @param serviceName Name of service of device to change state for * @param state New state data to set for service - * + * * @return Response of request * @throws InterruptedException * @throws ExecutionException diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java index 013064b0546ec..452c04095d2eb 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java @@ -37,7 +37,7 @@ /** * Handles the long polling to the Smart Home Controller. - * + * * @author Christian Oeing - Initial contribution */ @NonNullByDefault @@ -100,7 +100,7 @@ public void stop() { /** * Subscribe to events and store the subscription ID needed for long polling. - * + * * @param httpClient Http client to use for sending subscription request * @return Subscription id */ @@ -117,16 +117,21 @@ private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedExc logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc()); String subscriptionId = response.getResult(); return subscriptionId; - } catch (TimeoutException | ExecutionException | InterruptedException | BoschSHCException e) { + } catch (TimeoutException | ExecutionException | BoschSHCException e) { throw new LongPollingFailedException( String.format("Error on subscribe (Http client: %s): %s", httpClient.toString(), e.getMessage()), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new LongPollingFailedException( + String.format("Interrupted subscribe (Http client: %s): %s", httpClient.toString(), e.getMessage()), + e); } } /** * Create a new subscription for long polling. - * + * * @param httpClient Http client to send requests to */ private void resubscribe(BoschHttpClient httpClient) { @@ -172,7 +177,7 @@ public void onComplete(@Nullable Result result) { /** * This is the handler for responses of long poll requests. - * + * * @param httpClient HTTP client which received the response * @param subscriptionId Id of subscription the response is for * @param result Complete result of the response diff --git a/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/ContentItem.java b/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/ContentItem.java index d119580182331..84c5a40e355f4 100644 --- a/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/ContentItem.java +++ b/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/ContentItem.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang3.StringEscapeUtils; import org.openhab.core.types.StateOption; import com.google.gson.annotations.Expose; diff --git a/bundles/org.openhab.binding.broadlinkthermostat/NOTICE b/bundles/org.openhab.binding.broadlinkthermostat/NOTICE new file mode 100644 index 0000000000000..33ded0d06e53e --- /dev/null +++ b/bundles/org.openhab.binding.broadlinkthermostat/NOTICE @@ -0,0 +1,20 @@ +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 + +== Third-party Content + +broadlink-java-api +* License: MIT License +* Project: https://github.com/mob41/broadlink-java-api +* Source: https://github.com/mob41/broadlink-java-api diff --git a/bundles/org.openhab.binding.broadlinkthermostat/README.md b/bundles/org.openhab.binding.broadlinkthermostat/README.md new file mode 100644 index 0000000000000..6935e4e9c5d07 --- /dev/null +++ b/bundles/org.openhab.binding.broadlinkthermostat/README.md @@ -0,0 +1,67 @@ +# Broadlink Thermostat Binding + +The binding integrates devices based on Broadlinkthermostat controllers. +As the binding uses the [broadlink-java-api](https://github.com/mob41/broadlink-java-api), theoretically all devices supported by the api can be integrated with this binding. + +## Supported Things + +*Note:* So far only the Floureon Thermostat has been tested! The other things are "best guess" implementations. + +| Things | Description | Thing Type | +|-------------------------|---------------------------------------------------------------|----------------------| +| Floureon Thermostat | Broadlinkthermostat based Thermostat sold with the branding Floureon | floureonthermostat | +| Hysen Thermostat | Broadlinkthermostat based Thermostat sold with the branding Hysen | hysenthermostat | + +## Discovery + +Broadlinkthermostat devices are discovered on the network by sending a specific broadcast message. +Authentication is automatically sent after creating the thing. + +## Thing Configuration + +Two parameter are required for creating things: + +- `host`: The hostname or IP address of the device. +- `mac` : The network MAC of the device. + +The autodiscovery process finds both parts automatically. + +## Channels + +### Floureon-/Hysenthermostat + +| Channel Type ID | Item Type | Description | +|-------------------------------|--------------------|----------------------------------------------------------------------| +| power | Switch | Switch display on/off and enable/disables heating | +| mode | String | Current mode of the thermostat (`auto` or `manual`) | +| sensor | String | The sensor (`internal`/`external`) used for triggering the thermostat| +| roomtemperature | Number:Temperature | Room temperature, measured directly at the device | +| roomtemperatureexternalsensor | Number:Temperature | Room temperature, measured by an external sensor | +| active | Switch | Show if thermostat is currently actively heating | +| setpoint | Number:Temperature | Temperature setpoint that open/close valve | +| temperatureoffset | Number:Temperature | Manual temperature adjustment | +| remotelock | Switch | Locks the device to only allow remote actions | +| time | DateTime | The time and day of week of the device | + +## Full Example + +demo.things: + +``` +Thing broadlinkthermostat:floureonthermostat:bathroomthermostat "Bathroom Thermostat" [ host="192.168.0.23", mac="00:10:FA:6E:38:4A"] +``` + +demo.items: + +``` +Number:Temperature Bathroom_Thermostat_Temperature "Room temperature [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:roomtemperature"} +Number:Temperature Bathroom_Thermostat_Temperature_Ext "Room temperature (ext) [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:roomtemperature"} +Number:Temperature Bathroom_Thermostat_Setpoint "Setpoint [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:setpoint"} +Switch Bathroom_Thermostat_Power "Power" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:power"} +Switch Bathroom_Thermostat_Active "Active" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:active"} +String Bathroom_Thermostat_Mode "Mode" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:mode"} +String Bathroom_Thermostat_Sensor "Sensor" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:sensor"} +Switch Bathroom_Thermostat_Lock "Lock" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:remotelock"} +DateTime Bathroom_Thermostat_Time "Time [%1$tm/%1$td %1$tH:%1$tM]"

* Uses the warnings.xml from the resources directory instead of accessing the api endpoint. * + *

* The XML has 2 Entries: + * *

    - *
  1. Amtliche WARNUNG vor WINDBÖEN, MINOR
  2. - *
  3. Amtliche WARNUNG vor STURMBÖEN, MODERATE
  4. + *
  5. Amtliche WARNUNG vor WINDBÖEN, MINOR + *
  6. Amtliche WARNUNG vor STURMBÖEN, MODERATE *
* * @author Martin Koehler - Initial contribution @@ -60,21 +63,21 @@ public void setUp() throws IOException { public void testGetHeadline() { assertThat(warningsData.getHeadline(0), is("Amtliche WARNUNG vor STURMBÖEN")); assertThat(warningsData.getHeadline(1), is("Amtliche WARNUNG vor WINDBÖEN")); - assertThat(warningsData.getHeadline(2), is(UnDefType.NULL)); + assertThat(warningsData.getHeadline(2), is(UnDefType.UNDEF)); } @Test public void testGetSeverity() { assertThat(warningsData.getSeverity(0), is("Moderate")); assertThat(warningsData.getSeverity(1), is("Minor")); - assertThat(warningsData.getSeverity(2), is(UnDefType.NULL)); + assertThat(warningsData.getSeverity(2), is(UnDefType.UNDEF)); } @Test public void testGetEvent() { assertThat(warningsData.getEvent(0), is("STURMBÖEN")); assertThat(warningsData.getEvent(1), is("WINDBÖEN")); - assertThat(warningsData.getEvent(2), is(UnDefType.NULL)); + assertThat(warningsData.getEvent(2), is(UnDefType.UNDEF)); } @Test @@ -83,21 +86,21 @@ public void testGetDescription() { "Es treten Sturmböen mit Geschwindigkeiten zwischen 60 km/h (17m/s, 33kn, Bft 7) und 80 km/h (22m/s, 44kn, Bft 9) anfangs aus südwestlicher, später aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen um 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.")); assertThat(warningsData.getDescription(1), is( "Es treten Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.")); - assertThat(warningsData.getDescription(2), is(UnDefType.NULL)); + assertThat(warningsData.getDescription(2), is(UnDefType.UNDEF)); } @Test public void testGetAltitude() { assertThat(warningsData.getAltitude(0).format("%.0f ft"), is("0 ft")); assertThat(warningsData.getAltitude(1).format("%.0f ft"), is("0 ft")); - assertThat(warningsData.getAltitude(2), is(UnDefType.NULL)); + assertThat(warningsData.getAltitude(2), is(UnDefType.UNDEF)); } @Test public void testGetCeiling() { assertThat(warningsData.getCeiling(0).format("%.0f ft"), is("9843 ft")); assertThat(warningsData.getCeiling(1).format("%.0f ft"), is("9843 ft")); - assertThat(warningsData.getCeiling(2), is(UnDefType.NULL)); + assertThat(warningsData.getCeiling(2), is(UnDefType.UNDEF)); } @Test @@ -107,7 +110,7 @@ public void testGetExpires() { .toString(), is("2018-12-22T18:00Z[UTC]")); assertThat(((DateTimeType) warningsData.getExpires(1)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC")) .toString(), is("2018-12-23T01:00Z[UTC]")); - assertThat(warningsData.getExpires(2), is(UnDefType.NULL)); + assertThat(warningsData.getExpires(2), is(UnDefType.UNDEF)); } @Test @@ -117,7 +120,7 @@ public void testGetOnset() { .toString(), is("2018-12-21T10:00Z[UTC]")); assertThat(((DateTimeType) warningsData.getOnset(1)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC")) .toString(), is("2018-12-22T18:00Z[UTC]")); - assertThat(warningsData.getOnset(2), is(UnDefType.NULL)); + assertThat(warningsData.getOnset(2), is(UnDefType.UNDEF)); } @Test @@ -127,7 +130,7 @@ public void testGetEffective() { .withZoneSameInstant(ZoneId.of("UTC")).toString(), is("2018-12-22T03:02Z[UTC]")); assertThat(((DateTimeType) warningsData.getEffective(1)).getZonedDateTime() .withZoneSameInstant(ZoneId.of("UTC")).toString(), is("2018-12-22T10:15Z[UTC]")); - assertThat(warningsData.getEffective(2), is(UnDefType.NULL)); + assertThat(warningsData.getEffective(2), is(UnDefType.UNDEF)); } @Test diff --git a/bundles/org.openhab.binding.dwdunwetter/src/test/resources/org/openhab/binding/dwdunwetter/internal/data/warnings.xml b/bundles/org.openhab.binding.dwdunwetter/src/test/resources/org/openhab/binding/dwdunwetter/internal/dto/warnings.xml similarity index 100% rename from bundles/org.openhab.binding.dwdunwetter/src/test/resources/org/openhab/binding/dwdunwetter/internal/data/warnings.xml rename to bundles/org.openhab.binding.dwdunwetter/src/test/resources/org/openhab/binding/dwdunwetter/internal/dto/warnings.xml diff --git a/bundles/org.openhab.binding.ecobee/README.md b/bundles/org.openhab.binding.ecobee/README.md index d7a450b1b4f2d..1ba91fa6b9543 100644 --- a/bundles/org.openhab.binding.ecobee/README.md +++ b/bundles/org.openhab.binding.ecobee/README.md @@ -47,6 +47,7 @@ Once logged in, select the **Developer** option from the menu. If you don't see the **Developer** option on the menu, then something went wrong with the previous step. 4. Finally, create a new application. + Give the application a name and fill in the application summary. Select the **Ecobee PIN** Authorization Method, then press **Create**. You now should see the **API key** for the application you just created. diff --git a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeApi.java b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeApi.java index e77c5334e015f..93c28d5e3f982 100644 --- a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeApi.java +++ b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeApi.java @@ -26,7 +26,7 @@ import java.util.Set; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -168,16 +168,16 @@ private boolean isAuthorized() { ecobeeAuth.doAuthorization(); } catch (OAuthException | IOException | RuntimeException e) { if (logger.isDebugEnabled()) { - logger.info("API: Got exception trying to get access token from OAuth service", e); + logger.warn("API: Got exception trying to get access token from OAuth service", e); } else { - logger.info("API: Got {} trying to get access token from OAuth service: {}", + logger.warn("API: Got {} trying to get access token from OAuth service: {}", e.getClass().getSimpleName(), e.getMessage()); } } catch (EcobeeAuthException e) { if (logger.isDebugEnabled()) { - logger.info("API: The Ecobee authorization process threw an exception", e); + logger.warn("API: The Ecobee authorization process threw an exception", e); } else { - logger.info("API: The Ecobee authorization process threw an exception: {}", e.getMessage()); + logger.warn("API: The Ecobee authorization process threw an exception: {}", e.getMessage()); } ecobeeAuth.setState(EcobeeAuthState.NEED_PIN); } catch (OAuthResponseException e) { @@ -189,12 +189,12 @@ private boolean isAuthorized() { private void handleOAuthException(OAuthResponseException e) { if ("invalid_grant".equalsIgnoreCase(e.getError())) { // Usually indicates that the refresh token is no longer valid and will require reauthorization - logger.warn("API: Received 'invalid_grant' error response. Please reauthorize application with Ecobee"); + logger.debug("API: Received 'invalid_grant' error response. Please reauthorize application with Ecobee"); deleteOAuthClientService(); createOAuthClientService(); } else { // Other errors may not require reauthorization and/or may not apply - logger.warn("API: Exception getting access token: error='{}', description='{}'", e.getError(), + logger.debug("API: Exception getting access token: error='{}', description='{}'", e.getError(), e.getErrorDescription()); } } @@ -285,7 +285,7 @@ private String buildQueryUrl(String baseUrl, String requestJson) throws Unsuppor } catch (IOException e) { logIOException(e); } catch (EcobeeAuthException e) { - logger.info("API: Unable to execute GET: {}", e.getMessage()); + logger.debug("API: Unable to execute GET: {}", e.getMessage()); } return response; } @@ -306,7 +306,7 @@ private boolean executePost(String url, String json) { } catch (IOException e) { logIOException(e); } catch (EcobeeAuthException e) { - logger.info("API: Unable to execute POST: {}", e.getMessage()); + logger.debug("API: Unable to execute POST: {}", e.getMessage()); } return false; } @@ -318,22 +318,21 @@ private void logIOException(Exception e) { logger.debug("API: Call to Ecobee API failed with exception: {}: {}", rootCause.getClass().getSimpleName(), rootCause.getMessage()); } else { - // What's left are unexpected errors that should be logged as INFO with a full stack trace - logger.info("API: Call to Ecobee API failed", e); + // What's left are unexpected errors that should be logged as WARN with a full stack trace + logger.warn("API: Call to Ecobee API failed", e); } } private void logJSException(Exception e, String response) { // The API sometimes returns an HTML page complaining of an SSL error - // Otherwise, this probably should be INFO level logger.debug("API: JsonSyntaxException parsing response: {}", response, e); } private boolean isSuccess(@Nullable AbstractResponseDTO response) { if (response == null) { - logger.info("API: Ecobee API returned null response"); + logger.debug("API: Ecobee API returned null response"); } else if (response.status.code.intValue() != 0) { - logger.info("API: Ecobee API returned unsuccessful status: code={}, message={}", response.status.code, + logger.debug("API: Ecobee API returned unsuccessful status: code={}, message={}", response.status.code, response.status.message); if (response.status.code == ECOBEE_DEAUTHORIZED_TOKEN) { // Token has been deauthorized, so restart the authorization process from the beginning @@ -342,11 +341,11 @@ private boolean isSuccess(@Nullable AbstractResponseDTO response) { createOAuthClientService(); } else if (response.status.code == ECOBEE_TOKEN_EXPIRED) { // Check isAuthorized again to see if we can get a valid token - logger.info("API: Unable to complete API call because token is expired"); + logger.debug("API: Unable to complete API call because token is expired"); if (isAuthorized()) { return true; } else { - logger.warn("API: isAuthorized was NOT successful on second try"); + logger.debug("API: isAuthorized was NOT successful on second try"); } } } else { diff --git a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeAuth.java b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeAuth.java index e3aa1853277e2..cafea05ca2890 100644 --- a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeAuth.java +++ b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/api/EcobeeAuth.java @@ -18,8 +18,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -133,8 +132,9 @@ private void authorize() throws EcobeeAuthException { logger.debug("EcobeeAuth: Got null authorize response from Ecobee API"); setState(EcobeeAuthState.NEED_PIN); } else { - if (StringUtils.isNotEmpty(authResponse.error)) { - throw new EcobeeAuthException(authResponse.error + ": " + authResponse.errorDescription); + String error = authResponse.error; + if (error != null && !error.isEmpty()) { + throw new EcobeeAuthException(error + ": " + authResponse.errorDescription); } code = authResponse.code; writeLogMessage(authResponse.pin, authResponse.expiresIn); @@ -172,8 +172,9 @@ private void getTokens() throws EcobeeAuthException { setState(isPinExpired() ? EcobeeAuthState.NEED_PIN : EcobeeAuthState.NEED_TOKEN); return; } - if (StringUtils.isNotEmpty(tokenResponse.error)) { - throw new EcobeeAuthException(tokenResponse.error + ": " + tokenResponse.errorDescription); + String error = tokenResponse.error; + if (error != null && !error.isEmpty()) { + throw new EcobeeAuthException(error + ": " + tokenResponse.errorDescription); } AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); accessTokenResponse.setRefreshToken(tokenResponse.refreshToken); @@ -261,6 +262,7 @@ private boolean isPinExpired() { } } catch (InterruptedException e) { logger.debug("InterruptedException on call to Ecobee authorization API: {}", e.getMessage()); + Thread.currentThread().interrupt(); } return null; } diff --git a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeSensorThingHandler.java b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeSensorThingHandler.java index 7168910c1c7fc..760248fc8f273 100644 --- a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeSensorThingHandler.java +++ b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeSensorThingHandler.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.lang3.text.WordUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.ecobee.internal.config.EcobeeSensorConfiguration; import org.openhab.binding.ecobee.internal.dto.thermostat.RemoteSensorCapabilityDTO; diff --git a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java index 76541af49d686..f6b399bf6f867 100644 --- a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java +++ b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java @@ -25,7 +25,7 @@ import javax.measure.Unit; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.lang3.text.WordUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.ecobee.internal.action.EcobeeActions; diff --git a/bundles/org.openhab.binding.elerotransmitterstick/src/main/java/org/openhab/binding/elerotransmitterstick/internal/stick/SerialConnection.java b/bundles/org.openhab.binding.elerotransmitterstick/src/main/java/org/openhab/binding/elerotransmitterstick/internal/stick/SerialConnection.java index 16573c9828dfa..71cd206469f40 100644 --- a/bundles/org.openhab.binding.elerotransmitterstick/src/main/java/org/openhab/binding/elerotransmitterstick/internal/stick/SerialConnection.java +++ b/bundles/org.openhab.binding.elerotransmitterstick/src/main/java/org/openhab/binding/elerotransmitterstick/internal/stick/SerialConnection.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.TooManyListenersException; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPortEvent; diff --git a/bundles/org.openhab.binding.elerotransmitterstick/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.elerotransmitterstick/src/main/resources/OH-INF/thing/thing-types.xml index 2a1f8c04ab6a9..18a9352ae4ed8 100644 --- a/bundles/org.openhab.binding.elerotransmitterstick/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.elerotransmitterstick/src/main/resources/OH-INF/thing/thing-types.xml @@ -38,10 +38,9 @@ - + The id of this channel. - true diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java index b695105c2be0d..f636b1d9dfe90 100644 --- a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java +++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java @@ -24,7 +24,6 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.util.UrlEncoded; @@ -94,7 +93,7 @@ public Enigma2Client(String host, @Nullable String user, @Nullable String passwo } catch (ParserConfigurationException e) { logger.warn("Failed setting parser features against XXE attacks!", e); } - if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password)) { + if (user != null && !user.isEmpty() && password != null && !password.isEmpty()) { this.host = "http://" + user + ":" + password + "@" + host; } else { this.host = "http://" + host; diff --git a/bundles/org.openhab.binding.enocean/README.md b/bundles/org.openhab.binding.enocean/README.md index e6ff99b0a1686..54c77c8ad710b 100644 --- a/bundles/org.openhab.binding.enocean/README.md +++ b/bundles/org.openhab.binding.enocean/README.md @@ -14,7 +14,7 @@ First of all you have to configure an EnOcean transceiver (gateway). A directly connected USB300 can be auto discovered, an EnOceanPi has to be added manually to openHAB. Both gateways are represented by an _EnOcean gateway_ in openHAB. If you want to place the gateway for better reception apart from your openHAB server, you can forward its serial messages over TCP/IP (_ser2net_). -In this case you have to define the path to the gateway like this rfc2217://x.x.x.x:3001. +In this case you have to define the path to the gateway like this rfc2217://x.x.x.x:3001. When using _ser2net_ make sure to use _telnet_ instead of _raw_ in the _set2net_ config file. If everything is running fine you should see the _base id_ of your gateway in the properties of your bridge. Another way to improve sending and reception reliability is to setup a wired connection. @@ -126,7 +126,21 @@ The corresponding channels are created dynamically, too. If the actuator supports UTE teach-in, the corresponding thing can be created and paired automatically. First you have to **start the discovery scan for a gateway**. Then press the teach-in button of the actuator. -If the EEP of the actuator is known, the binding sends an UTE teach-in response with a new SenderId and creates a new thing with its channels. +If the EEP of the actuator is known, the binding sends an UTE teach-in response with a new SenderId and creates a new thing with its channels. + +This binding supports so called smart acknowlegde (SMACK) devices too. +Before you can pair a SMACK device you have to configure your gateway bridge as a SMACK postmaster. +If this option is enabled you can pair up to 20 SMACK devices with your gateway. + +Communication between your gateway and a SMACK device is handled through mailboxes. +A mailbox is created for each paired SMACK device and deleted after teach out. +You can see the paired SMACK devices and their mailbox index in the gateway properties. +SMACK devices send periodically status updates followed by a response request. +Whenever such a request is received a `requestAnswer` event is triggered for channel `statusRequestEvent`. +Afterwards you have 100ms time to recalculate your items states and update them. +A message with the updated item states is built, put into the corresponding mailbox and automatically sent upon request of the device. +Pairing and unpairing can be done through a discovery scan. +The corresponding thing of an unpaired device gets disabled, you have to delete it manually if you want to. If the actuator does not support UTE teach-ins, you have to create, configure and choose the right EEP of the thing manually. It is important to link the teach-in channel of this thing to a switch item. @@ -158,6 +172,8 @@ If you change the SenderId of your thing, you have to pair again the thing with | | espVersion | ESP Version of gateway | ESP3, ESP2 | | | rs485 | If gateway is directly connected to a RS485 bus the BaseId is set to 0x00 | true, false | | rs485BaseId | Override BaseId 0x00 if your bus contains a telegram duplicator (FTD14 for ex) | 4 byte hex value | +| | enableSmack | Enables SMACK pairing and handling of SMACK messages | true, false | +| | sendTeachOuts | Defines if a repeated teach in request should be answered with a learned in or teach out response | true, false | | pushButton | receivingEEPId | EEP used for receiving msg | F6_01_01, D2_03_0A | | | enoceanId | EnOceanId of device this thing belongs to | hex value as string | | rockerSwitch | receivingEEPId | | F6_02_01, F6_02_02 | @@ -300,6 +316,7 @@ The channels of a thing are determined automatically based on the chosen EEP. | rssi | Number | Received Signal Strength Indication (dBm) of last received message | | repeatCount | Number | Number of repeaters involved in the transmission of the telegram | | lastReceived | DateTime | Date and time the last telegram was received | +| statusRequestEvent | Trigger | Emits event 'requestAnswer' | Items linked to bi-directional actuators (actuator sends status messages back) should always disable the `autoupdate`. This is especially true for Eltako rollershutter, as their position is calculated out of the current position and the moving time. diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java index 4ae4bf2240bca..c6b3814c5edbc 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java @@ -177,7 +177,7 @@ public class EnOceanBindingConstants { public static final String CHANNEL_WAKEUPCYCLE = "wakeUpCycle"; public static final String CHANNEL_SERVICECOMMAND = "serviceCommand"; public static final String CHANNEL_STATUS_REQUEST_EVENT = "statusRequestEvent"; - public static final String CHANNEL_SEND_COMMAND = "sendCommand"; + public static final String VIRTUALCHANNEL_SEND_COMMAND = "sendCommand"; public static final String CHANNEL_VENTILATIONOPERATIONMODE = "ventilationOperationMode"; public static final String CHANNEL_FIREPLACESAFETYMODE = "fireplaceSafetyMode"; @@ -293,7 +293,8 @@ public class EnOceanBindingConstants { Map.entry(CHANNEL_INDOORAIRANALYSIS, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_INDOORAIRANALYSIS), CoreItemFactory.STRING)), - Map.entry(CHANNEL_SETPOINT, + Map.entry( + CHANNEL_SETPOINT, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SETPOINT), CoreItemFactory.NUMBER)), Map.entry(CHANNEL_CONTACT, @@ -444,13 +445,6 @@ public class EnOceanBindingConstants { new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SERVICECOMMAND), CoreItemFactory.NUMBER)), - Map.entry(CHANNEL_STATUS_REQUEST_EVENT, - new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_STATUS_REQUEST_EVENT), null, - "", false, true)), - Map.entry(CHANNEL_SEND_COMMAND, - new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SEND_COMMAND), - CoreItemFactory.SWITCH)), - Map.entry(CHANNEL_VENTILATIONOPERATIONMODE, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_VENTILATIONOPERATIONMODE), CoreItemFactory.STRING)), @@ -527,6 +521,10 @@ public class EnOceanBindingConstants { CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + Dimensionless.class.getSimpleName())), + Map.entry(CHANNEL_STATUS_REQUEST_EVENT, + new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_STATUS_REQUEST_EVENT), null, + "", false, true)), + Map.entry(CHANNEL_REPEATERMODE, new EnOceanChannelDescription( new ChannelTypeUID(BINDING_ID, CHANNEL_REPEATERMODE), CoreItemFactory.STRING))); @@ -536,11 +534,8 @@ public class EnOceanBindingConstants { public static final String REPEATERMODE_LEVEL_2 = "LEVEL2"; // Bridge config properties - public static final String SENDERID = "senderId"; public static final String PATH = "path"; - public static final String HOST = "host"; - public static final String RS485 = "rs485"; - public static final String NEXTSENDERID = "nextSenderId"; + public static final String PARAMETER_NEXT_SENDERID = "nextSenderId"; // Bridge properties public static final String PROPERTY_BASE_ID = "Base ID"; @@ -551,13 +546,12 @@ public class EnOceanBindingConstants { public static final String PROPERTY_DESCRIPTION = "Description"; // Thing properties - public static final String PROPERTY_ENOCEAN_ID = "enoceanId"; + public static final String PROPERTY_SENDINGENOCEAN_ID = "SendingEnoceanId"; // Thing config parameter public static final String PARAMETER_SENDERIDOFFSET = "senderIdOffset"; public static final String PARAMETER_SENDINGEEPID = "sendingEEPId"; public static final String PARAMETER_RECEIVINGEEPID = "receivingEEPId"; - public static final String PARAMETER_EEPID = "eepId"; public static final String PARAMETER_BROADCASTMESSAGES = "broadcastMessages"; public static final String PARAMETER_ENOCEANID = "enoceanId"; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java index 3ac9ea809cced..bbc82fff8d2b9 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java @@ -28,6 +28,7 @@ import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingManager; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; @@ -60,6 +61,9 @@ public class EnOceanHandlerFactory extends BaseThingHandlerFactory { @Reference ItemChannelLinkRegistry itemChannelLinkRegistry; + @Reference + ThingManager thingManager; + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -96,7 +100,7 @@ protected void removeHandler(ThingHandler thingHandler) { } private void registerDeviceDiscoveryService(EnOceanBridgeHandler handler) { - EnOceanDeviceDiscoveryService discoveryService = new EnOceanDeviceDiscoveryService(handler); + EnOceanDeviceDiscoveryService discoveryService = new EnOceanDeviceDiscoveryService(handler, thingManager); discoveryService.activate(); this.discoveryServiceRegs.put(handler.getThing().getUID(), bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java index 461e2e0c95d5d..eb9291abcafca 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java @@ -19,7 +19,7 @@ public class EnOceanActuatorConfig extends EnOceanBaseConfig { public int channel; - public int senderIdOffset = -1; + public Integer senderIdOffset = null; public String manufacturerId; public String teachInType; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java index 49a0cdae330df..100d83a3b8633 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java @@ -17,15 +17,23 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.util.HexUtils; /** * * @author Daniel Weber - Initial contribution */ +@NonNullByDefault public class EnOceanBaseConfig { + /** + * EnOceanId of the physical device + */ public String enoceanId; + /** + * EEP used/send by physical device + */ public List receivingEEPId = new ArrayList<>(); public boolean receivingSIGEEP = false; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java index baad63a36ed47..6e6bc671a4461 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java @@ -46,10 +46,16 @@ public static ESPVersion getESPVersion(String espVersion) { public boolean rs485; public String rs485BaseId; - public int nextSenderId = 0; + public Integer nextSenderId; + + public boolean enableSmack; + public boolean sendTeachOuts; public EnOceanBridgeConfig() { espVersion = "ESP3"; + sendTeachOuts = false; + enableSmack = true; + nextSenderId = null; } public ESPVersion getESPVersion() { diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java index 36471ec37be02..81c701f4579e4 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java @@ -12,12 +12,19 @@ */ package org.openhab.binding.enocean.internal.config; +import org.openhab.core.config.core.Configuration; + /** * * @author Daniel Weber - Initial contribution */ -public class EnOceanChannelTransformationConfig { +public class EnOceanChannelTransformationConfig extends Configuration { public String transformationType; public String transformationFunction; + + public EnOceanChannelTransformationConfig() { + put("transformationType", ""); + put("transformationFunction", ""); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java index 030e601e576bf..0373c474ba586 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java @@ -25,9 +25,14 @@ import org.openhab.binding.enocean.internal.messages.BasePacket; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; -import org.openhab.binding.enocean.internal.transceiver.PacketListener; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; +import org.openhab.binding.enocean.internal.messages.Responses.SMACKTeachInResponse; +import org.openhab.binding.enocean.internal.transceiver.TeachInListener; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingManager; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.util.HexUtils; @@ -39,15 +44,16 @@ * * @author Daniel Weber - Initial contribution */ - -public class EnOceanDeviceDiscoveryService extends AbstractDiscoveryService implements PacketListener { +public class EnOceanDeviceDiscoveryService extends AbstractDiscoveryService implements TeachInListener { private final Logger logger = LoggerFactory.getLogger(EnOceanDeviceDiscoveryService.class); private EnOceanBridgeHandler bridgeHandler; + private ThingManager thingManager; - public EnOceanDeviceDiscoveryService(EnOceanBridgeHandler bridgeHandler) { + public EnOceanDeviceDiscoveryService(EnOceanBridgeHandler bridgeHandler, ThingManager thingManager) { super(null, 60, false); this.bridgeHandler = bridgeHandler; + this.thingManager = thingManager; } /** @@ -102,72 +108,139 @@ public void packetReceived(BasePacket packet) { } String enoceanId = HexUtils.bytesToHex(eep.getSenderId()); - ThingTypeUID thingTypeUID = eep.getThingTypeUID(); - ThingUID thingUID = new ThingUID(thingTypeUID, bridgeHandler.getThing().getUID(), enoceanId); - - int senderIdOffset = 0; - boolean broadcastMessages = true; - - // check for bidirectional communication => do not use broadcast in this case - if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] - & UTEResponse.CommunicationType_MASK) == UTEResponse.CommunicationType_MASK) { - broadcastMessages = false; - } - - // if ute => send response if needed - if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] & UTEResponse.ResponseNeeded_MASK) == 0) { - logger.info("Sending UTE response to {}", enoceanId); - senderIdOffset = sendTeachInResponse(msg, enoceanId); - } - - // if 4BS teach in variation 3 => send response - if ((eep instanceof _4BSMessage) && ((_4BSMessage) eep).isTeachInVariation3Supported()) { - logger.info("Sending 4BS teach in variation 3 response to {}", enoceanId); - senderIdOffset = sendTeachInResponse(msg, enoceanId); - } - DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUID) - .withRepresentationProperty(enoceanId).withBridge(bridgeHandler.getThing().getUID()); + bridgeHandler.getThing().getThings().stream() + .filter(t -> t.getConfiguration().getProperties().getOrDefault(PARAMETER_ENOCEANID, EMPTYENOCEANID) + .toString().equals(enoceanId)) + .findFirst().ifPresentOrElse(t -> { + // If repeated learn is not allowed => send teach out + // otherwise do nothing + if (bridgeHandler.sendTeachOuts()) { + sendTeachOutResponse(msg, enoceanId, t); + thingManager.setEnabled(t.getUID(), false); + } + }, () -> { + Integer senderIdOffset = null; + boolean broadcastMessages = true; + + // check for bidirectional communication => do not use broadcast in this case + if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] + & UTEResponse.CommunicationType_MASK) == UTEResponse.CommunicationType_MASK) { + broadcastMessages = false; + } + + if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] & UTEResponse.ResponseNeeded_MASK) == 0) { + // if ute => send response if needed + logger.debug("Sending UTE response to {}", enoceanId); + senderIdOffset = sendTeachInResponse(msg, enoceanId); + if (senderIdOffset == null) { + return; + } + } else if ((eep instanceof _4BSMessage) && ((_4BSMessage) eep).isTeachInVariation3Supported()) { + // if 4BS teach in variation 3 => send response + logger.debug("Sending 4BS teach in variation 3 response to {}", enoceanId); + senderIdOffset = sendTeachInResponse(msg, enoceanId); + if (senderIdOffset == null) { + return; + } + } + + createDiscoveryResult(eep, broadcastMessages, senderIdOffset); + }); + } - eep.addConfigPropertiesTo(discoveryResultBuilder); - discoveryResultBuilder.withProperty(PARAMETER_BROADCASTMESSAGES, broadcastMessages); - discoveryResultBuilder.withProperty(PARAMETER_ENOCEANID, enoceanId); + @Override + public void eventReceived(EventMessage event) { + if (event.getEventMessageType() == EventMessageType.SA_CONFIRM_LEARN) { + EEP eep = EEPFactory.buildEEPFromTeachInSMACKEvent(event); + if (eep == null) { + return; + } - if (senderIdOffset > 0) { - // advance config with new device id - discoveryResultBuilder.withProperty(PARAMETER_SENDERIDOFFSET, senderIdOffset); + SMACKTeachInResponse response = EEPFactory.buildResponseFromSMACKTeachIn(event, + bridgeHandler.sendTeachOuts()); + if (response != null) { + bridgeHandler.sendMessage(response, null); + + if (response.isTeachIn()) { + // SenderIdOffset will be determined during Thing init + createDiscoveryResult(eep, false, -1); + } else if (response.isTeachOut()) { + // disable already teached in thing + bridgeHandler.getThing().getThings().stream() + .filter(t -> t.getConfiguration().getProperties() + .getOrDefault(PARAMETER_ENOCEANID, EMPTYENOCEANID).toString() + .equals(HexUtils.bytesToHex(eep.getSenderId()))) + .findFirst().ifPresentOrElse(t -> { + thingManager.setEnabled(t.getUID(), false); + logger.info("Disable thing with id {}", t.getUID()); + }, () -> { + logger.info("Thing for EnOceanId {} already deleted", + HexUtils.bytesToHex(eep.getSenderId())); + }); + } + } } - - thingDiscovered(discoveryResultBuilder.build()); - - // As we only support sensors to be teached in, we do not need to send a teach in response => 4bs - // bidirectional teach in proc is not supported yet - // this is true except for UTE teach in => we always have to send a response here } - private int sendTeachInResponse(ERP1Message msg, String enoceanId) { - int offset; + private Integer sendTeachInResponse(ERP1Message msg, String enoceanId) { // get new sender Id - offset = bridgeHandler.getNextSenderId(enoceanId); - if (offset > 0) { + Integer offset = bridgeHandler.getNextSenderId(enoceanId); + if (offset != null) { byte[] newSenderId = bridgeHandler.getBaseId(); newSenderId[3] += offset; // send response - EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, newSenderId); + EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, newSenderId, true); if (response != null) { bridgeHandler.sendMessage(response.getERP1Message(), null); - logger.info("Teach in response for {} with new senderId {} (= offset {}) sent", enoceanId, + logger.debug("Teach in response for {} with new senderId {} (= offset {}) sent", enoceanId, HexUtils.bytesToHex(newSenderId), offset); } else { logger.warn("Teach in response for enoceanId {} not supported!", enoceanId); } + } else { + logger.warn("Could not get new SenderIdOffset"); } return offset; } + private void sendTeachOutResponse(ERP1Message msg, String enoceanId, Thing thing) { + byte[] senderId = bridgeHandler.getBaseId(); + senderId[3] += (byte) thing.getConfiguration().getProperties().getOrDefault(PARAMETER_SENDERIDOFFSET, 0); + + // send response + EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, senderId, false); + if (response != null) { + bridgeHandler.sendMessage(response.getERP1Message(), null); + logger.debug("Teach out response for thing {} with EnOceanId {} sent", thing.getUID().getId(), enoceanId); + } else { + logger.warn("Teach out response for enoceanId {} not supported!", enoceanId); + } + } + + protected void createDiscoveryResult(EEP eep, boolean broadcastMessages, Integer senderIdOffset) { + String enoceanId = HexUtils.bytesToHex(eep.getSenderId()); + ThingTypeUID thingTypeUID = eep.getThingTypeUID(); + ThingUID thingUID = new ThingUID(thingTypeUID, bridgeHandler.getThing().getUID(), enoceanId); + + DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUID) + .withRepresentationProperty(PARAMETER_ENOCEANID).withProperty(PARAMETER_ENOCEANID, enoceanId) + .withProperty(PARAMETER_BROADCASTMESSAGES, broadcastMessages) + .withBridge(bridgeHandler.getThing().getUID()); + + eep.addConfigPropertiesTo(discoveryResultBuilder); + + if (senderIdOffset != null) { + // advance config with new device id + discoveryResultBuilder.withProperty(PARAMETER_SENDERIDOFFSET, senderIdOffset); + } + + thingDiscovered(discoveryResultBuilder.build()); + } + @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { // we just want teach in msg, so return zero here return 0; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java index 30c8522f7c2d2..957296c15f37f 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java @@ -20,7 +20,6 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; /** * @@ -51,9 +50,6 @@ protected int getUnscaledTemperatureValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } double scaledTemp = getScaledMin() - (((getUnscaledMin() - getUnscaledTemperatureValue()) * (getScaledMin() - getScaledMax())) diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java index fa4241caf9548..355d6365b356e 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java @@ -62,9 +62,6 @@ protected int getUnscaledHumidityValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_TEMPERATURE)) { double scaledTemp = getScaledTemperatureMin() diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java index ac78bbf02e832..56f074f799ae9 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java @@ -53,9 +53,6 @@ protected State getSupplyVoltage(int value) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_ILLUMINATION)) { return getIllumination(); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java index 5f6a54c806dd1..cf471e7fd7136 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java @@ -71,9 +71,6 @@ protected int getUnscaledIlluminationValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_TEMPERATURE)) { double scaledTemp = getScaledTemperatureMin() diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java index 99def10649e15..465ce91d2528f 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java @@ -40,9 +40,6 @@ public A5_10(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_FANSPEEDSTAGE: diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java index 1c54b86f78c95..058a34b760163 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java @@ -169,7 +169,7 @@ private byte getSer(Function getCurrentStateFunc) { @Override protected void convertFromCommandImpl(String channelId, String channelTypeId, Command command, Function getCurrentStateFunc, Configuration config) { - if (CHANNEL_SEND_COMMAND.equals(channelId) && (command.equals(OnOffType.ON))) { + if (VIRTUALCHANNEL_SEND_COMMAND.equals(channelId)) { byte db3 = getPos(getCurrentStateFunc); byte db2 = getTsp(getCurrentStateFunc); byte db1 = (byte) (0x00 | getMc(getCurrentStateFunc) | getWuc(getCurrentStateFunc)); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java index ce476f4f85237..ead041877797c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java @@ -55,9 +55,6 @@ protected void convertFromCommandImpl(String channelId, String channelTypeId, Co @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_GENERAL_SWITCHING: @@ -77,4 +74,9 @@ protected State convertToStateImpl(String channelId, String channelTypeId, return UnDefType.UNDEF; } + + @Override + public boolean isValidForTeachIn() { + return false; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java index 41cdcb1a88462..21dba840f65b3 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java @@ -27,11 +27,12 @@ public class UTEResponse extends _VLDMessage { public static final byte ResponseNeeded_MASK = 0x40; public static final byte TeachIn_NotSpecified = 0x20; - public UTEResponse(ERP1Message packet) { + public UTEResponse(ERP1Message packet, boolean teachIn) { int dataLength = packet.getPayload().length - ESP3_SENDERID_LENGTH - ESP3_RORG_LENGTH - ESP3_STATUS_LENGTH; setData(packet.getPayload(ESP3_RORG_LENGTH, dataLength)); - bytes[0] = (byte) 0x91; // bidirectional communication, teach in accepted, teach in response + bytes[0] = (byte) (teachIn ? 0x91 : 0xA1); // bidirectional communication, teach in accepted or teach out, teach + // in response setStatus((byte) 0x80); setSuppressRepeating(true); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java index 96d1200b0c4eb..651a786af8d72 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java @@ -23,11 +23,11 @@ */ public class _4BSTeachInVariation3Response extends _4BSMessage { - public _4BSTeachInVariation3Response(ERP1Message packet) { + public _4BSTeachInVariation3Response(ERP1Message packet, boolean teachIn) { byte[] payload = packet.getPayload(ESP3_RORG_LENGTH, RORG._4BS.getDataLength()); - payload[3] = (byte) 0xF0; // telegram with EEP number and Manufacturer ID, - // EEP supported, Sender ID stored, Response + payload[3] = (byte) (teachIn ? 0xF0 : 0xD0); // telegram with EEP number and Manufacturer ID, + // EEP supported, Sender ID stored or deleted, Response setData(payload); setDestinationId(packet.getSenderId()); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java index 4f6afd303d39e..3dd44acc26e6d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java @@ -51,4 +51,6 @@ public EEP setStatus(byte status) { return this; } + + public abstract boolean isValidForTeachIn(); } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java index c6d54180a628a..018b3ea06335b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java @@ -98,7 +98,8 @@ protected byte getChannel() { @Override public void addConfigPropertiesTo(DiscoveryResultBuilder discoveredThingResultBuilder) { - discoveredThingResultBuilder.withProperty(PARAMETER_EEPID, getEEPType().getId()); + discoveredThingResultBuilder.withProperty(PARAMETER_SENDINGEEPID, getEEPType().getId()) + .withProperty(PARAMETER_RECEIVINGEEPID, getEEPType().getId()); } @Override diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java index 63e9c0fcf3f43..c84a1a753f7f7 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java @@ -15,18 +15,24 @@ import static org.openhab.binding.enocean.internal.messages.ESP3Packet.*; import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; import org.openhab.binding.enocean.internal.eep.Base.UTEResponse; import org.openhab.binding.enocean.internal.eep.Base._4BSMessage; import org.openhab.binding.enocean.internal.eep.Base._4BSTeachInVariation3Response; +import org.openhab.binding.enocean.internal.eep.Base._RPSMessage; import org.openhab.binding.enocean.internal.eep.D5_00.D5_00_01; import org.openhab.binding.enocean.internal.eep.F6_01.F6_01_01; import org.openhab.binding.enocean.internal.eep.F6_02.F6_02_01; +import org.openhab.binding.enocean.internal.eep.F6_05.F6_05_02; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_00; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_00_EltakoFPE; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_01; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; +import org.openhab.binding.enocean.internal.messages.Responses.SMACKTeachInResponse; import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,8 +51,9 @@ public static EEP createEEP(EEPType eepType) { if (cl == null) { throw new IllegalArgumentException("Message " + eepType + " not implemented"); } - return cl.newInstance(); - } catch (IllegalAccessException | InstantiationException e) { + return cl.getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { throw new IllegalArgumentException(e); } } @@ -69,6 +76,21 @@ public static EEP buildEEP(EEPType eepType, ERP1Message packet) { } } + private static EEPType getGenericEEPTypeFor(byte rorg) { + logger.info("Received unsupported EEP teach in, trying to fallback to generic thing"); + RORG r = RORG.getRORG(rorg); + if (r == RORG._4BS) { + logger.info("Fallback to 4BS generic thing"); + return EEPType.Generic4BS; + } else if (r == RORG.VLD) { + logger.info("Fallback to VLD generic thing"); + return EEPType.GenericVLD; + } else { + logger.info("Fallback not possible"); + return null; + } + } + public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { if (!msg.getIsTeachIn() && !(msg.getRORG() == RORG.RPS)) { return null; @@ -77,38 +99,48 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { switch (msg.getRORG()) { case RPS: try { - EEP result = new F6_01_01(msg); - if (result.isValid()) { // check if t21 is set, nu not set, and data == 0x10 or 0x00 + _RPSMessage result = new F6_10_00(msg); + if (result.isValidForTeachIn()) { + return result; + } + } catch (Exception e) { + } + + try { + _RPSMessage result = new F6_10_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } try { - EEP result = new F6_02_01(msg); - if (result.isValid()) { // check if highest bit is not set + _RPSMessage result = new F6_02_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } try { - EEP result = new F6_10_00(msg); - if (result.isValid()) { + _RPSMessage result = new F6_05_02(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } + try { - EEP result = new F6_10_00_EltakoFPE(msg); - if (result.isValid()) { // check if data == 0x10 or 0x00 + _RPSMessage result = new F6_01_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } + try { - EEP result = new F6_10_01(msg); - if (result.isValid()) { + _RPSMessage result = new F6_10_00_EltakoFPE(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { @@ -120,8 +152,8 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { case _4BS: { int db_0 = msg.getPayload()[4]; if ((db_0 & _4BSMessage.LRN_Type_Mask) == 0) { // Variation 1 - logger.info("Received 4BS Teach In variation 1 without EEP"); - return null; + logger.info("Received 4BS Teach In variation 1 without EEP, fallback to generic thing"); + return buildEEP(EEPType.Generic4BS, msg); } byte db_3 = msg.getPayload()[1]; @@ -132,19 +164,21 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { int type = ((db_3 & 0b11) << 5) + ((db_2 & 0xFF) >>> 3); int manufId = ((db_2 & 0b111) << 8) + (db_1 & 0xff); - logger.info("Received 4BS Teach In with EEP A5-{}-{} and manufacturerID {}", + logger.debug("Received 4BS Teach In with EEP A5-{}-{} and manufacturerID {}", HexUtils.bytesToHex(new byte[] { (byte) func }), HexUtils.bytesToHex(new byte[] { (byte) type }), HexUtils.bytesToHex(new byte[] { (byte) manufId })); EEPType eepType = EEPType.getType(RORG._4BS, func, type, manufId); if (eepType == null) { - logger.debug("Received unsupported EEP teach in, fallback to generic thing"); - eepType = EEPType.Generic4BS; + eepType = getGenericEEPTypeFor(RORG._4BS.getValue()); } - return buildEEP(eepType, msg); + if (eepType != null) { + return buildEEP(eepType, msg); + } } + break; case UTE: { byte[] payload = msg.getPayload(); @@ -161,38 +195,58 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { EEPType eepType = EEPType.getType(RORG.getRORG(rorg), func, type, manufId); if (eepType == null) { - logger.info("Received unsupported EEP teach in, fallback to generic thing"); - RORG r = RORG.getRORG(rorg); - if (r == RORG._4BS) { - eepType = EEPType.Generic4BS; - } else if (r == RORG.VLD) { - eepType = EEPType.GenericVLD; - } else { - return null; - } + eepType = getGenericEEPTypeFor(rorg); } - return buildEEP(eepType, msg); + if (eepType != null) { + return buildEEP(eepType, msg); + } } - case Unknown: - case VLD: - case MSC: - case SIG: + break; + default: return null; } return null; } - public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] senderId) { + public static EEP buildEEPFromTeachInSMACKEvent(EventMessage event) { + if (event.getEventMessageType() != EventMessageType.SA_CONFIRM_LEARN) { + return null; + } + + byte[] payload = event.getPayload(); + byte manufIdMSB = payload[2]; + byte manufIdLSB = payload[3]; + int manufId = ((manufIdMSB & 0b111) << 8) + (manufIdLSB & 0xff); + + byte rorg = payload[4]; + int func = payload[5] & 0xFF; + int type = payload[6] & 0xFF; + + byte[] senderId = Arrays.copyOfRange(payload, 12, 12 + 4); + + logger.debug("Received SMACK Teach In with EEP {}-{}-{} and manufacturerID {}", + HexUtils.bytesToHex(new byte[] { (byte) rorg }), HexUtils.bytesToHex(new byte[] { (byte) func }), + HexUtils.bytesToHex(new byte[] { (byte) type }), HexUtils.bytesToHex(new byte[] { (byte) manufId })); + + EEPType eepType = EEPType.getType(RORG.getRORG(rorg), func, type, manufId); + if (eepType == null) { + eepType = getGenericEEPTypeFor(rorg); + } + + return createEEP(eepType).setSenderId(senderId); + } + + public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] senderId, boolean teachIn) { switch (msg.getRORG()) { case UTE: - EEP result = new UTEResponse(msg); + EEP result = new UTEResponse(msg, teachIn); result.setSenderId(senderId); return result; case _4BS: - result = new _4BSTeachInVariation3Response(msg); + result = new _4BSTeachInVariation3Response(msg, teachIn); result.setSenderId(senderId); return result; @@ -200,4 +254,31 @@ public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] sender return null; } } + + public static SMACKTeachInResponse buildResponseFromSMACKTeachIn(EventMessage event, boolean sendTeachOuts) { + SMACKTeachInResponse response = new SMACKTeachInResponse(); + + byte priority = event.getPayload()[1]; + if ((priority & 0b1001) == 0b1001) { + logger.debug("gtw is already postmaster"); + if (sendTeachOuts) { + logger.debug("Repeated learn is not allow hence send teach out"); + response.setTeachOutResponse(); + } else { + logger.debug("Send a repeated learn in"); + response.setRepeatedTeachInResponse(); + } + } else if ((priority & 0b100) == 0) { + logger.debug("no place for further mailbox"); + response.setNoPlaceForFurtherMailbox(); + } else if ((priority & 0b10) == 0) { + logger.debug("rssi is not good enough"); + response.setBadRSSI(); + } else if ((priority & 0b1) == 0b1) { + logger.debug("gtw is candidate for postmaster => teach in"); + response.setTeachIn(); + } + + return response; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java index 451c853303fdd..26c461ae8257c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.enocean.internal.EnOceanChannelDescription; +import org.openhab.binding.enocean.internal.config.EnOceanChannelTransformationConfig; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_01; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_02; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_03; @@ -164,15 +165,9 @@ public enum EEPType { UTEResponse(RORG.UTE, 0, 0, false, UTEResponse.class, null), _4BSTeachInVariation3Response(RORG._4BS, 0, 0, false, _4BSTeachInVariation3Response.class, null), - GenericRPS(RORG.RPS, 0xFF, 0xFF, false, GenericRPS.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD), - Generic4BS(RORG._4BS, 0xFF, 0xFF, false, Generic4BS.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD, CHANNEL_VIBRATION), - GenericVLD(RORG.VLD, 0xFF, 0xFF, false, GenericVLD.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD), + GenericRPS(RORG.RPS, 0xFF, 0xFF, false, GenericRPS.class, THING_TYPE_GENERICTHING), + Generic4BS(RORG._4BS, 0xFF, 0xFF, false, Generic4BS.class, THING_TYPE_GENERICTHING, CHANNEL_VIBRATION), + GenericVLD(RORG.VLD, 0xFF, 0xFF, false, GenericVLD.class, THING_TYPE_GENERICTHING), PTM200(RORG.RPS, 0x00, 0x00, false, PTM200Message.class, null, CHANNEL_GENERAL_SWITCHING, CHANNEL_ROLLERSHUTTER, CHANNEL_CONTACT), @@ -391,8 +386,8 @@ public enum EEPType { // UniversalCommand(RORG._4BS, 0x3f, 0x7f, false, A5_3F_7F_Universal.class, THING_TYPE_UNIVERSALACTUATOR, // CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_LIGHT_SWITCHING, CHANNEL_GENERIC_DIMMER, CHANNEL_TEACHINCMD), - EltakoFSB(RORG._4BS, 0x3f, 0x7f, false, "EltakoFSB", 0, A5_3F_7F_EltakoFSB.class, THING_TYPE_ROLLERSHUTTER, 0, - new Hashtable() { + EltakoFSB(RORG._4BS, 0x3f, 0x7f, false, false, "EltakoFSB", 0, A5_3F_7F_EltakoFSB.class, THING_TYPE_ROLLERSHUTTER, + 0, new Hashtable() { private static final long serialVersionUID = 1L; { put(CHANNEL_ROLLERSHUTTER, new Configuration()); @@ -404,10 +399,10 @@ public enum EEPType { } }), - Thermostat(RORG._4BS, 0x20, 0x04, false, A5_20_04.class, THING_TYPE_THERMOSTAT, CHANNEL_VALVE_POSITION, + Thermostat(RORG._4BS, 0x20, 0x04, false, true, A5_20_04.class, THING_TYPE_THERMOSTAT, CHANNEL_VALVE_POSITION, CHANNEL_BUTTON_LOCK, CHANNEL_DISPLAY_ORIENTATION, CHANNEL_TEMPERATURE_SETPOINT, CHANNEL_TEMPERATURE, CHANNEL_FEED_TEMPERATURE, CHANNEL_MEASUREMENT_CONTROL, CHANNEL_FAILURE_CODE, CHANNEL_WAKEUPCYCLE, - CHANNEL_SERVICECOMMAND, CHANNEL_STATUS_REQUEST_EVENT, CHANNEL_SEND_COMMAND), + CHANNEL_SERVICECOMMAND), SwitchWithEnergyMeasurment_00(RORG.VLD, 0x01, 0x00, true, D2_01_00.class, THING_TYPE_MEASUREMENTSWITCH, CHANNEL_GENERAL_SWITCHING, CHANNEL_TOTALUSAGE), @@ -512,23 +507,36 @@ public enum EEPType { private boolean supportsRefresh; + private boolean requestsResponse; + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, Class eepClass, ThingTypeUID thingTypeUID, String... channelIds) { this(rorg, func, type, supportsRefresh, eepClass, thingTypeUID, -1, channelIds); } + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, + Class eepClass, ThingTypeUID thingTypeUID, String... channelIds) { + this(rorg, func, type, supportsRefresh, requestsResponse, eepClass, thingTypeUID, -1, channelIds); + } + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, Class eepClass, ThingTypeUID thingTypeUID, String... channelIds) { - this(rorg, func, type, supportsRefresh, manufactorSuffix, manufId, eepClass, thingTypeUID, 0, channelIds); + this(rorg, func, type, supportsRefresh, false, manufactorSuffix, manufId, eepClass, thingTypeUID, 0, + channelIds); } EEPType(RORG rorg, int func, int type, boolean supportsRefresh, Class eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { - this(rorg, func, type, supportsRefresh, "", 0, eepClass, thingTypeUID, command, channelIds); + this(rorg, func, type, supportsRefresh, false, "", 0, eepClass, thingTypeUID, command, channelIds); } - EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, Class eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { + this(rorg, func, type, supportsRefresh, requestsResponse, "", 0, eepClass, thingTypeUID, command, channelIds); + } + + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, String manufactorSuffix, + int manufId, Class eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { this.rorg = rorg; this.func = func; this.type = type; @@ -538,24 +546,18 @@ public enum EEPType { this.manufactorSuffix = manufactorSuffix; this.manufactorId = manufId; this.supportsRefresh = supportsRefresh; + this.requestsResponse = requestsResponse; for (String id : channelIds) { this.channelIdsWithConfig.put(id, new Configuration()); this.supportedChannels.put(id, CHANNELID2CHANNELDESCRIPTION.get(id)); } - this.channelIdsWithConfig.put(CHANNEL_RSSI, new Configuration()); - this.supportedChannels.put(CHANNEL_RSSI, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_RSSI)); - - this.channelIdsWithConfig.put(CHANNEL_REPEATCOUNT, new Configuration()); - this.supportedChannels.put(CHANNEL_REPEATCOUNT, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_REPEATCOUNT)); - - this.channelIdsWithConfig.put(CHANNEL_LASTRECEIVED, new Configuration()); - this.supportedChannels.put(CHANNEL_LASTRECEIVED, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_LASTRECEIVED)); + addDefaultChannels(); } - EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, - Class eepClass, ThingTypeUID thingTypeUID, int command, + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, String manufactorSuffix, + int manufId, Class eepClass, ThingTypeUID thingTypeUID, int command, Hashtable channelConfigs) { this.rorg = rorg; this.func = func; @@ -567,11 +569,46 @@ public enum EEPType { this.manufactorSuffix = manufactorSuffix; this.manufactorId = manufId; this.supportsRefresh = supportsRefresh; + this.requestsResponse = requestsResponse; for (String id : channelConfigs.keySet()) { this.supportedChannels.put(id, CHANNELID2CHANNELDESCRIPTION.get(id)); } + addDefaultChannels(); + } + + private void addDefaultChannels() { + + if (THING_TYPE_GENERICTHING.equals(this.thingTypeUID)) { + this.channelIdsWithConfig.put(CHANNEL_GENERIC_SWITCH, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_SWITCH, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_SWITCH)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_ROLLERSHUTTER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_ROLLERSHUTTER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_ROLLERSHUTTER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_DIMMER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_DIMMER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_DIMMER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_NUMBER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_NUMBER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_NUMBER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_STRING, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_STRING, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_STRING)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_COLOR, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_COLOR, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_COLOR)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_TEACHINCMD, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_TEACHINCMD, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_TEACHINCMD)); + } + this.channelIdsWithConfig.put(CHANNEL_RSSI, new Configuration()); this.supportedChannels.put(CHANNEL_RSSI, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_RSSI)); @@ -580,6 +617,12 @@ public enum EEPType { this.channelIdsWithConfig.put(CHANNEL_LASTRECEIVED, new Configuration()); this.supportedChannels.put(CHANNEL_LASTRECEIVED, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_LASTRECEIVED)); + + if (requestsResponse) { + this.channelIdsWithConfig.put(CHANNEL_STATUS_REQUEST_EVENT, new Configuration()); + this.supportedChannels.put(CHANNEL_STATUS_REQUEST_EVENT, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_STATUS_REQUEST_EVENT)); + } } public Class getEEPClass() { @@ -602,6 +645,10 @@ public boolean getSupportsRefresh() { return supportsRefresh; } + public boolean getRequstesResponse() { + return requestsResponse; + } + public Map GetSupportedChannels() { return Collections.unmodifiableMap(supportedChannels); } @@ -614,7 +661,7 @@ public boolean isChannelSupported(Channel channel) { } public boolean isChannelSupported(String channelId, String channelTypeId) { - return supportedChannels.containsKey(channelId) + return supportedChannels.containsKey(channelId) || VIRTUALCHANNEL_SEND_COMMAND.equals(channelId) || supportedChannels.values().stream().anyMatch(c -> c.channelTypeUID.getId().equals(channelTypeId)); } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java index ce19528af36f0..b69aa8d4d6386 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java @@ -34,9 +34,6 @@ public F6_01_01(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } return getBit(bytes[0], 4) ? CommonTriggerEvents.PRESSED : CommonTriggerEvents.RELEASED; } @@ -45,4 +42,10 @@ protected String convertToEventImpl(String channelId, String channelTypeId, Stri protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && !getBit(bytes[0], 7); } + + @Override + public boolean isValidForTeachIn() { + // just treat press as teach in, ignore release + return t21 && !nu && bytes[0] == 0x10; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java index 2af97fdc06f67..8152c5e90a30a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java @@ -55,9 +55,6 @@ public F6_02_01(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } if (t21 && nu) { byte dir1 = channelId.equals(CHANNEL_ROCKERSWITCH_CHANNELA) ? A0 : B0; @@ -112,11 +109,6 @@ protected State convertToStateImpl(String channelId, String channelTypeId, // this method is used by the classic device listener channels to convert an rocker switch message into an // appropriate item update State currentState = getCurrentStateFunc.apply(channelId); - - if (!isValid()) { - return UnDefType.UNDEF; - } - if (t21 && nu) { EnOceanChannelVirtualRockerSwitchConfig c = config.as(EnOceanChannelVirtualRockerSwitchConfig.class); byte dir1 = c.getChannel() == Channel.ChannelA ? A0 : B0; @@ -179,4 +171,22 @@ private State inverse(UpDownType currentState) { protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && !getBit(bytes[0], 7); } + + @Override + public boolean isValidForTeachIn() { + if (t21) { + // just treat press as teach in => DB0.4 has to be set + if (!getBit(bytes[0], 4)) { + return false; + } + // DB0.7 is never set for rocker switch message + if (getBit(bytes[0], 7)) { + return false; + } + } else { + return false; + } + + return true; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java index 29b643cb01a1a..76e169785171a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java @@ -52,9 +52,6 @@ public F6_02_02(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } if (t21 && nu) { byte dir1 = channelId.equals(CHANNEL_ROCKERSWITCH_CHANNELA) ? AI : BI; @@ -109,11 +106,6 @@ protected State convertToStateImpl(String channelId, String channelTypeId, // this method is used by the classic device listener channels to convert an rocker switch message into an // appropriate item update State currentState = getCurrentStateFunc.apply(channelId); - - if (!isValid()) { - return UnDefType.UNDEF; - } - if (t21 && nu) { EnOceanChannelVirtualRockerSwitchConfig c = config.as(EnOceanChannelVirtualRockerSwitchConfig.class); byte dir1 = c.getChannel() == Channel.ChannelA ? AI : BI; @@ -171,4 +163,9 @@ private State inverse(OnOffType currentState) { private State inverse(UpDownType currentState) { return currentState == UpDownType.UP ? UpDownType.DOWN : UpDownType.UP; } + + @Override + public boolean isValidForTeachIn() { + return false; // Never treat a message as F6-02-02, let user decide which orientation of rocker switch is used + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java index 50855ab2e92cc..4fafc5b68ac6e 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java @@ -44,9 +44,6 @@ public F6_05_02(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_SMOKEDETECTION: @@ -62,4 +59,10 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && (bytes[0] == ALARM_OFF || bytes[0] == ALARM_ON || bytes[0] == ENERGY_LOW); } + + @Override + public boolean isValidForTeachIn() { + // just treat the first message with ALARM_ON as teach in + return !t21 && !nu && bytes[0] == ALARM_ON; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java index 43fc0f75344d4..498a807458fdb 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java @@ -47,9 +47,6 @@ public F6_10_00(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } byte data = (byte) (bytes[0] & 0xF0); @@ -82,4 +79,9 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && getBit(bytes[0], 7) && getBit(bytes[0], 6); } + + @Override + public boolean isValidForTeachIn() { + return t21 && !nu && getBit(bytes[0], 7) && getBit(bytes[0], 6); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java index 8a381ea9e1600..ee9cd485f9c9b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java @@ -61,4 +61,10 @@ protected boolean validateData(byte[] bytes) { // FPE just sends 0b00010000 or 0b00000000 value, so we apply mask 0b11101111 return super.validateData(bytes) && ((bytes[0] & (byte) 0xEF) == (byte) 0x00); } + + @Override + public boolean isValidForTeachIn() { + // just treat CLOSED as teach in + return bytes[0] == CLOSED; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java index 51ed9306212f0..3773cafe49c8d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java @@ -47,9 +47,6 @@ public F6_10_01(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } byte data = (byte) (bytes[0] & 0x0F); @@ -82,4 +79,10 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && getBit(bytes[0], 6) && getBit(bytes[0], 3) && getBit(bytes[0], 2); } + + @Override + public boolean isValidForTeachIn() { + return !getBit(bytes[0], 7) && getBit(bytes[0], 6) && !getBit(bytes[0], 5) && !getBit(bytes[0], 4) + && getBit(bytes[0], 3) && getBit(bytes[0], 2); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java index 178ae890b8689..d930e8206400b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.enocean.internal.eep.Generic; -import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.PARAMETER_EEPID; +import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import static org.openhab.binding.enocean.internal.messages.ESP3Packet.*; import java.lang.reflect.InvocationTargetException; @@ -161,6 +161,7 @@ protected boolean validateData(byte[] bytes) { @Override public void addConfigPropertiesTo(DiscoveryResultBuilder discoveredThingResultBuilder) { - discoveredThingResultBuilder.withProperty(PARAMETER_EEPID, getEEPType().getId()); + discoveredThingResultBuilder.withProperty(PARAMETER_SENDINGEEPID, getEEPType().getId()) + .withProperty(PARAMETER_RECEIVINGEEPID, getEEPType().getId()); } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java index 590769153791d..be2398ac0cf6c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java @@ -28,6 +28,7 @@ import org.openhab.binding.enocean.internal.eep.EEPType; import org.openhab.binding.enocean.internal.messages.BasePacket; import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -68,8 +69,8 @@ public EnOceanBaseActuatorHandler(Thing thing, ItemChannelLinkRegistry itemChann * @param senderIdOffset to be validated * @return true if senderIdOffset is between ]0;128[ and is not used yet */ - private boolean validateSenderIdOffset(int senderIdOffset) { - if (senderIdOffset == -1) { + private boolean validateSenderIdOffset(Integer senderIdOffset) { + if (senderIdOffset == null) { return true; } @@ -157,26 +158,24 @@ boolean validateConfig() { } private boolean initializeIdForSending() { - // Generic things are treated as actuator things, however to support also generic sensors one can define a - // senderIdOffset of -1 - // TODO: seperate generic actuators from generic sensors? - String thingTypeId = this.getThing().getThingTypeUID().getId(); - String genericThingTypeId = THING_TYPE_GENERICTHING.getId(); - - if (getConfiguration().senderIdOffset == -1 && thingTypeId.equals(genericThingTypeId)) { - return true; - } - EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); if (bridgeHandler == null) { return false; } - // if senderIdOffset is not set (=> defaults to -1) or set to -1, the next free senderIdOffset is determined - if (getConfiguration().senderIdOffset == -1) { + // Generic things are treated as actuator things, however to support also generic sensors one can omit + // senderIdOffset + // TODO: seperate generic actuators from generic sensors? + if ((getConfiguration().senderIdOffset == null + && THING_TYPE_GENERICTHING.equals(this.getThing().getThingTypeUID()))) { + return true; + } + + // if senderIdOffset is not set, the next free senderIdOffset is determined + if (getConfiguration().senderIdOffset == null) { Configuration updateConfig = editConfiguration(); getConfiguration().senderIdOffset = bridgeHandler.getNextSenderId(thing); - if (getConfiguration().senderIdOffset == -1) { + if (getConfiguration().senderIdOffset == null) { configurationErrorDescription = "Could not get a free sender Id from Bridge"; return false; } @@ -185,12 +184,10 @@ private boolean initializeIdForSending() { } byte[] baseId = bridgeHandler.getBaseId(); - baseId[3] = (byte) ((baseId[3] & 0xFF) + getConfiguration().senderIdOffset); + baseId[3] = (byte) ((baseId[3] + getConfiguration().senderIdOffset) & 0xFF); this.senderId = baseId; - - this.updateProperty(PROPERTY_ENOCEAN_ID, HexUtils.bytesToHex(this.senderId)); + this.updateProperty(PROPERTY_SENDINGENOCEAN_ID, HexUtils.bytesToHex(this.senderId)); bridgeHandler.addSender(getConfiguration().senderIdOffset, thing); - return true; } @@ -203,6 +200,22 @@ private void refreshStates() { } } + @Override + protected void sendRequestResponse() { + sendMessage(VIRTUALCHANNEL_SEND_COMMAND, VIRTUALCHANNEL_SEND_COMMAND, OnOffType.ON, null); + } + + protected void sendMessage(String channelId, String channelTypeId, Command command, Configuration channelConfig) { + EEP eep = EEPFactory.createEEP(sendingEEPType); + if (eep.convertFromCommand(channelId, channelTypeId, command, id -> getCurrentState(id), channelConfig) + .hasData()) { + BasePacket msg = eep.setSenderId(senderId).setDestinationId(destinationId) + .setSuppressRepeating(getConfiguration().suppressRepeating).getERP1Message(); + + getBridgeHandler().sendMessage(msg, null); + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { // We must have a valid sendingEEPType and sender id to send commands @@ -237,16 +250,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { Configuration channelConfig = channel.getConfiguration(); - - EEP eep = EEPFactory.createEEP(sendingEEPType); - if (eep.convertFromCommand(channelId, channelTypeId, command, id -> getCurrentState(id), channelConfig) - .hasData()) { - BasePacket msg = eep.setSenderId(senderId).setDestinationId(destinationId) - .setSuppressRepeating(getConfiguration().suppressRepeating).getERP1Message(); - - getBridgeHandler().sendMessage(msg, null); - } - + sendMessage(channelId, channelTypeId, command, channelConfig); } catch (IllegalArgumentException e) { logger.warn("Exception while sending telegram!", e); } @@ -254,11 +258,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void handleRemoval() { - if (getConfiguration().senderIdOffset > 0) { - EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); - if (bridgeHandler != null) { + + EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null) { + if (getConfiguration().senderIdOffset != null && getConfiguration().senderIdOffset > 0) { bridgeHandler.removeSender(getConfiguration().senderIdOffset); } + + if (bridgeHandler.isSmackClient(this.thing)) { + logger.warn("Removing smack client (ThingId: {}) without teach out!", this.thing.getUID().getId()); + } } super.handleRemoval(); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java index d533c1e558c4a..736b37be23671 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java @@ -19,6 +19,8 @@ import java.util.Comparator; import java.util.Hashtable; import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import org.openhab.binding.enocean.internal.config.EnOceanBaseConfig; @@ -57,6 +59,8 @@ public class EnOceanBaseSensorHandler extends EnOceanBaseThingHandler implements protected final Hashtable receivingEEPTypes = new Hashtable<>(); + protected ScheduledFuture responseFuture = null; + public EnOceanBaseSensorHandler(Thing thing, ItemChannelLinkRegistry itemChannelLinkRegistry) { super(thing, itemChannelLinkRegistry); } @@ -104,7 +108,7 @@ boolean validateConfig() { } @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { return Long.parseLong(config.enoceanId, 16); } @@ -129,6 +133,10 @@ protected Predicate channelFilter(EEPType eepType, byte[] senderId) { }; } + protected void sendRequestResponse() { + throw new UnsupportedOperationException("Sensor cannot send responses"); + } + @Override public void packetReceived(BasePacket packet) { ERP1Message msg = (ERP1Message) packet; @@ -175,6 +183,15 @@ public void packetReceived(BasePacket packet) { break; } }); + + if (receivingEEPType.getRequstesResponse()) { + // fire trigger for receive + triggerChannel(prepareAnswer, "requestAnswer"); + // Send response after 100ms + if (responseFuture == null || responseFuture.isDone()) { + responseFuture = scheduler.schedule(this::sendRequestResponse, 100, TimeUnit.MILLISECONDS); + } + } } } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java index 20ccfa80b5062..f561db01859cf 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.enocean.internal.handler; +import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; + import java.util.AbstractMap.SimpleEntry; import java.util.Collection; import java.util.Hashtable; @@ -63,18 +65,25 @@ public abstract class EnOceanBaseThingHandler extends ConfigStatusThingHandler { private ItemChannelLinkRegistry itemChannelLinkRegistry; + protected @NonNull ChannelUID prepareAnswer; + public EnOceanBaseThingHandler(Thing thing, ItemChannelLinkRegistry itemChannelLinkRegistry) { super(thing); this.itemChannelLinkRegistry = itemChannelLinkRegistry; + prepareAnswer = new ChannelUID(thing.getUID(), CHANNEL_STATUS_REQUEST_EVENT); } - @SuppressWarnings("null") @Override public void initialize() { logger.debug("Initializing enocean base thing handler."); this.gateway = null; // reset gateway in case we change the bridge this.config = null; - initializeThing((getBridge() == null) ? null : getBridge().getStatus()); + Bridge bridge = getBridge(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "A bridge is required"); + } else { + initializeThing(bridge.getStatus()); + } } private void initializeThing(ThingStatus bridgeStatus) { @@ -143,6 +152,10 @@ protected void updateChannels() { String channelId = entry.getKey(); EnOceanChannelDescription cd = entry.getValue().GetSupportedChannels().get(channelId); + if (cd == null) { + return; + } + // if we do not need to auto create channel => skip if (!cd.autoCreate) { return; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java index c888f6a60be2d..e793366f26698 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java @@ -15,30 +15,35 @@ import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import java.io.IOException; -import java.math.BigDecimal; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.openhab.binding.enocean.internal.EnOceanConfigStatusMessage; import org.openhab.binding.enocean.internal.config.EnOceanBaseConfig; import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig; +import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig.ESPVersion; import org.openhab.binding.enocean.internal.messages.BasePacket; -import org.openhab.binding.enocean.internal.messages.BaseResponse; import org.openhab.binding.enocean.internal.messages.ESP3PacketFactory; -import org.openhab.binding.enocean.internal.messages.RDBaseIdResponse; -import org.openhab.binding.enocean.internal.messages.RDRepeaterResponse; -import org.openhab.binding.enocean.internal.messages.RDVersionResponse; import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.binding.enocean.internal.messages.Response.ResponseType; +import org.openhab.binding.enocean.internal.messages.Responses.BaseResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDBaseIdResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDLearnedClientsResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDLearnedClientsResponse.LearnedClient; +import org.openhab.binding.enocean.internal.messages.Responses.RDRepeaterResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDVersionResponse; import org.openhab.binding.enocean.internal.transceiver.EnOceanESP2Transceiver; import org.openhab.binding.enocean.internal.transceiver.EnOceanESP3Transceiver; import org.openhab.binding.enocean.internal.transceiver.EnOceanTransceiver; import org.openhab.binding.enocean.internal.transceiver.PacketListener; import org.openhab.binding.enocean.internal.transceiver.ResponseListener; import org.openhab.binding.enocean.internal.transceiver.ResponseListenerIgnoringTimeouts; +import org.openhab.binding.enocean.internal.transceiver.TeachInListener; import org.openhab.binding.enocean.internal.transceiver.TransceiverErrorListener; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.status.ConfigStatusMessage; @@ -76,9 +81,12 @@ public class EnOceanBridgeHandler extends ConfigStatusBridgeHandler implements T private byte[] baseId = null; private Thing[] sendingThings = new Thing[128]; - private int nextSenderId = 0; private SerialPortManager serialPortManager; + private boolean smackAvailable = false; + private boolean sendTeachOuts = true; + private Set smackClients = Set.of(); + public EnOceanBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) { super(bridge); this.serialPortManager = serialPortManager; @@ -157,13 +165,6 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SerialPortManager could not be found"); } else { - Object devId = getConfig().get(NEXTSENDERID); - if (devId != null) { - nextSenderId = ((BigDecimal) devId).intValue(); - } else { - nextSenderId = 0; - } - if (connectorTask == null || connectorTask.isDone()) { connectorTask = scheduler.scheduleWithFixedDelay(new Runnable() { @Override @@ -187,9 +188,12 @@ private synchronized void initTransceiver() { switch (c.getESPVersion()) { case ESP2: transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager); + smackAvailable = false; + sendTeachOuts = false; break; case ESP3: transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager); + sendTeachOuts = c.sendTeachOuts; break; default: break; @@ -200,6 +204,7 @@ private synchronized void initTransceiver() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread..."); transceiver.StartReceiving(scheduler); + logger.info("EnOceanSerialTransceiver RX thread up and running"); if (c.rs485) { if (c.rs485BaseId != null && !c.rs485BaseId.isEmpty()) { @@ -238,6 +243,28 @@ public void responseReceived(RDBaseIdResponse response) { } } }); + + if (c.getESPVersion() == ESPVersion.ESP3) { + logger.debug("set postmaster mailboxes"); + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_POSTMASTER((byte) (c.enableSmack ? 20 : 0)), + new ResponseListenerIgnoringTimeouts() { + + @Override + public void responseReceived(BaseResponse response) { + + logger.debug("received response for postmaster mailboxes"); + if (response.isOK()) { + updateProperty("Postmaster mailboxes:", + Integer.toString(c.enableSmack ? 20 : 0)); + smackAvailable = c.enableSmack; + refreshProperties(); + } else { + updateProperty("Postmaster mailboxes:", "Not supported"); + smackAvailable = false; + } + } + }); + } } logger.debug("request version info"); @@ -283,7 +310,7 @@ public Collection getConfigStatus() { Collection configStatusMessages = new LinkedList<>(); // The serial port must be provided - String path = (String) getThing().getConfiguration().get(PATH); + String path = getThing().getConfiguration().as(EnOceanBridgeConfig.class).path; if (path == null || path.isEmpty()) { configStatusMessages.add(ConfigStatusMessage.Builder.error(PATH) .withMessageKeySuffix(EnOceanConfigStatusMessage.PORT_MISSING.getMessageKey()).withArguments(PATH) @@ -297,30 +324,33 @@ public byte[] getBaseId() { return baseId.clone(); } - public int getNextSenderId(Thing sender) { - // TODO: change id to enoceanId + public boolean isSmackClient(Thing sender) { + return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId); + } + + public Integer getNextSenderId(Thing sender) { return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId); } - public int getNextSenderId(String senderId) { - if (nextSenderId != 0 && sendingThings[nextSenderId] == null) { - int result = nextSenderId; - Configuration config = getConfig(); - config.put(NEXTSENDERID, null); - updateConfiguration(config); - nextSenderId = 0; + public Integer getNextSenderId(String enoceanId) { + EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class); - return result; + if (config.nextSenderId != null && sendingThings[config.nextSenderId] == null) { + Configuration c = this.editConfiguration(); + c.put(PARAMETER_NEXT_SENDERID, null); + updateConfiguration(c); + + return config.nextSenderId; } - for (byte i = 1; i < sendingThings.length; i++) { + for (int i = 1; i < sendingThings.length; i++) { if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId - .equalsIgnoreCase(senderId)) { + .equalsIgnoreCase(enoceanId)) { return i; } } - return -1; + return null; } public boolean existsSender(int id, Thing sender) { @@ -345,7 +375,7 @@ public void sendMessage(BasePacket message, ResponseListene } public void addPacketListener(PacketListener listener) { - addPacketListener(listener, listener.getSenderIdToListenTo()); + addPacketListener(listener, listener.getEnOceanIdToListenTo()); } public void addPacketListener(PacketListener listener, long senderIdToListenTo) { @@ -355,7 +385,7 @@ public void addPacketListener(PacketListener listener, long senderIdToListenTo) } public void removePacketListener(PacketListener listener) { - removePacketListener(listener, listener.getSenderIdToListenTo()); + removePacketListener(listener, listener.getEnOceanIdToListenTo()); } public void removePacketListener(PacketListener listener, long senderIdToListenTo) { @@ -364,12 +394,74 @@ public void removePacketListener(PacketListener listener, long senderIdToListenT } } - public void startDiscovery(PacketListener teachInListener) { + public void startDiscovery(TeachInListener teachInListener) { transceiver.startDiscovery(teachInListener); + + if (smackAvailable) { + // activate smack teach in + logger.debug("activate smack teach in"); + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true), + new ResponseListenerIgnoringTimeouts() { + + @Override + public void responseReceived(BaseResponse response) { + + if (response.isOK()) { + logger.debug("Smack teach in activated"); + } + } + }); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + } + } } public void stopDiscovery() { transceiver.stopDiscovery(); + + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null); + refreshProperties(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + } + } + + private void refreshProperties() { + if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) { + + logger.debug("request learned smack clients"); + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS, + new ResponseListenerIgnoringTimeouts() { + + @Override + public void responseReceived(RDLearnedClientsResponse response) { + + logger.debug("received response for learned smack clients"); + if (response.isValid() && response.isOK()) { + LearnedClient[] clients = response.getLearnedClients(); + updateProperty("Learned smart ack clients", Integer.toString(clients.length)); + updateProperty("Smart ack clients", + Arrays.stream(clients) + .map(x -> String.format("%s (MB Idx: %d)", + HexUtils.bytesToHex(x.clientId), x.mailboxIndex)) + .collect(Collectors.joining(", "))); + smackClients = Arrays.stream(clients).map(x -> HexUtils.bytesToHex(x.clientId)) + .collect(Collectors.toSet()); + } + } + }); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + + } + } } @Override @@ -378,4 +470,8 @@ public void ErrorOccured(Throwable exception) { transceiver = null; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage()); } + + public boolean sendTeachOuts() { + return sendTeachOuts; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java index 412cd9ad8d8fe..08a1e499e82d4 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java @@ -71,7 +71,7 @@ void initializeConfig() { } @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { return 0; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java index 73551cdc83bbe..f410d6e1aba8a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java @@ -13,8 +13,10 @@ package org.openhab.binding.enocean.internal.messages; import org.openhab.binding.enocean.internal.EnOceanBindingConstants; +import org.openhab.binding.enocean.internal.Helper; import org.openhab.binding.enocean.internal.messages.BasePacket.ESPPacketType; import org.openhab.binding.enocean.internal.messages.CCMessage.CCMessageType; +import org.openhab.binding.enocean.internal.messages.SAMessage.SAMessageType; import org.openhab.core.library.types.StringType; /** @@ -42,6 +44,28 @@ public static BasePacket CO_WR_REPEATER(StringType level) { } } + public static BasePacket SA_WR_LEARNMODE(boolean activate) { + return new SAMessage(SAMessageType.SA_WR_LEARNMODE, + new byte[] { SAMessageType.SA_WR_LEARNMODE.getValue(), (byte) (activate ? 1 : 0), 0, 0, 0, 0, 0 }); + } + + public final static BasePacket SA_RD_LEARNEDCLIENTS = new SAMessage(SAMessageType.SA_RD_LEARNEDCLIENTS); + + public static BasePacket SA_RD_MAILBOX_STATUS(byte[] clientId, byte[] controllerId) { + return new SAMessage(SAMessageType.SA_RD_MAILBOX_STATUS, + Helper.concatAll(new byte[] { SAMessageType.SA_RD_MAILBOX_STATUS.getValue() }, clientId, controllerId)); + } + + public static BasePacket SA_WR_POSTMASTER(byte mailboxes) { + return new SAMessage(SAMessageType.SA_WR_POSTMASTER, + new byte[] { SAMessageType.SA_WR_POSTMASTER.getValue(), mailboxes }); + } + + public static BasePacket SA_WR_CLIENTLEARNRQ(byte manu1, byte manu2, byte rorg, byte func, byte type) { + return new SAMessage(SAMessageType.SA_WR_CLIENTLEARNRQ, + new byte[] { SAMessageType.SA_WR_CLIENTLEARNRQ.getValue(), manu1, manu2, rorg, func, type }); + } + public static BasePacket BuildPacket(int dataLength, int optionalDataLength, byte packetType, byte[] payload) { ESPPacketType type = ESPPacketType.getPacketType(packetType); @@ -50,6 +74,8 @@ public static BasePacket BuildPacket(int dataLength, int optionalDataLength, byt return new Response(dataLength, optionalDataLength, payload); case RADIO_ERP1: return new ERP1Message(dataLength, optionalDataLength, payload); + case EVENT: + return new EventMessage(dataLength, optionalDataLength, payload); default: return null; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java new file mode 100644 index 0000000000000..4cacceba47052 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages; + +import java.util.stream.Stream; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class EventMessage extends BasePacket { + + public enum EventMessageType { + UNKNOWN((byte) 0x00, 1), + SA_RECLAIM_NOT_SUCCESSFUL((byte) 0x01, 1), + SA_CONFIRM_LEARN((byte) 0x02, 17), + SA_LEARN_ACK((byte) 0x03, 4), + CO_READY((byte) 0x04, 2), + CO_EVENT_SECUREDEVICES((byte) 0x05, 6), + CO_DUTYCYCLE_LIMIT((byte) 0x06, 2), + CO_TRANSMIT_FAILED((byte) 0x07, 2); + + private byte value; + private int dataLength; + + EventMessageType(byte value, int dataLength) { + this.value = value; + this.dataLength = dataLength; + } + + public byte getValue() { + return this.value; + } + + public int getDataLength() { + return dataLength; + } + + public static EventMessageType getEventMessageType(byte value) { + return Stream.of(EventMessageType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } + + private EventMessageType type; + + EventMessage(int dataLength, int optionalDataLength, byte[] payload) { + super(dataLength, optionalDataLength, ESPPacketType.EVENT, payload); + + type = EventMessageType.getEventMessageType(payload[0]); + } + + public EventMessageType getEventMessageType() { + return type; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java index 2c8a0699fe366..e7b8f2094d30a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java @@ -57,7 +57,7 @@ public static ResponseType getResponsetype(byte value) { protected ResponseType responseType; protected boolean _isValid = false; - protected Response(int dataLength, int optionalDataLength, byte[] payload) { + public Response(int dataLength, int optionalDataLength, byte[] payload) { super(dataLength, optionalDataLength, ESPPacketType.RESPONSE, payload); try { diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java similarity index 85% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java index 63de3f5cdd984..56a4ccbbf869d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java @@ -10,9 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; /** * diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java similarity index 91% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java index e2220c053f863..b2a8a0da72ea2 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java @@ -10,9 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; /** * diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java new file mode 100644 index 0000000000000..481915212bd31 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages.Responses; + +import java.util.Arrays; + +import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class RDLearnedClientsResponse extends Response { + + public class LearnedClient { + public byte[] clientId; + public byte[] controllerId; + public int mailboxIndex; + } + + LearnedClient[] learnedClients; + + public RDLearnedClientsResponse(Response response) { + this(response.getPayload().length, response.getOptionalPayload().length, + Helper.concatAll(response.getPayload(), response.getOptionalPayload())); + } + + RDLearnedClientsResponse(int dataLength, int optionalDataLength, byte[] payload) { + super(dataLength, optionalDataLength, payload); + + if (payload == null || ((payload.length - 1) % 9) != 0) { + return; + } else { + _isValid = true; + } + + learnedClients = new LearnedClient[(payload.length - 1) / 9]; + for (int i = 0; i < learnedClients.length; i++) { + LearnedClient client = new LearnedClient(); + client.clientId = Arrays.copyOfRange(payload, 1 + i * 9, 1 + i * 9 + 4); + client.controllerId = Arrays.copyOfRange(payload, 5 + i * 9, 5 + i * 9 + 4); + client.mailboxIndex = payload[9 + i * 9] & 0xFF; + learnedClients[i] = client; + } + } + + public LearnedClient[] getLearnedClients() { + return learnedClients; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java similarity index 93% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java index cecc623b86dcb..440068ba4dcfd 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java @@ -10,11 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.library.types.StringType; /** diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java similarity index 94% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java index 1374c56e01970..daedc9aa9ae01 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java @@ -10,11 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import java.util.Arrays; import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.util.HexUtils; /** diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java new file mode 100644 index 0000000000000..a827922d300fb --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages.Responses; + +import org.openhab.binding.enocean.internal.messages.Response; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class SMACKTeachInResponse extends Response { + + // set response time to 250ms + static final byte RESPONSE_TIME_HVALUE = 0; + static final byte RESPONSE_TIME_LVALUE = (byte) 0xFA; + + static final byte TEACH_IN = 0x00; + static final byte TEACH_OUT = 0x20; + static final byte REPEATED_TEACH_IN = 0x01; + static final byte NOPLACE_FOR_MAILBOX = 0x12; + static final byte BAD_RSSI = 0x14; + + public SMACKTeachInResponse() { + super(4, 0, new byte[] { Response.ResponseType.RET_OK.getValue(), RESPONSE_TIME_HVALUE, RESPONSE_TIME_LVALUE, + TEACH_IN }); + } + + public void setTeachOutResponse() { + data[3] = TEACH_OUT; + } + + public boolean isTeachOut() { + return data[3] == TEACH_OUT; + } + + public void setRepeatedTeachInResponse() { + data[3] = REPEATED_TEACH_IN; + } + + public void setNoPlaceForFurtherMailbox() { + data[3] = NOPLACE_FOR_MAILBOX; + } + + public void setBadRSSI() { + data[3] = BAD_RSSI; + } + + public void setTeachIn() { + data[3] = TEACH_IN; + } + + public boolean isTeachIn() { + return data[3] == TEACH_IN; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java new file mode 100644 index 0000000000000..e63087ee58717 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class SAMessage extends BasePacket { + + public enum SAMessageType { + SA_WR_LEARNMODE((byte) 0x01, 7), + SA_RD_LEARNMODE((byte) 0x02, 1), + SA_WR_LEARNCONFIRM((byte) 0x03, 1), + SA_WR_CLIENTLEARNRQ((byte) 0x04, 6), + SA_WR_RESET((byte) 0x05, 1), + SA_RD_LEARNEDCLIENTS((byte) 0x06, 1), + SA_WR_RECLAIMS((byte) 0x07, 1), + SA_WR_POSTMASTER((byte) 0x08, 2), + SA_RD_MAILBOX_STATUS((byte) 0x09, 9); + + private byte value; + private int dataLength; + + SAMessageType(byte value, int dataLength) { + this.value = value; + this.dataLength = dataLength; + } + + public byte getValue() { + return this.value; + } + + public int getDataLength() { + return dataLength; + } + } + + private SAMessageType type; + + public SAMessage(SAMessageType type) { + this(type, new byte[] { type.getValue() }); + } + + public SAMessage(SAMessageType type, byte[] payload) { + super(type.getDataLength(), 0, ESPPacketType.SMART_ACK_COMMAND, payload); + + this.type = type; + } + + public SAMessageType getSAMessageType() { + return type; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java index db15a3fec0c5f..28e8b04209630 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java @@ -18,8 +18,6 @@ import org.openhab.binding.enocean.internal.EnOceanException; import org.openhab.binding.enocean.internal.messages.BasePacket; -import org.openhab.binding.enocean.internal.messages.ERP1Message; -import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; import org.openhab.binding.enocean.internal.messages.ESP3Packet; import org.openhab.binding.enocean.internal.messages.ESP3PacketFactory; import org.openhab.binding.enocean.internal.messages.Response; @@ -138,21 +136,8 @@ protected void processMessage(byte firstByte) { HexUtils.bytesToHex(packet.getPayload())); break; case EVENT: - logger.debug("Event occured: {}", HexUtils.bytesToHex(packet.getPayload())); - break; - case RADIO_ERP1: { - ERP1Message msg = (ERP1Message) packet; - logger.debug("{} with RORG {} for {} payload {} received", - packet.getPacketType().name(), msg.getRORG().name(), - HexUtils.bytesToHex(msg.getSenderId()), HexUtils.bytesToHex( - Arrays.copyOf(dataBuffer, dataLength + optionalLength))); - - if (msg.getRORG() != RORG.Unknown) { - informListeners(msg); - } else { - logger.debug("Received unknown RORG"); - } - } + case RADIO_ERP1: + informListeners(packet); break; case RADIO_ERP2: break; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java index df729a1dbc0e2..55be4988b0259 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java @@ -27,9 +27,13 @@ import org.openhab.binding.enocean.internal.EnOceanBindingConstants; import org.openhab.binding.enocean.internal.EnOceanException; +import org.openhab.binding.enocean.internal.Helper; import org.openhab.binding.enocean.internal.messages.BasePacket; +import org.openhab.binding.enocean.internal.messages.BasePacket.ESPPacketType; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; @@ -138,7 +142,8 @@ private synchronized void send() throws IOException { Request currentRequest = null; protected Map> listeners; - protected PacketListener teachInListener; + protected HashSet eventListeners; + protected TeachInListener teachInListener; protected InputStream inputStream; protected OutputStream outputStream; @@ -151,6 +156,7 @@ public EnOceanTransceiver(String path, TransceiverErrorListener errorListener, S requestQueue = new RequestQueue(scheduler); listeners = new HashMap<>(); + eventListeners = new HashSet<>(); teachInListener = null; this.errorListener = errorListener; @@ -192,6 +198,7 @@ public void run() { } }); } + logger.info("EnOceanSerialTransceiver RX thread started"); } public void ShutDown() { @@ -266,36 +273,65 @@ protected int read(byte[] buffer, int length) { } } - protected void informListeners(ERP1Message msg) { + protected void informListeners(BasePacket packet) { try { - byte[] senderId = msg.getSenderId(); + if (packet.getPacketType() == ESPPacketType.RADIO_ERP1) { + ERP1Message msg = (ERP1Message) packet; + byte[] senderId = msg.getSenderId(); + byte[] d = Helper.concatAll(msg.getPayload(), msg.getOptionalPayload()); + + logger.debug("{} with RORG {} for {} payload {} received", packet.getPacketType().name(), + msg.getRORG().name(), HexUtils.bytesToHex(msg.getSenderId()), HexUtils.bytesToHex(d)); + + if (msg.getRORG() != RORG.Unknown) { + if (senderId != null) { + if (filteredDeviceId != null && senderId[0] == filteredDeviceId[0] + && senderId[1] == filteredDeviceId[1] && senderId[2] == filteredDeviceId[2]) { + // filter away own messages which are received through a repeater + return; + } - if (senderId != null) { - if (filteredDeviceId != null && senderId[0] == filteredDeviceId[0] && senderId[1] == filteredDeviceId[1] - && senderId[2] == filteredDeviceId[2]) { - // filter away own messages which are received through a repeater - return; - } + if (teachInListener != null && (msg.getIsTeachIn() || msg.getRORG() == RORG.RPS)) { + logger.info("Received teach in message from {}", HexUtils.bytesToHex(msg.getSenderId())); + teachInListener.packetReceived(msg); + return; + } else if (teachInListener == null && msg.getIsTeachIn()) { + logger.info("Discard message because this is a teach-in telegram from {}!", + HexUtils.bytesToHex(msg.getSenderId())); + return; + } - if (teachInListener != null) { - if (msg.getIsTeachIn() || (msg.getRORG() == RORG.RPS)) { - logger.info("Received teach in message from {}", HexUtils.bytesToHex(msg.getSenderId())); - teachInListener.packetReceived(msg); - return; + long s = Long.parseLong(HexUtils.bytesToHex(senderId), 16); + HashSet pl = listeners.get(s); + if (pl != null) { + pl.forEach(l -> l.packetReceived(msg)); + } } } else { - if (msg.getIsTeachIn()) { - logger.info("Discard message because this is a teach-in telegram from {}!", - HexUtils.bytesToHex(msg.getSenderId())); + logger.debug("Received unknown RORG"); + } + } else if (packet.getPacketType() == ESPPacketType.EVENT) { + EventMessage event = (EventMessage) packet; + + byte[] d = Helper.concatAll(packet.getPayload(), packet.getOptionalPayload()); + logger.debug("{} with type {} payload {} received", ESPPacketType.EVENT.name(), + event.getEventMessageType().name(), HexUtils.bytesToHex(d)); + + if (event.getEventMessageType() == EventMessageType.SA_CONFIRM_LEARN) { + byte[] senderId = event.getPayload(EventMessageType.SA_CONFIRM_LEARN.getDataLength() - 5, 4); + + if (teachInListener != null) { + logger.info("Received smart teach in from {}", HexUtils.bytesToHex(senderId)); + teachInListener.eventReceived(event); + return; + } else { + logger.info("Discard message because this is a smart teach-in telegram from {}!", + HexUtils.bytesToHex(senderId)); return; } } - long s = Long.parseLong(HexUtils.bytesToHex(senderId), 16); - HashSet pl = listeners.get(s); - if (pl != null) { - pl.forEach(l -> l.packetReceived(msg)); - } + eventListeners.forEach(l -> l.eventReceived(event)); } } catch (Exception e) { logger.error("Exception in informListeners", e); @@ -354,7 +390,15 @@ public void removePacketListener(PacketListener listener, long senderIdToListenT } } - public void startDiscovery(PacketListener teachInListener) { + public void addEventMessageListener(EventListener listener) { + eventListeners.add(listener); + } + + public void removeEventMessageListener(EventListener listener) { + eventListeners.remove(listener); + } + + public void startDiscovery(TeachInListener teachInListener) { this.teachInListener = teachInListener; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java new file mode 100644 index 0000000000000..2a27bfb14f15b --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.transceiver; + +import org.openhab.binding.enocean.internal.messages.EventMessage; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public interface EventListener { + public void eventReceived(EventMessage event); +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java index 04e55fdba857b..da161441aa87a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java @@ -22,5 +22,5 @@ public interface PacketListener { public void packetReceived(BasePacket packet); - public long getSenderIdToListenTo(); + public long getEnOceanIdToListenTo(); } diff --git a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/Hhphc.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java similarity index 64% rename from bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/Hhphc.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java index e3f81fcc274f4..06a26c41800c0 100644 --- a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/Hhphc.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java @@ -10,17 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.teleinfo.internal.dto.common; +package org.openhab.binding.enocean.internal.transceiver; /** - * The {@link Hhphc} enumeration defines all HHPHC values. * - * @author Nicolas SIBERIL - Initial contribution + * @author Daniel Weber - Initial contribution */ -public enum Hhphc { - A, - C, - D, - E, - Y +public interface TeachInListener extends PacketListener, EventListener { + } diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/config/config.xml index 199039a8eafb1..ebd49c604dd7b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/config/config.xml @@ -5,15 +5,13 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + The type of the transformation, e.g. "MAP" - true - + The transformation function, e.g. for transformation type map => filename of mapping file - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/AutomatedMeterSensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/AutomatedMeterSensor.xml index 20db15ce26eb8..11d4daae595a2 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/AutomatedMeterSensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/AutomatedMeterSensor.xml @@ -14,12 +14,11 @@ Sensor for different meters like energy measurement (EEP: A5-12) - + EnOceanId of device this thing belongs to - true - + EEP which is used by panel @@ -29,7 +28,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml index 7115374ab58f8..31a5b2c166697 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml @@ -13,30 +13,28 @@ Controls a switching or dimming actuator (EEP: A5-38) - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - + true - true true - + @@ -44,7 +42,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml index 6d24ab2084156..4fa6e9da97390 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml @@ -20,12 +20,12 @@ - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - + EEP which is used to control the device @@ -34,9 +34,8 @@ true F6_02_01 - true - + EEP which is used by rocker switch listener(s) @@ -45,7 +44,6 @@ true F6_02_01 - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Contact.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Contact.xml index 57f6069ea4945..648cb2c6b6253 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Contact.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Contact.xml @@ -13,12 +13,11 @@ Single input contact sensor (EEP: D5-00) - + EnOceanId of device this thing belongs to - true - + EEP which is used by contact @@ -30,7 +29,6 @@ D5_00_01 2 true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/EnvironmentalSensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/EnvironmentalSensor.xml index aae17def9b664..0962fb9f6128d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/EnvironmentalSensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/EnvironmentalSensor.xml @@ -13,19 +13,17 @@ Sensor for different environmental sensors like a weather station (EEP: A5-13) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml index 2fa60566e40e8..67cfc54e72a63 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml @@ -13,17 +13,16 @@ Thing whose EEP is unsupported. Use a TRANSFORM to convert things messages. - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - + @@ -31,9 +30,8 @@ true - true - + @@ -41,7 +39,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml index cd58af2cbf2fa..46d461cf8e5fb 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml @@ -13,18 +13,16 @@ Sensor and actuator to control heat recovery ventilation units (EEP: D2-50) - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - true - + @@ -33,13 +31,12 @@ true - true false - + EEP which is used by Ventilation Unit @@ -49,7 +46,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightSensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightSensor.xml index 632067232aeb4..00d978f5ed546 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightSensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightSensor.xml @@ -13,12 +13,11 @@ Sensor which sends light data (EEP: A5-06) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor @@ -26,7 +25,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightTemperatureOccupancySensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightTemperatureOccupancySensor.xml index c67a1fbdc3918..c1c36d1c6daec 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightTemperatureOccupancySensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/LightTemperatureOccupancySensor.xml @@ -13,12 +13,11 @@ Sensor which sends light, temperature and occupancy data (EEP: A5-08) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor @@ -28,7 +27,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml index 0f095d5ed6e18..abe36f166243e 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml @@ -13,23 +13,21 @@ Electronic switches and dimmers with energy measurement and local control (EEP: D2-01) - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - true Time in seconds after a refresh is triggerd, 0 to disable 300 - + @@ -55,13 +53,12 @@ true - true false - + @@ -89,7 +86,6 @@ 2 true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MechanicalHandle.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MechanicalHandle.xml index 95f18fad67553..775eab0a85be2 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MechanicalHandle.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MechanicalHandle.xml @@ -14,12 +14,11 @@ Mechanical handle sensor for window/door handles - + EnOceanId of device this thing belongs to - true - + EEP which is used by handle @@ -29,7 +28,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MultiFunctionSmokeDetector.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MultiFunctionSmokeDetector.xml index 3b367bf202237..e292562a738ac 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MultiFunctionSmokeDetector.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MultiFunctionSmokeDetector.xml @@ -13,12 +13,11 @@ Multi Function Sensor like a Smoke Detector (EEP: F6-05, D2-14) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor @@ -26,7 +25,6 @@ - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/OccupancySensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/OccupancySensor.xml index c6d093c2cc3b2..7f770684e64c2 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/OccupancySensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/OccupancySensor.xml @@ -13,12 +13,11 @@ Sensor which sends light, supply voltage and occupancy data (EEP: A5-07) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor @@ -27,7 +26,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/PushButton.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/PushButton.xml index d743aece64f20..635d291363312 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/PushButton.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/PushButton.xml @@ -14,12 +14,11 @@ Represents a physical Push Button (EEP: F6-01, D2-03) - + EnOceanId of device this thing belongs to - true - + EEP which is used by push button @@ -28,7 +27,6 @@ true F6_01_01 - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RockerSwitch.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RockerSwitch.xml index 505f02c2bbf92..c8652487d8b16 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RockerSwitch.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RockerSwitch.xml @@ -23,12 +23,11 @@ - + EnOceanId of device this thing belongs to - true - + EEP which is used by rocker switch @@ -36,7 +35,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml index 486ceebf0e2e4..b0f00d97962fb 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml @@ -13,12 +13,11 @@ Rollershutter like Eltako FSB14/61/71 or NodOn SIN-2-RS-01 (EEP: A5-37, D2-05) - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge @@ -28,7 +27,7 @@ Time in seconds after a refresh is triggered, 0 to disable 300 - + @@ -37,13 +36,12 @@ A5_3F_7F_EltakoFSB true - true true - + @@ -53,7 +51,6 @@ 4 true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RoomOperatingPanel.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RoomOperatingPanel.xml index e9829d5dc836f..c8a177104763d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RoomOperatingPanel.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/RoomOperatingPanel.xml @@ -13,12 +13,11 @@ Room operating panel with different kinds of sensors (EEP: A5-10) - + EnOceanId of device this thing belongs to - true - + EEP which is used by panel @@ -58,7 +57,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureHumiditySensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureHumiditySensor.xml index a057604fcfac4..fd7282fa92009 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureHumiditySensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureHumiditySensor.xml @@ -13,12 +13,11 @@ Sensor which sends temperature and humidity data (EEP: A5-04) - + EnOceanId of device this thing belongs to - true - + EEP which is used by sensor @@ -28,7 +27,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureSensor.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureSensor.xml index 5e233edca17f8..9f28e772caa04 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureSensor.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/TemperatureSensor.xml @@ -13,12 +13,11 @@ Sensor which send temperature data (EEP: A5-02) - + EnOceanId of device this thing belongs to - true - + EEP which is used by panel @@ -49,7 +48,6 @@ true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml index 2be75d8e93033..750bbb27d650c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml @@ -13,18 +13,16 @@ Sensor and actuator to control radiator thermostats - + EnOceanId of device this thing belongs to - true - + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge - true - + EEP which is used by sensor @@ -33,9 +31,8 @@ "A5_20_04" true - true - + "A5_20_04" true - true diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml index afa99f504c7f8..4938404623f5d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml @@ -15,12 +15,11 @@ - + Path to the EnOcean gateway - true - + true @@ -28,9 +27,13 @@ true - true ESP3 + + + Declare Gateway as a SMACK Postmaster and handle SMACK messages + true + true @@ -41,9 +44,15 @@ 00000000 - + + true + + Should a learned in or teach out response been send on a repeated smack teach in request + false + + true - + Defines the next device Id, if empty, the next device id is automatically determined diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml index 9469d48b8c9af..941eb0d76487a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml @@ -488,13 +488,6 @@ - - Switch - - You can send telegrams to the device by switching this channel on - thermostat - - Switch diff --git a/bundles/org.openhab.binding.enphase/NOTICE b/bundles/org.openhab.binding.enphase/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.enphase/README.md b/bundles/org.openhab.binding.enphase/README.md new file mode 100644 index 0000000000000..4f768bb405b4a --- /dev/null +++ b/bundles/org.openhab.binding.enphase/README.md @@ -0,0 +1,113 @@ +# Enphase Binding + +This is the binding for the [Enphase](https://enphase.com/) Envoy Solar Panel gateway. +The binding uses the local API of the Envoy gateway. +Some calls can be made without authentication and some use a user name and password. +The default user name is `envoy` and the default password is the last 6 numbers of the serial number. +The Envoy gateway updates the data every 5 minutes. +Therefore using a refresh rate shorter doesn't provide more information. + +## Supported Things + +The follow things are supported: + +* `envoy` The Envoy gateway thing, which is a bridge thing. +* `inverter` A Enphase micro inverter connected to a solar panel. +* `relay` A Enphase relay. + +Not all Envoy gateways support all channels and things. +Therefore some data on inverters and the relay may not be available. +The binding auto detects which data is available and will report this in the log on initialization of the gateway bridge. + +## Discovery + +The binding can discover Envoy gateways, micro inverters and relays. + +## Thing Configuration + +The Envoy gateway thing `envoy` has the following configuration options: + +| parameter | required | description | +|--------------|----------|-------------------------------------------------------------------------------------------------------------| +| serialNumber | yes | The serial number of the Envoy gateway which can be found on the gateway | +| hostname | no | The host name/ip address of the Envoy gateway. Leave empty to auto detect | +| username | no | The user name to the Envoy gateway. Leave empty when using the default user name | +| password | no | The password to the Envoy gateway. Leave empty when using the default password | +| refresh | no | Period between data updates. The default is the same 5 minutes the data is actual refreshed on the Envoy | + +The micro inverter `inverter` and `relay` things have only 1 parameter: + +| parameter | required | description | +|--------------|----------|-----------------------------------| +| serialNumber | yes | The serial number of the inverter | + +## Channels + +The `envoy` thing has can show both production as well as consumption data. +There are channel groups for `production` and `consumption` data. +The `consumption` data is only available if the gateway reports this. +A example of a production channel name is: `production#wattsNow`. + +| channel | type | description | +|--------------------|---------------|---------------------------------------| +| wattHoursToday | Number:Energy | Watt hours produced today | +| wattHoursSevenDays | Number:Energy | Watt hours produced the last 7 days | +| wattHoursLifetime | Number:Energy | Watt hours produced over the lifetime | +| wattsNow | Number:Power | Latest watts produced | + +The `inverter` thing has the following channels: + +| channel | type | description | +|-----------------|--------------|--------------------------------------| +| lastReportWatts | Number:Power | Last reported power delivery | +| maxReportWatts | Number:Power | Maximum reported power | +| lastReportDate | DateTime | Date of last reported power delivery | + +The following channels are only available if supported by the Envoy gateway: + +The `relay` thing has the following channels: + +| channel | type | description | +|-----------------|--------------|--------------------------------------------------------| +| relay | Contact | Status of the relay. | +| line1Connected | Contact | If power line 1 is connected. If closed it's connected | +| line2Connected | Contact | If power line 2 is connected. If closed it's connected | +| line2Connected | Contact | If power line 3 is connected. If closed it's connected | + +The `inverter` and `relay` have the following additional advanced channels: + +| channel | type | description | +|-----------------|--------------------|--------------------------------------| +| producing | Switch (Read Only) | If the device is producing | +| communicating | Switch (Read Only) | If the device is communicating | +| provisioned | Switch (Read Only) | If the device is provisioned | +| operating | Switch (Read Only) | If the device is operating | + +## Full Example + +Things example: + +``` +Bridge enphase:envoy:789012 "Envoy" [ serialNumber="12345789012" ] { + Things: + inverter 123456 "Enphase Inverter 123456" [ serialNumber="789012123456" ] + inverter 223456 "Enphase Inverter 223456" [ serialNumber="789012223456" ] +} +``` + +Items example: + +``` +Number:Power envoyWattsNow "Watts Now [%d %unit%]" { channel="enphase:envoy:789012:production#wattsNow" } +Number:Energy envoyWattHoursToday "Watt Hours Today [%d %unit%]" { channel="enphase:envoy:789012:production#wattHoursToday" } +Number:Energy envoyWattHours7Days "Watt Hours 7 Days [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursSevenDays" } +Number:Energy envoyWattHoursLifetime "Watt Hours Lifetime [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursLifetime" } + +Number:Power i1LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:123456:lastReportWatts" } +Number:Power i1MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:123456:maxReportWatts" } +DateTime i1LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:123456:lastReportDate" } + +Number:Power i2LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:223456:lastReportWatts" } +Number:Power i21MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:223456:maxReportWatts" } +DateTime i2LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:223456:lastReportDate" } +``` diff --git a/bundles/org.openhab.binding.enphase/pom.xml b/bundles/org.openhab.binding.enphase/pom.xml new file mode 100644 index 0000000000000..12e5c60bac012 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.enphase + + openHAB Add-ons :: Bundles :: Enphase Binding + + diff --git a/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml b/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml new file mode 100644 index 0000000000000..537cf0dc53526 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.enphase/${project.version} + + diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java new file mode 100644 index 0000000000000..4d08743545709 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EnphaseBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnphaseBindingConstants { + + private static final String BINDING_ID = "enphase"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ENPHASE_ENVOY = new ThingTypeUID(BINDING_ID, "envoy"); + public static final ThingTypeUID THING_TYPE_ENPHASE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter"); + public static final ThingTypeUID THING_TYPE_ENPHASE_RELAY = new ThingTypeUID(BINDING_ID, "relay"); + + // Configuration parameters + public static final String CONFIG_SERIAL_NUMBER = "serialNumber"; + public static final String CONFIG_HOSTNAME = "hostname"; + public static final String CONFIG_USERNAME = "username"; + public static final String CONFIG_PASSWORD = "password"; + public static final String CONFIG_REFRESH = "refresh"; + public static final String PROPERTY_VERSION = "version"; + + // Envoy gateway channels + public static final String ENVOY_CHANNELGROUP_CONSUMPTION = "consumption"; + public static final String ENVOY_WATT_HOURS_TODAY = "wattHoursToday"; + public static final String ENVOY_WATT_HOURS_SEVEN_DAYS = "wattHoursSevenDays"; + public static final String ENVOY_WATT_HOURS_LIFETIME = "wattHoursLifetime"; + public static final String ENVOY_WATTS_NOW = "wattsNow"; + + // Device channels + public static final String DEVICE_CHANNEL_STATUS = "status"; + public static final String DEVICE_CHANNEL_PRODUCING = "producing"; + public static final String DEVICE_CHANNEL_COMMUNICATING = "communicating"; + public static final String DEVICE_CHANNEL_PROVISIONED = "provisioned"; + public static final String DEVICE_CHANNEL_OPERATING = "operating"; + + // Inverter channels + public static final String INVERTER_CHANNEL_LAST_REPORT_WATTS = "lastReportWatts"; + public static final String INVERTER_CHANNEL_MAX_REPORT_WATTS = "maxReportWatts"; + public static final String INVERTER_CHANNEL_LAST_REPORT_DATE = "lastReportDate"; + + // Relay channels + public static final String RELAY_CHANNEL_RELAY = "relay"; + public static final String RELAY_CHANNEL_LINE_1_CONNECTED = "line1Connected"; + public static final String RELAY_CHANNEL_LINE_2_CONNECTED = "line2Connected"; + public static final String RELAY_CHANNEL_LINE_3_CONNECTED = "line3Connected"; + + public static final String RELAY_STATUS_CLOSED = "closed"; + + // Properties + public static final String DEVICE_PROPERTY_PART_NUMBER = "partNumber"; + + // Discovery constants + public static final String DISCOVERY_SERIAL = "serialnum"; + public static final String DISCOVERY_VERSION = "protovers"; + + // Status messages + public static final String DEVICE_STATUS_OK = "envoy.global.ok"; + public static final String ERROR_NODATA = "error.nodata"; + + public enum EnphaseDeviceType { + ACB, // AC Battery + PSU, // Inverter + NSRB; // Network system relay controller + + public static @Nullable EnphaseDeviceType safeValueOf(final String type) { + try { + return valueOf(type); + } catch (final IllegalArgumentException e) { + return null; + } + } + } + + /** + * Derives the default password from the serial number. + * + * @param serialNumber serial number to use + * @return the default password or empty string if serial number is to short. + */ + public static String defaultPassword(final String serialNumber) { + return isValidSerial(serialNumber) ? serialNumber.substring(serialNumber.length() - 6) : ""; + } + + /** + * Checks if the serial number is at least long enough to contain the default password. + * + * @param serialNumber serial number to check + * @return true if not null and at least 6 characters long. + */ + public static boolean isValidSerial(@Nullable final String serialNumber) { + return serialNumber != null && serialNumber.length() > 6; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java new file mode 100644 index 0000000000000..4f267a5cd51c0 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.enphase.internal.handler.EnphaseInverterHandler; +import org.openhab.binding.enphase.internal.handler.EnphaseRelayHandler; +import org.openhab.binding.enphase.internal.handler.EnvoyBridgeHandler; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EnphaseHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.enphase", service = ThingHandlerFactory.class) +public class EnphaseHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ENPHASE_ENVOY, + THING_TYPE_ENPHASE_INVERTER, THING_TYPE_ENPHASE_RELAY); + + private final MessageTranslator messageTranslator; + private final HttpClient commonHttpClient; + private final EnvoyHostAddressCache envoyHostAddressCache; + + @Activate + public EnphaseHandlerFactory(final @Reference LocaleProvider localeProvider, + final @Reference TranslationProvider i18nProvider, final @Reference HttpClientFactory httpClientFactory, + @Reference final EnvoyHostAddressCache envoyHostAddressCache) { + messageTranslator = new MessageTranslator(localeProvider, i18nProvider); + commonHttpClient = httpClientFactory.getCommonHttpClient(); + this.envoyHostAddressCache = envoyHostAddressCache; + } + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ENPHASE_ENVOY.equals(thingTypeUID)) { + return new EnvoyBridgeHandler((Bridge) thing, commonHttpClient, envoyHostAddressCache); + } else if (THING_TYPE_ENPHASE_INVERTER.equals(thingTypeUID)) { + return new EnphaseInverterHandler(thing, messageTranslator); + } else if (THING_TYPE_ENPHASE_RELAY.equals(thingTypeUID)) { + return new EnphaseRelayHandler(thing, messageTranslator); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java new file mode 100644 index 0000000000000..e9a0aef7d710a --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EnvoyConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnvoyConfiguration { + + public static final String DEFAULT_USERNAME = "envoy"; + private static final int DEFAULT_REFRESH_MINUTES = 5; + + public String serialNumber = ""; + public String hostname = ""; + public String username = DEFAULT_USERNAME; + public String password = ""; + public int refresh = DEFAULT_REFRESH_MINUTES; + + @Override + public String toString() { + return "EnvoyConfiguration [serialNumber=" + serialNumber + ", hostname=" + hostname + ", username=" + username + + ", password=" + password + ", refresh=" + refresh + "]"; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java new file mode 100644 index 0000000000000..d468d3d32053e --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Exception thrown when a connection problem occurs to the Envoy gateway. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnvoyConnectionException extends Exception { + + private static final long serialVersionUID = 1L; + + public EnvoyConnectionException(final String message) { + super(message); + } + + public EnvoyConnectionException(final String message, final @Nullable Throwable e) { + super(message + (e == null ? "" : e.getMessage()), e); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java new file mode 100644 index 0000000000000..be6198d3b63ab --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Service that keeps track of host names/ip addresses of discovered Envoy devices. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public interface EnvoyHostAddressCache { + + /** + * Returns the known host name/ip address for the device with the given serial number. + * If not known an empty string is returned. + * + * @param serialNumber serial number of device to get host address for + * @return the known host address or an empty string if not known + */ + String getLastKnownHostAddress(String serialNumber); +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java new file mode 100644 index 0000000000000..95c25c6b4e04c --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when a api call is made while the hostname / ip address is not set. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnvoyNoHostnameException extends Exception { + + private static final long serialVersionUID = 1L; + + public EnvoyNoHostnameException(final String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java new file mode 100644 index 0000000000000..71e60408926c9 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ERROR_NODATA; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +/** + * Class to get the message for the enphase message code. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class MessageTranslator { + + private final LocaleProvider localeProvider; + private final TranslationProvider i18nProvider; + private final Bundle bundle; + + public MessageTranslator(LocaleProvider localeProvider, TranslationProvider i18nProvider) { + this.localeProvider = localeProvider; + this.i18nProvider = i18nProvider; + bundle = FrameworkUtil.getBundle(this.getClass()); + } + + /** + * Gets the message text for the enphase message code. + * + * @param key the enphase message code + * @return translated key + */ + public @Nullable String translate(String key) { + return i18nProvider.getText(bundle, key, ERROR_NODATA, localeProvider.getLocale()); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java new file mode 100644 index 0000000000000..7b1bfe3fe0409 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.discovery; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.enphase.internal.EnphaseBindingConstants.EnphaseDeviceType; +import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO; +import org.openhab.binding.enphase.internal.dto.InverterDTO; +import org.openhab.binding.enphase.internal.handler.EnvoyBridgeHandler; +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.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovery service to discovery Enphase inverters connected to an Envoy gateway. + * + * @author Thomas Hentschel - Initial contribution + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnphaseDevicesDiscoveryService extends AbstractDiscoveryService + implements ThingHandlerService, DiscoveryService { + + private static final int TIMEOUT_SECONDS = 20; + + private final Logger logger = LoggerFactory.getLogger(EnphaseDevicesDiscoveryService.class); + private @Nullable EnvoyBridgeHandler envoyHandler; + + public EnphaseDevicesDiscoveryService() { + super(Collections.singleton(THING_TYPE_ENPHASE_INVERTER), TIMEOUT_SECONDS, false); + } + + @Override + public void setThingHandler(final @Nullable ThingHandler handler) { + if (handler instanceof EnvoyBridgeHandler) { + envoyHandler = (EnvoyBridgeHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return envoyHandler; + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected void startScan() { + removeOlderResults(getTimestampOfLastScan()); + final EnvoyBridgeHandler envoyHandler = this.envoyHandler; + + if (envoyHandler == null || !envoyHandler.isOnline()) { + logger.debug("Envoy handler not available or online: {}", envoyHandler); + return; + } + final ThingUID uid = envoyHandler.getThing().getUID(); + + scanForInverterThings(envoyHandler, uid); + scanForDeviceThings(envoyHandler, uid); + } + + private void scanForInverterThings(final EnvoyBridgeHandler envoyHandler, final ThingUID bridgeID) { + final Map inverters = envoyHandler.getInvertersData(true); + + if (inverters == null) { + logger.debug("No inverter data for Enphase inverters in discovery for Envoy {}.", bridgeID); + } else { + for (final Entry entry : inverters.entrySet()) { + discover(bridgeID, entry.getKey(), THING_TYPE_ENPHASE_INVERTER, "Inverter "); + } + } + } + + /** + * Scans for other device things ('other' as in: no inverters). + * + * @param envoyHandler + * @param bridgeID + */ + private void scanForDeviceThings(final EnvoyBridgeHandler envoyHandler, final ThingUID bridgeID) { + final Map devices = envoyHandler.getDevices(true); + + if (devices == null) { + logger.debug("No device data for Enphase devices in discovery for Envoy {}.", bridgeID); + } else { + for (final Entry entry : devices.entrySet()) { + final DeviceDTO dto = entry.getValue(); + final EnphaseDeviceType type = dto == null ? null : EnphaseDeviceType.safeValueOf(dto.type); + + if (type == EnphaseDeviceType.NSRB) { + discover(bridgeID, entry.getKey(), THING_TYPE_ENPHASE_RELAY, "Relay "); + } + } + } + } + + private void discover(final ThingUID bridgeID, final String serialNumber, final ThingTypeUID typeUID, + final String label) { + final String shortSerialNumber = defaultPassword(serialNumber); + final ThingUID thingUID = new ThingUID(typeUID, bridgeID, shortSerialNumber); + final Map properties = new HashMap<>(1); + + properties.put(CONFIG_SERIAL_NUMBER, serialNumber); + final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeID) + .withRepresentationProperty(CONFIG_SERIAL_NUMBER).withProperties(properties) + .withLabel("Enphase " + label + shortSerialNumber).build(); + thingDiscovered(discoveryResult); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java new file mode 100644 index 0000000000000..beae1cabfed10 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.discovery; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import java.net.Inet4Address; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.enphase.internal.EnphaseBindingConstants; +import org.openhab.binding.enphase.internal.EnvoyHostAddressCache; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +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; + +/** + * MDNS discovery participant for discovering Envoy gateways. + * This service also keeps track of any discovered Envoys host name to provide this information for existing Envoy + * bridges + * so the bridge cat get the host name/ip address if that is unknown. + * + * @author Thomas Hentschel - Initial contribution + * @author Hilbrand Bouwkamp - Initial contribution + */ +@Component(service = { EnvoyHostAddressCache.class, MDNSDiscoveryParticipant.class }) +@NonNullByDefault +public class EnvoyDiscoveryParticipant implements MDNSDiscoveryParticipant, EnvoyHostAddressCache { + private static final String ENVOY_MDNS_ID = "envoy"; + + private final Logger logger = LoggerFactory.getLogger(EnvoyDiscoveryParticipant.class); + + private final Map lastKnownHostAddresses = new ConcurrentHashMap<>(); + + @Override + public Set getSupportedThingTypeUIDs() { + return Collections.singleton(EnphaseBindingConstants.THING_TYPE_ENPHASE_ENVOY); + } + + @Override + public String getServiceType() { + return "_enphase-envoy._tcp.local."; + } + + @Override + public @Nullable DiscoveryResult createResult(final ServiceInfo info) { + final String id = info.getName(); + + logger.debug("id found: {} with type: {}", id, info.getType()); + + if (!id.contains(ENVOY_MDNS_ID)) { + return null; + } + + if (info.getInet4Addresses().length == 0 || info.getInet4Addresses()[0] == null) { + return null; + } + + final ThingUID uid = getThingUID(info); + + if (uid == null) { + return null; + } + + final Inet4Address hostname = info.getInet4Addresses()[0]; + final String serialNumber = info.getPropertyString(DISCOVERY_SERIAL); + + if (serialNumber == null) { + logger.debug("No serial number found in data for discovered Envoy {}: {}", id, info); + return null; + } + final String version = info.getPropertyString(DISCOVERY_VERSION); + final String hostAddress = hostname == null ? "" : hostname.getHostAddress(); + + lastKnownHostAddresses.put(serialNumber, hostAddress); + final Map properties = new HashMap<>(3); + + properties.put(CONFIG_SERIAL_NUMBER, serialNumber); + properties.put(CONFIG_HOSTNAME, hostAddress); + properties.put(PROPERTY_VERSION, version); + return DiscoveryResultBuilder.create(uid).withProperties(properties) + .withRepresentationProperty(CONFIG_SERIAL_NUMBER) + .withLabel("Enphase Envoy " + defaultPassword(serialNumber)).build(); + } + + @Override + public String getLastKnownHostAddress(final String serialNumber) { + final String hostAddress = lastKnownHostAddresses.get(serialNumber); + + return hostAddress == null ? "" : hostAddress; + } + + @Override + public @Nullable ThingUID getThingUID(final ServiceInfo info) { + final String name = info.getName(); + + if (!name.contains(ENVOY_MDNS_ID)) { + logger.trace("Found other type of device that is not recognized as an Envoy: {}", name); + return null; + } + if (info.getInet4Addresses().length == 0 || info.getInet4Addresses()[0] == null) { + logger.debug("Found an Envoy, but no ip address is given: {}", info); + return null; + } + logger.debug("ServiceInfo addr: {}", info.getInet4Addresses()[0]); + if (getServiceType().equals(info.getType())) { + final String serial = info.getPropertyString(DISCOVERY_SERIAL); + + logger.debug("Discovered an Envoy with serial number '{}'", serial); + return new ThingUID(THING_TYPE_ENPHASE_ENVOY, serial); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java new file mode 100644 index 0000000000000..8857858bf6158 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.dto; + +/** + * Data from api/v1/production api call. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +public class EnvoyEnergyDTO { + public int wattHoursToday; + public int wattHoursSevenDays; + public int wattHoursLifetime; + public int wattsNow; +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java new file mode 100644 index 0000000000000..a8a29d8649cf6 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.dto; + +/** + * Data class for handling errors returned by the Envoy gateway. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +public class EnvoyErrorDTO { + public int status; + public String error; + public String info; + public String moreInfo; + + @Override + public String toString() { + return "EnvoyErrorDTO [status=" + status + ", error=" + error + ", info=" + info + ", moreInfo=" + moreInfo + + "]"; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java new file mode 100644 index 0000000000000..d6efa5ae0c73f --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Hilbrand Bouwkamp - Initial contribution + */ +public class InventoryJsonDTO { + + public class DeviceDTO { + public String type; + + @SerializedName("part_num") + public String partNumber; + @SerializedName("serial_num") + public String serialNumber; + + @SerializedName("device_status") + private String[] deviceStatus; + @SerializedName("last_rpt_date") + public String lastReportDate; + public boolean producing; + public boolean communicating; + public boolean provisioned; + public boolean operating; + // NSRB data + public String relay; + @SerializedName("line1-connected") + public boolean line1Connected; + @SerializedName("line2-connected") + public boolean line2Connected; + @SerializedName("line3-connected") + public boolean line3Connected; + + public String getSerialNumber() { + return serialNumber; + } + + public String getDeviceStatus() { + return deviceStatus == null || deviceStatus.length == 0 ? "" : deviceStatus[0]; + } + } + + public String type; + public DeviceDTO[] devices; +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java new file mode 100644 index 0000000000000..28a400054964b --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.dto; + +/** + * Data class for Enphase Inverter data. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +public class InverterDTO { + public String serialNumber; + public long lastReportDate; + public int devType; + public int lastReportWatts; + public int maxReportWatts; + + /** + * @return the serialNumber + */ + public String getSerialNumber() { + return serialNumber; + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java new file mode 100644 index 0000000000000..3513e5cd33511 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.dto; + +/** + * Data class for Envoy production and consumption data from production.json api call. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +public class ProductionJsonDTO { + + public static class DataDTO { + public String type; + public int activeCount; + public float whLifetime; + public float whLastSevenDays; + public float whToday; + public float wNow; + public float rmsCurrent; + public float rmsVoltage; + public float reactPwr; + public float apprntPwr; + public float pwrFactor; + public long readingTime; + public float varhLeadToday; + public float varhLagToday; + public float vahToday; + public float varhLeadLifetime; + public float varhLagLifetime; + public float vahLifetime; + } + + public DataDTO[] production; + public DataDTO[] consumption; +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java new file mode 100644 index 0000000000000..7ffcd0f7a795a --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.handler; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.enphase.internal.EnphaseBindingConstants; +import org.openhab.binding.enphase.internal.MessageTranslator; +import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Generic base Thing handler for different Enphase devices. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +abstract class EnphaseDeviceHandler extends BaseThingHandler { + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + protected @Nullable DeviceDTO lastKnownDeviceState; + + private final MessageTranslator messageTranslator; + private String serialNumber = ""; + + public EnphaseDeviceHandler(final Thing thing, MessageTranslator messageTranslator) { + super(thing); + this.messageTranslator = messageTranslator; + } + + /** + * @return the serialNumber + */ + public String getSerialNumber() { + return serialNumber; + } + + protected void handleCommandRefresh(final String channelId) { + switch (channelId) { + case DEVICE_CHANNEL_STATUS: + refreshStatus(lastKnownDeviceState); + break; + case DEVICE_CHANNEL_PRODUCING: + refreshProducing(lastKnownDeviceState); + break; + case DEVICE_CHANNEL_COMMUNICATING: + refreshCommunicating(lastKnownDeviceState); + break; + case DEVICE_CHANNEL_PROVISIONED: + refreshProvisioned(lastKnownDeviceState); + break; + case DEVICE_CHANNEL_OPERATING: + refreshOperating(lastKnownDeviceState); + break; + } + } + + private void refreshStatus(final @Nullable DeviceDTO deviceDTO) { + updateState(DEVICE_CHANNEL_STATUS, deviceDTO == null ? UnDefType.UNDEF + : new StringType(messageTranslator.translate((deviceDTO.getDeviceStatus())))); + } + + private void refreshProducing(final @Nullable DeviceDTO deviceDTO) { + updateState(DEVICE_CHANNEL_PRODUCING, + deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.producing)); + } + + private void refreshCommunicating(final @Nullable DeviceDTO deviceDTO) { + updateState(DEVICE_CHANNEL_COMMUNICATING, + deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.communicating)); + } + + private void refreshProvisioned(final @Nullable DeviceDTO deviceDTO) { + updateState(DEVICE_CHANNEL_PROVISIONED, + deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.provisioned)); + } + + private void refreshOperating(final @Nullable DeviceDTO deviceDTO) { + updateState(DEVICE_CHANNEL_OPERATING, + deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.operating)); + } + + public void refreshDeviceState(final @Nullable DeviceDTO deviceDTO) { + refreshStatus(deviceDTO); + refreshProducing(deviceDTO); + refreshCommunicating(deviceDTO); + refreshProvisioned(deviceDTO); + refreshOperating(deviceDTO); + refreshProperties(deviceDTO); + refreshDeviceStatus(deviceDTO != null); + } + + public void refreshDeviceStatus(final boolean hasData) { + if (isInitialized()) { + if (hasData) { + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + messageTranslator.translate(ERROR_NODATA)); + } + } + } + + private void refreshProperties(@Nullable final DeviceDTO deviceDTO) { + if (deviceDTO != null) { + final Map properties = editProperties(); + + properties.put(DEVICE_PROPERTY_PART_NUMBER, deviceDTO.partNumber); + updateProperties(properties); + } + } + + @Override + public void initialize() { + serialNumber = (String) getConfig().get(EnphaseBindingConstants.CONFIG_SERIAL_NUMBER); + if (!EnphaseBindingConstants.isValidSerial(serialNumber)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial Number is not valid"); + } else { + updateStatus(ThingStatus.UNKNOWN); + } + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java new file mode 100644 index 0000000000000..7e3af0a6e1740 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.handler; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.enphase.internal.MessageTranslator; +import org.openhab.binding.enphase.internal.dto.InverterDTO; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link EnphaseInverterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnphaseInverterHandler extends EnphaseDeviceHandler { + + private @Nullable InverterDTO lastKnownState; + + public EnphaseInverterHandler(final Thing thing, MessageTranslator messageTranslator) { + super(thing, messageTranslator); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + final String channelId = channelUID.getId(); + + switch (channelId) { + case INVERTER_CHANNEL_LAST_REPORT_WATTS: + refreshLastReportWatts(lastKnownState); + break; + case INVERTER_CHANNEL_MAX_REPORT_WATTS: + refreshMaxReportWatts(lastKnownState); + break; + case INVERTER_CHANNEL_LAST_REPORT_DATE: + refreshLastReportDate(lastKnownState); + break; + default: + super.handleCommandRefresh(channelId); + break; + } + } + } + + public void refreshInverterChannels(final @Nullable InverterDTO inverterDTO) { + refreshLastReportWatts(inverterDTO); + refreshMaxReportWatts(inverterDTO); + refreshLastReportDate(inverterDTO); + lastKnownState = inverterDTO; + } + + private void refreshLastReportWatts(final @Nullable InverterDTO inverterDTO) { + updateState(INVERTER_CHANNEL_LAST_REPORT_WATTS, + inverterDTO == null ? UnDefType.UNDEF : new QuantityType<>(inverterDTO.lastReportWatts, Units.WATT)); + } + + private void refreshMaxReportWatts(final @Nullable InverterDTO inverterDTO) { + updateState(INVERTER_CHANNEL_MAX_REPORT_WATTS, + inverterDTO == null ? UnDefType.UNDEF : new QuantityType<>(inverterDTO.maxReportWatts, Units.WATT)); + } + + private void refreshLastReportDate(final @Nullable InverterDTO inverterDTO) { + final State state; + + if (inverterDTO == null) { + state = UnDefType.UNDEF; + } else { + final Instant instant = Instant.ofEpochSecond(inverterDTO.lastReportDate); + final ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); + logger.trace("[{}] Epoch time {}, zonedDateTime: {}", getThing().getUID(), inverterDTO.lastReportDate, + zonedDateTime); + state = new DateTimeType(zonedDateTime); + } + updateState(INVERTER_CHANNEL_LAST_REPORT_DATE, state); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java new file mode 100644 index 0000000000000..aadee72348953 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.handler; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.enphase.internal.MessageTranslator; +import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; + +/** + * The {@link EnphaseInverterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnphaseRelayHandler extends EnphaseDeviceHandler { + + public EnphaseRelayHandler(final Thing thing, MessageTranslator messageTranslator) { + super(thing, messageTranslator); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + final String channelId = channelUID.getId(); + + switch (channelId) { + case RELAY_CHANNEL_RELAY: + refreshRelayChannel(lastKnownDeviceState); + break; + case RELAY_CHANNEL_LINE_1_CONNECTED: + refreshLine1Connect(lastKnownDeviceState); + break; + case RELAY_CHANNEL_LINE_2_CONNECTED: + refreshLine2Connect(lastKnownDeviceState); + break; + case RELAY_CHANNEL_LINE_3_CONNECTED: + refreshLine3Connect(lastKnownDeviceState); + break; + default: + super.handleCommandRefresh(channelId); + break; + } + } + } + + private void refreshRelayChannel(@Nullable final DeviceDTO deviceDTO) { + updateState(RELAY_CHANNEL_RELAY, deviceDTO == null ? UnDefType.UNDEF + : (RELAY_STATUS_CLOSED.equals(deviceDTO.relay) ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + private void refreshLine1Connect(@Nullable final DeviceDTO deviceDTO) { + updateState(RELAY_CHANNEL_LINE_1_CONNECTED, deviceDTO == null ? UnDefType.UNDEF + : (deviceDTO.line1Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + private void refreshLine2Connect(@Nullable final DeviceDTO deviceDTO) { + updateState(RELAY_CHANNEL_LINE_2_CONNECTED, deviceDTO == null ? UnDefType.UNDEF + : (deviceDTO.line2Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + private void refreshLine3Connect(@Nullable final DeviceDTO deviceDTO) { + updateState(RELAY_CHANNEL_LINE_3_CONNECTED, deviceDTO == null ? UnDefType.UNDEF + : (deviceDTO.line3Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + @Override + public void refreshDeviceState(@Nullable final DeviceDTO deviceDTO) { + refreshRelayChannel(deviceDTO); + refreshLine1Connect(deviceDTO); + refreshLine2Connect(deviceDTO); + refreshLine3Connect(deviceDTO); + super.refreshDeviceState(deviceDTO); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java new file mode 100644 index 0000000000000..7074857c27b6d --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java @@ -0,0 +1,411 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.handler; + +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.CONFIG_HOSTNAME; +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_CHANNELGROUP_CONSUMPTION; +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATTS_NOW; +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_LIFETIME; +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_SEVEN_DAYS; +import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_TODAY; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.enphase.internal.EnphaseBindingConstants; +import org.openhab.binding.enphase.internal.EnvoyConfiguration; +import org.openhab.binding.enphase.internal.EnvoyConnectionException; +import org.openhab.binding.enphase.internal.EnvoyHostAddressCache; +import org.openhab.binding.enphase.internal.EnvoyNoHostnameException; +import org.openhab.binding.enphase.internal.discovery.EnphaseDevicesDiscoveryService; +import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO; +import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO; +import org.openhab.binding.enphase.internal.dto.InverterDTO; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +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.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BridgeHandler for the Envoy gateway. + * + * @author Thomas Hentschel - Initial contribution + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class EnvoyBridgeHandler extends BaseBridgeHandler { + + private enum FeatureStatus { + UNKNOWN, + SUPPORTED, + UNSUPPORTED + } + + private static final long RETRY_RECONNECT_SECONDS = 10; + + private final Logger logger = LoggerFactory.getLogger(EnvoyBridgeHandler.class); + private final EnvoyConnector connector; + private final EnvoyHostAddressCache envoyHostnameCache; + + private EnvoyConfiguration configuration = new EnvoyConfiguration(); + private @Nullable ScheduledFuture updataDataFuture; + private @Nullable ScheduledFuture updateHostnameFuture; + private @Nullable ExpiringCache> invertersCache; + private @Nullable ExpiringCache> devicesCache; + private @Nullable EnvoyEnergyDTO productionDTO; + private @Nullable EnvoyEnergyDTO consumptionDTO; + private FeatureStatus consumptionSupported = FeatureStatus.UNKNOWN; + private FeatureStatus jsonSupported = FeatureStatus.UNKNOWN; + + public EnvoyBridgeHandler(final Bridge thing, final HttpClient httpClient, + final EnvoyHostAddressCache envoyHostAddressCache) { + super(thing); + connector = new EnvoyConnector(httpClient); + this.envoyHostnameCache = envoyHostAddressCache; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + refresh(channelUID); + } + } + + private void refresh(final ChannelUID channelUID) { + final EnvoyEnergyDTO data = ENVOY_CHANNELGROUP_CONSUMPTION.equals(channelUID.getGroupId()) ? consumptionDTO + : productionDTO; + + if (data == null) { + updateState(channelUID, UnDefType.UNDEF); + } else { + switch (channelUID.getIdWithoutGroup()) { + case ENVOY_WATT_HOURS_TODAY: + updateState(channelUID, new QuantityType<>(data.wattHoursToday, Units.WATT_HOUR)); + break; + case ENVOY_WATT_HOURS_SEVEN_DAYS: + updateState(channelUID, new QuantityType<>(data.wattHoursSevenDays, Units.WATT_HOUR)); + break; + case ENVOY_WATT_HOURS_LIFETIME: + updateState(channelUID, new QuantityType<>(data.wattHoursLifetime, Units.WATT_HOUR)); + break; + case ENVOY_WATTS_NOW: + updateState(channelUID, new QuantityType<>(data.wattsNow, Units.WATT)); + break; + } + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(EnphaseDevicesDiscoveryService.class); + } + + @Override + public void initialize() { + configuration = getConfigAs(EnvoyConfiguration.class); + if (!EnphaseBindingConstants.isValidSerial(configuration.serialNumber)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial number is not valid"); + return; + } + updateStatus(ThingStatus.UNKNOWN); + connector.setConfiguration(configuration); + consumptionSupported = FeatureStatus.UNKNOWN; + jsonSupported = FeatureStatus.UNKNOWN; + invertersCache = new ExpiringCache<>(Duration.of(configuration.refresh, ChronoUnit.MINUTES), + this::refreshInverters); + devicesCache = new ExpiringCache<>(Duration.of(configuration.refresh, ChronoUnit.MINUTES), + this::refreshDevices); + updataDataFuture = scheduler.scheduleWithFixedDelay(this::updateData, 0, configuration.refresh, + TimeUnit.MINUTES); + } + + /** + * Method called by the ExpiringCache when no inverter data is present to get the data from the Envoy gateway. + * When there are connection problems it will start a scheduled job to try to reconnect to the + * + * @return the inverter data from the Envoy gateway or null if no data is available. + */ + private @Nullable Map refreshInverters() { + try { + return connector.getInverters().stream() + .collect(Collectors.toMap(InverterDTO::getSerialNumber, Function.identity())); + } catch (final EnvoyNoHostnameException e) { + // ignore hostname exception here. It's already handled by others. + } catch (final EnvoyConnectionException e) { + logger.trace("refreshInverters connection problem", e); + } + return null; + } + + private @Nullable Map refreshDevices() { + try { + if (jsonSupported != FeatureStatus.UNSUPPORTED) { + final Map devicesData = connector.getInventoryJson().stream() + .flatMap(inv -> Stream.of(inv.devices).map(d -> { + d.type = inv.type; + return d; + })).collect(Collectors.toMap(DeviceDTO::getSerialNumber, Function.identity())); + + jsonSupported = FeatureStatus.SUPPORTED; + return devicesData; + } + } catch (final EnvoyNoHostnameException e) { + // ignore hostname exception here. It's already handled by others. + } catch (final EnvoyConnectionException e) { + if (jsonSupported == FeatureStatus.UNKNOWN) { + logger.info( + "This Ephase Envoy device ({}) doesn't seem to support json data. So not all channels are set.", + getThing().getUID()); + jsonSupported = FeatureStatus.UNSUPPORTED; + } else if (consumptionSupported == FeatureStatus.SUPPORTED) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + return null; + } + + /** + * Returns the data for the inverters. It get the data from cache or updates the cache if possible in case no data + * is available. + * + * @param force force a cache refresh + * @return data if present or null + */ + public @Nullable Map getInvertersData(final boolean force) { + final ExpiringCache> invertersCache = this.invertersCache; + + if (invertersCache == null || !isOnline()) { + return null; + } else { + if (force) { + invertersCache.invalidateValue(); + } + return invertersCache.getValue(); + } + } + + /** + * Returns the data for the devices. It get the data from cache or updates the cache if possible in case no data + * is available. + * + * @param force force a cache refresh + * @return data if present or null + */ + public @Nullable Map getDevices(final boolean force) { + final ExpiringCache> devicesCache = this.devicesCache; + + if (devicesCache == null || !isOnline()) { + return null; + } else { + if (force) { + devicesCache.invalidateValue(); + } + return devicesCache.getValue(); + } + } + + /** + * Method called by the refresh thread. + */ + public synchronized void updateData() { + try { + updateInverters(); + updateEnvoy(); + updateDevices(); + } catch (final EnvoyNoHostnameException e) { + scheduleHostnameUpdate(false); + } catch (final EnvoyConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + scheduleHostnameUpdate(false); + } catch (final RuntimeException e) { + logger.debug("Unexpected error in Enphase {}: ", getThing().getUID(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void updateEnvoy() throws EnvoyNoHostnameException, EnvoyConnectionException { + productionDTO = connector.getProduction(); + setConsumptionDTOData(); + getThing().getChannels().stream().map(Channel::getUID).filter(this::isLinked).forEach(this::refresh); + if (isInitialized() && !isOnline()) { + updateStatus(ThingStatus.ONLINE); + } + } + + /** + * Retrieve consumption data if supported, and keep track if this feature is supported by the device. + * + * @throws EnvoyConnectionException + */ + private void setConsumptionDTOData() throws EnvoyConnectionException { + if (consumptionSupported != FeatureStatus.UNSUPPORTED && isOnline()) { + try { + consumptionDTO = connector.getConsumption(); + consumptionSupported = FeatureStatus.SUPPORTED; + } catch (final EnvoyNoHostnameException e) { + // ignore hostname exception here. It's already handled by others. + } catch (final EnvoyConnectionException e) { + if (consumptionSupported == FeatureStatus.UNKNOWN) { + logger.info( + "This Enphase Envoy device ({}) doesn't seem to support consumption data. So no consumption channels are set.", + getThing().getUID()); + consumptionSupported = FeatureStatus.UNSUPPORTED; + } else if (consumptionSupported == FeatureStatus.SUPPORTED) { + throw e; + } + } + } + } + + /** + * Updates channels of the inverter things with inverter specific data. + */ + private void updateInverters() { + final Map inverters = getInvertersData(false); + + if (inverters != null) { + getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseInverterHandler) + .map(EnphaseInverterHandler.class::cast) + .forEach(invHandler -> updateInverter(inverters, invHandler)); + } + } + + private void updateInverter(final @Nullable Map inverters, + final EnphaseInverterHandler invHandler) { + if (inverters == null) { + return; + } + final InverterDTO inverterDTO = inverters.get(invHandler.getSerialNumber()); + + invHandler.refreshInverterChannels(inverterDTO); + if (jsonSupported == FeatureStatus.UNSUPPORTED) { + // if inventory json is supported device status is set in #updateDevices + invHandler.refreshDeviceStatus(inverterDTO != null); + } + } + + /** + * Updates channels of the device things with device specific data. + * This data is not available on all envoy devices. + */ + private void updateDevices() { + final Map devices = getDevices(false); + + getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseDeviceHandler) + .map(EnphaseDeviceHandler.class::cast).forEach(invHandler -> invHandler + .refreshDeviceState(devices == null ? null : devices.get(invHandler.getSerialNumber()))); + } + + /** + * Schedules a hostname update, but only schedules the task when not yet running or forced. + * Force is used to reschedule the task and should only be used from within {@link #updateHostname()}. + * + * @param force if true will always schedule the task + */ + private synchronized void scheduleHostnameUpdate(final boolean force) { + if (force || updateHostnameFuture == null) { + logger.debug("Schedule hostname/ip address update for thing {} in {} seconds.", getThing().getUID(), + RETRY_RECONNECT_SECONDS); + updateHostnameFuture = scheduler.schedule(this::updateHostname, RETRY_RECONNECT_SECONDS, TimeUnit.SECONDS); + } + } + + @Override + public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) { + if (childHandler instanceof EnphaseInverterHandler) { + updateInverter(getInvertersData(false), (EnphaseInverterHandler) childHandler); + } + if (childHandler instanceof EnphaseDeviceHandler) { + final Map devices = getDevices(false); + + if (devices != null) { + ((EnphaseDeviceHandler) childHandler) + .refreshDeviceState(devices.get(((EnphaseDeviceHandler) childHandler).getSerialNumber())); + } + } + } + + /** + * Handles a host name / ip address update. + */ + private void updateHostname() { + final String lastKnownHostname = envoyHostnameCache.getLastKnownHostAddress(configuration.serialNumber); + + if (lastKnownHostname.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "No ip address known of the envoy gateway. If this isn't updated in a few minutes check your connection."); + scheduleHostnameUpdate(true); + } else { + final Configuration config = editConfiguration(); + + config.put(CONFIG_HOSTNAME, lastKnownHostname); + logger.info("Enphase Envoy ({}) hostname/ip address set to {}", getThing().getUID(), lastKnownHostname); + configuration.hostname = lastKnownHostname; + connector.setConfiguration(configuration); + updateConfiguration(config); + updateData(); + // The task is done so the future can be released by setting it to null. + updateHostnameFuture = null; + } + } + + @Override + public void dispose() { + final ScheduledFuture retryFuture = this.updateHostnameFuture; + if (retryFuture != null) { + retryFuture.cancel(true); + } + final ScheduledFuture inverterFuture = this.updataDataFuture; + + if (inverterFuture != null) { + inverterFuture.cancel(true); + } + } + + /** + * @return Returns true if the bridge is online and not has an configuration pending. + */ + public boolean isOnline() { + return getThing().getStatus() == ThingStatus.ONLINE; + } + + @Override + public String toString() { + return "EnvoyBridgeHandler(" + thing.getUID() + ") Status: " + thing.getStatus(); + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java new file mode 100644 index 0000000000000..d604b3fc55b07 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2010-2021 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.enphase.internal.handler; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.Authentication.Result; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.DigestAuthentication; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.enphase.internal.EnphaseBindingConstants; +import org.openhab.binding.enphase.internal.EnvoyConfiguration; +import org.openhab.binding.enphase.internal.EnvoyConnectionException; +import org.openhab.binding.enphase.internal.EnvoyNoHostnameException; +import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO; +import org.openhab.binding.enphase.internal.dto.EnvoyErrorDTO; +import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO; +import org.openhab.binding.enphase.internal.dto.InverterDTO; +import org.openhab.binding.enphase.internal.dto.ProductionJsonDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * Methods to make API calls to the Envoy gateway. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +class EnvoyConnector { + + private static final String HTTP = "http://"; + private static final String PRODUCTION_JSON_URL = "/production.json"; + private static final String INVENTORY_JSON_URL = "/inventory.json"; + private static final String PRODUCTION_URL = "/api/v1/production"; + private static final String CONSUMPTION_URL = "/api/v1/consumption"; + private static final String INVERTERS_URL = PRODUCTION_URL + "/inverters"; + private static final long CONNECT_TIMEOUT_SECONDS = 5; + + private final Logger logger = LoggerFactory.getLogger(EnvoyConnector.class); + private final Gson gson = new GsonBuilder().create(); + private final HttpClient httpClient; + private String hostname = ""; + private @Nullable DigestAuthentication envoyAuthn; + private @Nullable URI invertersURI; + + public EnvoyConnector(final HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Sets the Envoy connection configuration. + * + * @param configuration the configuration to set + */ + public void setConfiguration(final EnvoyConfiguration configuration) { + hostname = configuration.hostname; + if (hostname.isEmpty()) { + return; + } + final String password = configuration.password.isEmpty() + ? EnphaseBindingConstants.defaultPassword(configuration.serialNumber) + : configuration.password; + final String username = configuration.username.isEmpty() ? EnvoyConfiguration.DEFAULT_USERNAME + : configuration.username; + final AuthenticationStore store = httpClient.getAuthenticationStore(); + + if (envoyAuthn != null) { + store.removeAuthentication(envoyAuthn); + } + invertersURI = URI.create(HTTP + hostname + INVERTERS_URL); + envoyAuthn = new DigestAuthentication(invertersURI, Authentication.ANY_REALM, username, password); + store.addAuthentication(envoyAuthn); + } + + /** + * @return Returns the production data from the Envoy gateway. + */ + public EnvoyEnergyDTO getProduction() throws EnvoyConnectionException, EnvoyNoHostnameException { + return retrieveData(PRODUCTION_URL, this::jsonToEnvoyEnergyDTO); + } + + /** + * @return Returns the consumption data from the Envoy gateway. + */ + public EnvoyEnergyDTO getConsumption() throws EnvoyConnectionException, EnvoyNoHostnameException { + return retrieveData(CONSUMPTION_URL, this::jsonToEnvoyEnergyDTO); + } + + private @Nullable EnvoyEnergyDTO jsonToEnvoyEnergyDTO(final String json) { + return gson.fromJson(json, EnvoyEnergyDTO.class); + } + + /** + * @return Returns the production/consumption data from the Envoy gateway. + */ + public ProductionJsonDTO getProductionJson() throws EnvoyConnectionException, EnvoyNoHostnameException { + return retrieveData(PRODUCTION_JSON_URL, json -> gson.fromJson(json, ProductionJsonDTO.class)); + } + + /** + * @return Returns the inventory data from the Envoy gateway. + */ + public List getInventoryJson() throws EnvoyConnectionException, EnvoyNoHostnameException { + return retrieveData(INVENTORY_JSON_URL, this::jsonToEnvoyInventoryJson); + } + + private @Nullable List jsonToEnvoyInventoryJson(final String json) { + final InventoryJsonDTO @Nullable [] list = gson.fromJson(json, InventoryJsonDTO[].class); + + return list == null ? null : Arrays.asList(list); + } + + /** + * @return Returns the production data for the inverters. + */ + public List getInverters() throws EnvoyConnectionException, EnvoyNoHostnameException { + synchronized (this) { + final AuthenticationStore store = httpClient.getAuthenticationStore(); + final Result invertersResult = store.findAuthenticationResult(invertersURI); + + if (invertersResult != null) { + store.removeAuthenticationResult(invertersResult); + } + } + return retrieveData(INVERTERS_URL, json -> Arrays.asList(gson.fromJson(json, InverterDTO[].class))); + } + + private synchronized T retrieveData(final String urlPath, final Function jsonConverter) + throws EnvoyConnectionException, EnvoyNoHostnameException { + try { + if (hostname.isEmpty()) { + throw new EnvoyNoHostnameException("No host name/ip address known (yet)"); + } + final URI uri = URI.create(HTTP + hostname + urlPath); + logger.trace("Retrieving data from '{}'", uri); + final Request request = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(CONNECT_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + final ContentResponse response = request.send(); + final String content = response.getContentAsString(); + + logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content); + try { + if (response.getStatus() == HttpStatus.OK_200) { + final T result = jsonConverter.apply(content); + if (result == null) { + throw new EnvoyConnectionException("No data received"); + } + return result; + } else { + final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class); + + logger.debug("Envoy returned an error: {}", error); + throw new EnvoyConnectionException(error == null ? response.getReason() : error.info); + } + } catch (final JsonSyntaxException e) { + logger.debug("Error parsing json: {}", content, e); + throw new EnvoyConnectionException("Error parsing data: ", e); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new EnvoyConnectionException("Interrupted"); + } catch (final TimeoutException e) { + logger.debug("TimeoutException: {}", e.getMessage()); + throw new EnvoyConnectionException("Connection timeout: ", e); + } catch (final ExecutionException e) { + logger.debug("ExecutionException: {}", e.getMessage(), e); + throw new EnvoyConnectionException("Could not retrieve data: ", e.getCause()); + } + } +} diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..ca4b31911c7ae --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Enphase Envoy Binding + This is the binding for Enphase Envoy solar panels. + + diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties new file mode 100644 index 0000000000000..db42ff05d51e0 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties @@ -0,0 +1,80 @@ +error.nodata=No Data +envoy.global.ok=Normal + +envoy.cond_flags.acb_ctrl.bmuhardwareerror=BMU Hardware Error +envoy.cond_flags.acb_ctrl.bmuimageerror=BMU Image Error +envoy.cond_flags.acb_ctrl.bmumaxcurrentwarning=BMU Max Current Warning +envoy.cond_flags.acb_ctrl.bmusenseerror=BMU Sense Error + +envoy.cond_flags.acb_ctrl.cellmaxtemperror=Cell Max Temperature Error +envoy.cond_flags.acb_ctrl.cellmaxtempwarning=Cell Max Temperature Warning +envoy.cond_flags.acb_ctrl.cellmaxvoltageerror=Cell Max Voltage Error +envoy.cond_flags.acb_ctrl.cellmaxvoltagewarning=Cell Max Voltage Warning +envoy.cond_flags.acb_ctrl.cellmintemperror=Cell Min Temperature Error +envoy.cond_flags.acb_ctrl.cellmintempwarning=Cell Min Temperature Warning +envoy.cond_flags.acb_ctrl.cellminvoltageerror=Cell Min Voltage Error +envoy.cond_flags.acb_ctrl.cellminvoltagewarning=Cell Min Voltage Warning +envoy.cond_flags.acb_ctrl.cibcanerror=CIB CAN Error +envoy.cond_flags.acb_ctrl.cibimageerror=CIB Image Error +envoy.cond_flags.acb_ctrl.cibspierror=CIB SPI Error" +envoy.cond_flags.obs_strs.discovering=Discovering +envoy.cond_flags.obs_strs.failure=Failure to report +envoy.cond_flags.obs_strs.flasherror=Flash Error +envoy.cond_flags.obs_strs.notmonitored=Not Monitored +envoy.cond_flags.obs_strs.ok=Normal +envoy.cond_flags.obs_strs.plmerror=PLM Error +envoy.cond_flags.obs_strs.secmodeenterfailure=Secure mode enter failure +envoy.cond_flags.obs_strs.secmodeexitfailure=Secure mode exit failure +envoy.cond_flags.obs_strs.sleeping=Sleeping" + +envoy.cond_flags.pcu_chan.acMonitorError=AC Monitor Error +envoy.cond_flags.pcu_chan.acfrequencyhigh=AC Frequency High +envoy.cond_flags.pcu_chan.acfrequencylow=AC Frequency Low +envoy.cond_flags.pcu_chan.acfrequencyoor=AC Frequency Out Of Range +envoy.cond_flags.pcu_chan.acvoltage_avg_hi=AC Voltage Average High +envoy.cond_flags.pcu_chan.acvoltagehigh=AC Voltage High +envoy.cond_flags.pcu_chan.acvoltagelow=AC Voltage Low +envoy.cond_flags.pcu_chan.acvoltageoor=AC Voltage Out Of Range +envoy.cond_flags.pcu_chan.acvoltageoosp1=AC Voltage Out Of Range - Phase 1 +envoy.cond_flags.pcu_chan.acvoltageoosp2=AC Voltage Out Of Range - Phase 2 +envoy.cond_flags.pcu_chan.acvoltageoosp3=AC Voltage Out Of Range - Phase 3 +envoy.cond_flags.pcu_chan.agfpowerlimiting=AGF Power Limiting +envoy.cond_flags.pcu_chan.dcresistancelow=DC Resistance Low +envoy.cond_flags.pcu_chan.dcresistancelowpoweroff=DC Resistance Low - Power Off +envoy.cond_flags.pcu_chan.dcvoltagetoohigh=DC Voltage Too High +envoy.cond_flags.pcu_chan.dcvoltagetoolow=DC Voltage Too Low +envoy.cond_flags.pcu_chan.dfdt=AC Frequency Changing too Fast +envoy.cond_flags.pcu_chan.gfitripped=GFI Tripped +envoy.cond_flags.pcu_chan.gridgone=Grid Gone +envoy.cond_flags.pcu_chan.gridinstability=Grid Instability +envoy.cond_flags.pcu_chan.gridoffsethi=Grid Offset Hi +envoy.cond_flags.pcu_chan.gridoffsetlow=Grid Offset Low +envoy.cond_flags.pcu_chan.hardwareError=Hardware Error +envoy.cond_flags.pcu_chan.hardwareWarning=Hardware Warning +envoy.cond_flags.pcu_chan.highskiprate=High Skip Rate +envoy.cond_flags.pcu_chan.invalidinterval=Invalid Interval +envoy.cond_flags.pcu_chan.pwrgenoffbycmd=Power generation off by command +envoy.cond_flags.pcu_chan.skippedcycles=Skipped Cycles +envoy.cond_flags.pcu_chan.vreferror=Voltage Ref Error" + +envoy.cond_flags.pcu_ctrl.alertactive=Alert Active +envoy.cond_flags.pcu_ctrl.altpwrgenmode=Alternate Power Generation Mode +envoy.cond_flags.pcu_ctrl.altvfsettings=Alternate Voltage and Frequency Settings +envoy.cond_flags.pcu_ctrl.badflashimage=Bad Flash Image +envoy.cond_flags.pcu_ctrl.bricked=No Grid Profile +envoy.cond_flags.pcu_ctrl.commandedreset=Commanded Reset +envoy.cond_flags.pcu_ctrl.criticaltemperature=Critical Temperature +envoy.cond_flags.pcu_ctrl.dc-pwr-low=DC Power Too Low +envoy.cond_flags.pcu_ctrl.iuplinkproblem=IUP Link Problem +envoy.cond_flags.pcu_ctrl.manutestmode=In Manu Test Mode +envoy.cond_flags.pcu_ctrl.nsync=Grid Perturbation Unsynchronized +envoy.cond_flags.pcu_ctrl.overtemperature=Over Temperature +envoy.cond_flags.pcu_ctrl.poweronreset=Power On Reset +envoy.cond_flags.pcu_ctrl.pwrgenoffbycmd=Power generation off by command +envoy.cond_flags.pcu_ctrl.runningonac=Running on AC +envoy.cond_flags.pcu_ctrl.tpmtest=Transient Grid Profile +envoy.cond_flags.pcu_ctrl.unexpectedreset=Unexpected Reset +envoy.cond_flags.pcu_ctrl.watchdogreset=Watchdog Reset + +envoy.cond_flags.rgm_chan.check_meter=Meter Error +envoy.cond_flags.rgm_chan.power_quality=Poor Power Quality diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..21d1a36fed798 --- /dev/null +++ b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,228 @@ + + + + + + + Envoy gateway + + + + + Production data from the solar panels + + + + Consumption data from the solar panels + + + + serialNumber + + + + + The serial number of the Envoy gateway which can be found on the gateway + + + + The host name/ip address of the Envoy gateway. Leave empty to auto detect + true + + + + The user name to the Envoy gateway. Leave empty when using the default user name + envoy + true + + + password + + The password to the Envoy gateway. Leave empty when using the default password + true + + + + Period between updates. The default is 5 minutes, the refresh frequency of the Envoy itself + 5 + true + + + + + + + + + + + + Inverter + + + + + + + + + + + + + + + + + serialNumber + + + + + The serial number of the inverter + + + + + + + + + + + Network system relay controller + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + The serial number of the inverter + + + + + + + + + + + + + + + + + Number:Energy + + Watt hours produced today + + + + Number:Energy + + Watt hours produced the last 7 days + + + + Number:Energy + + Watt hours produced over the lifetime + + + + Number:Power + + Latest watts produced + + + + + + Number:Power + + Last reported power delivery + + + + Number:Power + + Maximum reported power + + + + DateTime + + Date of last reported power delivery + + + + + + Contact + + + + + Contact + + When closed power line is connected + + + + + + String + + The status of the Enphase device + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + diff --git a/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/EnturNoHandler.java b/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/EnturNoHandler.java index 15ff8fbd77a81..2a701eaba3597 100644 --- a/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/EnturNoHandler.java +++ b/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/EnturNoHandler.java @@ -20,7 +20,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -95,14 +94,15 @@ public void initialize() { logger.debug("Stop place id: {}", stopId); boolean configValid = true; - if (StringUtils.trimToNull(stopId) == null) { + if (stopId == null || stopId.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.conf-error-missing-stopId"); configValid = false; } - logger.debug("Line code: {}", config.getLineCode()); - if (StringUtils.trimToNull(config.getLineCode()) == null) { + String lineCode = config.getLineCode(); + logger.debug("Line code: {}", lineCode); + if (lineCode == null || lineCode.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.conf-error-missing-lineCode"); configValid = false; diff --git a/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/connection/EnturNoConnection.java b/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/connection/EnturNoConnection.java index 6f852ac302221..70ecd71b23cda 100644 --- a/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/connection/EnturNoConnection.java +++ b/bundles/org.openhab.binding.enturno/src/main/java/org/openhab/binding/enturno/internal/connection/EnturNoConnection.java @@ -32,7 +32,7 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -77,7 +77,6 @@ public class EnturNoConnection { private final EnturNoHandler handler; private final HttpClient httpClient; - private final JsonParser parser = new JsonParser(); private final Gson gson = new Gson(); public EnturNoConnection(EnturNoHandler handler, HttpClient httpClient) { @@ -96,9 +95,9 @@ public EnturNoConnection(EnturNoHandler handler, HttpClient httpClient) { */ public synchronized List getEnturTimeTable(@Nullable String stopPlaceId, @Nullable String lineCode) throws JsonSyntaxException, EnturConfigurationException, EnturCommunicationException { - if (StringUtils.isBlank(stopPlaceId)) { + if (stopPlaceId == null || stopPlaceId.isBlank()) { throw new EnturConfigurationException("Stop place id cannot be empty or null"); - } else if (lineCode == null || StringUtils.isBlank(lineCode)) { + } else if (lineCode == null || lineCode.isBlank()) { throw new EnturConfigurationException("Line code cannot be empty or null"); } @@ -115,8 +114,9 @@ public synchronized List getEnturTimeTable(@Nullable String stopPla private Map getRequestParams(EnturNoConfiguration config) { Map params = new HashMap<>(); - params.put(PARAM_STOPID, StringUtils.trimToEmpty(config.getStopPlaceId())); - params.put(PARAM_START_DATE_TIME, StringUtils.trimToEmpty(LocalDateTime.now(ZoneId.of(TIME_ZONE)).toString())); + String stopPlaceId = config.getStopPlaceId(); + params.put(PARAM_STOPID, stopPlaceId == null ? "" : stopPlaceId.trim()); + params.put(PARAM_START_DATE_TIME, LocalDateTime.now(ZoneId.of(TIME_ZONE)).toString()); return params; } @@ -141,7 +141,7 @@ private String getResponse(String url, Map params) { int httpStatus = contentResponse.getStatus(); String content = contentResponse.getContentAsString(); - String errorMessage = StringUtils.EMPTY; + String errorMessage = ""; logger.trace("Entur response: status = {}, content = '{}'", httpStatus, content); switch (httpStatus) { case OK_200: @@ -160,14 +160,18 @@ private String getResponse(String url, Map params) { String errorMessage = e.getLocalizedMessage(); logger.debug("Exception occurred during execution: {}", errorMessage, e); throw new EnturCommunicationException(errorMessage, e); - } catch (InterruptedException | TimeoutException | IOException e) { + } catch (TimeoutException | IOException e) { logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e); throw new EnturCommunicationException(e.getLocalizedMessage(), e); + } catch (InterruptedException e) { + logger.debug("Execution interrupted: {}", e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + throw new EnturCommunicationException(e.getLocalizedMessage(), e); } } private String getErrorMessage(String response) { - JsonObject jsonResponse = parser.parse(response).getAsJsonObject(); + JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject(); if (jsonResponse.has(PROPERTY_MESSAGE)) { return jsonResponse.get(PROPERTY_MESSAGE).getAsString(); } diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java index d9029b21de245..2d8cc2ca41497 100644 --- a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java +++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java @@ -156,10 +156,13 @@ private void updateChannelState(Channel channel) { State state = queryDataFromDevice(epsonCommand); if (state != null) { - updateStatus(ThingStatus.ONLINE); if (isLinked(channel.getUID())) { updateState(channel.getUID(), state); } + // the first valid response will cause the thing to go ONLINE + if (state != UnDefType.UNDEF) { + updateStatus(ThingStatus.ONLINE); + } } } catch (IllegalArgumentException e) { logger.warn("Unknown channel {}", channel.getUID().getId()); diff --git a/bundles/org.openhab.binding.etherrain/src/main/java/org/openhab/binding/etherrain/internal/api/EtherRainCommunication.java b/bundles/org.openhab.binding.etherrain/src/main/java/org/openhab/binding/etherrain/internal/api/EtherRainCommunication.java index 67589fa9af11b..a10417d93395b 100644 --- a/bundles/org.openhab.binding.etherrain/src/main/java/org/openhab/binding/etherrain/internal/api/EtherRainCommunication.java +++ b/bundles/org.openhab.binding.etherrain/src/main/java/org/openhab/binding/etherrain/internal/api/EtherRainCommunication.java @@ -191,9 +191,13 @@ private synchronized List sendGet(String command) throws IOException { logger.warn("Etherrain return status other than HTTP_OK : {}", response.getStatus()); return Collections.emptyList(); } - } catch (InterruptedException | TimeoutException | ExecutionException e) { + } catch (TimeoutException | ExecutionException e) { logger.warn("Could not connect to Etherrain with exception: {}", e.getMessage()); return Collections.emptyList(); + } catch (InterruptedException e) { + logger.warn("Connect to Etherrain interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); + return Collections.emptyList(); } return new BufferedReader(new StringReader(response.getContentAsString())).lines().collect(Collectors.toList()); diff --git a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/ApiAccess.java b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/ApiAccess.java index a53d0d963f89a..2d49c2bddef68 100644 --- a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/ApiAccess.java +++ b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/ApiAccess.java @@ -119,8 +119,11 @@ public TOut doRequest(HttpMethod method, String url, Map retVal = new Gson().fromJson(reply, outClass); } } - } catch (InterruptedException | ExecutionException e) { + } catch (ExecutionException e) { logger.debug("Error in handling request: ", e); + } catch (InterruptedException e) { + logger.debug("Handling request interrupted: ", e); + Thread.currentThread().interrupt(); } return retVal; diff --git a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/EvohomeApiClient.java b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/EvohomeApiClient.java index 48939b1819d04..af9caf58b78d9 100644 --- a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/EvohomeApiClient.java +++ b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/api/EvohomeApiClient.java @@ -48,7 +48,6 @@ public class EvohomeApiClient { private static final String CLIENT_SECRET = "1a15cdb8-42de-407b-add0-059f92c530cb"; private final Logger logger = LoggerFactory.getLogger(EvohomeApiClient.class); - private final HttpClient httpClient; private final EvohomeAccountConfiguration configuration; private final ApiAccess apiAccess; @@ -60,19 +59,9 @@ public class EvohomeApiClient { * Creates a new API client based on the V2 API interface * * @param configuration The configuration of the account to use - * @throws Exception */ - public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) throws Exception { + public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) { this.configuration = configuration; - this.httpClient = httpClient; - - try { - httpClient.start(); - } catch (Exception e) { - logger.error("Could not start http client", e); - throw new EvohomeApiClientException("Could not start http client", e); - } - apiAccess = new ApiAccess(httpClient); apiAccess.setApplicationId(APPLICATION_ID); } @@ -85,14 +74,6 @@ public void close() { useraccount = null; locations = null; locationsStatus = null; - - if (httpClient.isStarted()) { - try { - httpClient.stop(); - } catch (Exception e) { - logger.debug("Could not stop http client.", e); - } - } } public boolean login() { diff --git a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/BaseEvohomeHandler.java b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/BaseEvohomeHandler.java index e3255b4bac225..f959c01d88f7e 100644 --- a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/BaseEvohomeHandler.java +++ b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/BaseEvohomeHandler.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.evohome.internal.handler; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.evohome.internal.api.models.v2.response.Locations; import org.openhab.binding.evohome.internal.configuration.EvohomeThingConfiguration; import org.openhab.core.thing.Bridge; @@ -132,7 +131,7 @@ private void checkConfig() { if (configuration == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration is missing or corrupted"); - } else if (StringUtils.isEmpty(configuration.id)) { + } else if (configuration.id == null || configuration.id.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Id not configured"); } } diff --git a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/EvohomeAccountBridgeHandler.java b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/EvohomeAccountBridgeHandler.java index 06e563f776ab4..069e0ca1b4b82 100644 --- a/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/EvohomeAccountBridgeHandler.java +++ b/bundles/org.openhab.binding.evohome/src/main/java/org/openhab/binding/evohome/internal/handler/EvohomeAccountBridgeHandler.java @@ -22,7 +22,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.evohome.internal.RunnableWithTimeout; import org.openhab.binding.evohome.internal.api.EvohomeApiClient; @@ -75,30 +74,22 @@ public void initialize() { configuration = getConfigAs(EvohomeAccountConfiguration.class); if (checkConfig()) { - try { - apiClient = new EvohomeApiClient(configuration, this.httpClient); - } catch (Exception e) { - logger.error("Could not start API client", e); - updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not create evohome API client"); - } + apiClient = new EvohomeApiClient(configuration, this.httpClient); - if (apiClient != null) { - // Initialization can take a while, so kick it off on a separate thread - scheduler.schedule(() -> { - if (apiClient.login()) { - if (checkInstallationInfoHasDuplicateIds(apiClient.getInstallationInfo())) { - startRefreshTask(); - } else { - updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "System Information Sanity Check failed"); - } + // Initialization can take a while, so kick it off on a separate thread + scheduler.schedule(() -> { + if (apiClient.login()) { + if (checkInstallationInfoHasDuplicateIds(apiClient.getInstallationInfo())) { + startRefreshTask(); } else { updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Authentication failed"); + "System Information Sanity Check failed"); } - }, 0, TimeUnit.SECONDS); - } + } else { + updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Authentication failed"); + } + }, 0, TimeUnit.SECONDS); } } @@ -190,9 +181,9 @@ private boolean checkConfig() { if (configuration == null) { errorMessage = "Configuration is missing or corrupted"; - } else if (StringUtils.isEmpty(configuration.username)) { + } else if (configuration.username == null || configuration.username.isEmpty()) { errorMessage = "Username not configured"; - } else if (StringUtils.isEmpty(configuration.password)) { + } else if (configuration.password == null || configuration.password.isEmpty()) { errorMessage = "Password not configured"; } else { return true; diff --git a/bundles/org.openhab.binding.exec/src/main/feature/feature.xml b/bundles/org.openhab.binding.exec/src/main/feature/feature.xml index ff73780e211b3..4bc0c8c65475f 100644 --- a/bundles/org.openhab.binding.exec/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.exec/src/main/feature/feature.xml @@ -4,7 +4,7 @@ openhab-runtime-base - mvn:${project.groupId}/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/cfg/exec.whitelist + mvn:org.openhab.addons.features.karaf/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/cfg/exec.whitelist mvn:org.openhab.addons.bundles/org.openhab.binding.exec/${project.version} diff --git a/bundles/org.openhab.binding.exec/src/main/java/org/openhab/binding/exec/internal/handler/ExecHandler.java b/bundles/org.openhab.binding.exec/src/main/java/org/openhab/binding/exec/internal/handler/ExecHandler.java index 33c70ce165df6..10d700467cfd9 100644 --- a/bundles/org.openhab.binding.exec/src/main/java/org/openhab/binding/exec/internal/handler/ExecHandler.java +++ b/bundles/org.openhab.binding.exec/src/main/java/org/openhab/binding/exec/internal/handler/ExecHandler.java @@ -29,7 +29,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.exec.internal.ExecWhitelistWatchService; diff --git a/bundles/org.openhab.binding.feican/src/main/java/org/openhab/binding/feican/internal/Commands.java b/bundles/org.openhab.binding.feican/src/main/java/org/openhab/binding/feican/internal/Commands.java index 49da817f56a51..5284de881c6ce 100644 --- a/bundles/org.openhab.binding.feican/src/main/java/org/openhab/binding/feican/internal/Commands.java +++ b/bundles/org.openhab.binding.feican/src/main/java/org/openhab/binding/feican/internal/Commands.java @@ -13,6 +13,7 @@ package org.openhab.binding.feican.internal; import java.math.BigDecimal; +import java.math.RoundingMode; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.HSBType; @@ -82,7 +83,7 @@ public byte[] color(HSBType color) { */ private byte convertColorPercentToByte(PercentType percent) { return percent.toBigDecimal().multiply(BigDecimal.valueOf(255)) - .divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP).byteValue(); + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).byteValue(); } /** diff --git a/bundles/org.openhab.binding.folderwatcher/NOTICE b/bundles/org.openhab.binding.folderwatcher/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.folderwatcher/README.md b/bundles/org.openhab.binding.folderwatcher/README.md new file mode 100755 index 0000000000000..7f0abadb66358 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/README.md @@ -0,0 +1,82 @@ +# FolderWatcher Binding + +This binding is intended to monitor FTP and local folder and its subfolders and notify of new files + +## Supported Things + +Currently the binding support two types of things: `ftpfolder` and `localfolder`. + + +## Thing Configuration + +The `ftpfolder` thing has the following configuration options: + +| Parameter | Name | Description | Required | Default value | +|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| ftpAddress | FTP server | IP address of FTP server | yes | n/a | +| ftpPort | FTP port | Port of FTP server | yes | 21 | +| secureMode | FTP Security | FTP Security | yes | None | +| ftpUsername | Username | FTP user name | yes | n/a | +| ftpPassword | Password | FTP password | yes | n/a | +| ftpDir | RootDir | Root directory to be watched | yes | n/a | +| listRecursiveFtp | List Sub Folders | Allow listing of sub folders | yes | No | +| listHidden | List Hidden | Allow listing of hidden files | yes | false | +| connectionTimeout | Connection timeout, s | Connection timeout for FTP request | yes | 30 | +| pollInterval | Polling interval, s | Interval for polling folder changes | yes | 60 | +| diffHours | Time stamp difference, h | How many hours back to analyze | yes | 24 | + +The `localfolder` thing has the following configuration options: + +| Parameter | Name | Description | Required | Default value | +|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| localDir | Local Directory | Local directory to be watched | yes | n/a | +| listHiddenLocal | List Hidden | Allow listing of hidden files | yes | No | +| pollIntervalLocal | Polling interval, s | Interval for polling folder changes | yes | 60 | +| listRecursiveLocal | List Sub Folders | Allow listing of sub folders | yes | No | + +## Events + +This binding currently supports the following events: + +| Channel Type ID | Item Type | Description | +|-----------------|--------------|----------------------------------------------------------------------------------------| +| newftpfile | String | A new file name discovered on FTP | +| newlocalfile | String | A new file name discovered on in local folder | + + +## Full Example + +Thing configuration: + +```java +folderwatcher:localfolder:myLocalFolder [ localDir="/myfolder", pollIntervalLocal=60, listHiddenLocal="false", listRecursiveLocal="false" ] +folderwatcher:ftpfolder:myLocalFolder [ ftpAddress="X.X.X.X", ftpPort=21, secureMode="EXPLICIT", ftpUsername="username", ftpPassword="password",ftpDir="/myfolder/",listHidden="true",listRecursiveFtp="true",connectionTimeout=33,pollInterval=66,diffHours=25] +``` + +### Using in a rule: + +FTP example: + +```java +rule "New FTP file" +when + Channel 'folderwatcher:ftpfolder:XXXXX:newfile' triggered +then + + logInfo('NewFTPFile', receivedEvent.toString()) + +end +``` + +Local folder example: + +```java +rule "New Local file" +when + Channel 'folderwatcher:localfolder:XXXXX:newfile' triggered +then + + logInfo('NewLocalFile', receivedEvent.toString()) + +end +``` diff --git a/bundles/org.openhab.binding.folderwatcher/pom.xml b/bundles/org.openhab.binding.folderwatcher/pom.xml new file mode 100644 index 0000000000000..856523c8706f8 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.folderwatcher + + openHAB Add-ons :: Bundles :: FolderWatcher Binding + + + + commons-net + commons-net + 3.7.2 + + + + diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml b/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml new file mode 100644 index 0000000000000..b19d79626f14d --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.folderwatcher/${project.version} + + diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java new file mode 100755 index 0000000000000..174c328ae47c8 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link FolderWatcherBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class FolderWatcherBindingConstants { + private static final String BINDING_ID = "folderwatcher"; + public static final ThingTypeUID THING_TYPE_FTPFOLDER = new ThingTypeUID(BINDING_ID, "ftpfolder"); + public static final ThingTypeUID THING_TYPE_LOCALFOLDER = new ThingTypeUID(BINDING_ID, "localfolder"); + public static final String CHANNEL_NEWFILE = "newfile"; +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java new file mode 100755 index 0000000000000..0fa785c14e6d3 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal; + +import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.folderwatcher.internal.handler.FtpFolderWatcherHandler; +import org.openhab.binding.folderwatcher.internal.handler.LocalFolderWatcherHandler; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link FolderWatcherHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.folderwatcher", service = ThingHandlerFactory.class) +public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_FTPFOLDER, + THING_TYPE_LOCALFOLDER); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_FTPFOLDER.equals(thingTypeUID)) { + return new FtpFolderWatcherHandler(thing); + } else if (THING_TYPE_LOCALFOLDER.equals(thingTypeUID)) { + return new LocalFolderWatcherHandler(thing); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java new file mode 100644 index 0000000000000..b65a78828af25 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link FolderWatcherBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public enum SecureMode { + NONE, + IMPLICIT, + EXPLICIT +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java new file mode 100755 index 0000000000000..239e6dafb13bd --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal.common; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WatcherCommon} class contains commonly used methods. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class WatcherCommon { + + private static void initFile(File file, String watchDir) throws IOException { + try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(file))) { + fileWriter.write(watchDir); + fileWriter.newLine(); + } + } + + public static List initStorage(File file, String watchDir) throws IOException { + List returnList = List.of(); + List currentFileListing = List.of(); + if (!file.exists()) { + Files.createDirectories(file.toPath().getParent()); + initFile(file, watchDir); + } else { + currentFileListing = Files.readAllLines(file.toPath().toAbsolutePath()); + if (currentFileListing.get(0).equals(watchDir)) { + returnList = currentFileListing; + } else { + initFile(file, watchDir); + } + } + return returnList; + } + + public static void saveNewListing(List newList, File listingFile) throws IOException { + try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(listingFile, true))) { + for (String newFile : newList) { + fileWriter.write(newFile); + fileWriter.newLine(); + } + } + } +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java new file mode 100755 index 0000000000000..7a4448ea3b8bb --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.folderwatcher.internal.SecureMode; + +/** + * The {@link FtpFolderWatcherConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class FtpFolderWatcherConfiguration { + public String ftpAddress = ""; + public int ftpPort; + public String ftpUsername = ""; + public String ftpPassword = ""; + public String ftpDir = ""; + public int pollInterval; + public int connectionTimeout; + public boolean listHidden; + public int diffHours; + public boolean listRecursiveFtp; + public SecureMode secureMode = SecureMode.NONE; +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java new file mode 100755 index 0000000000000..18d31831b5e1e --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LocalFolderWatcherConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class LocalFolderWatcherConfiguration { + public String localDir = ""; + public boolean listHiddenLocal; + public int pollIntervalLocal; + public boolean listRecursiveLocal; +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java new file mode 100755 index 0000000000000..c89fced81dae0 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal.handler; + +import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; +import org.apache.commons.net.ftp.FTPReply; +import org.apache.commons.net.ftp.FTPSClient; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.folderwatcher.internal.common.WatcherCommon; +import org.openhab.binding.folderwatcher.internal.config.FtpFolderWatcherConfiguration; +import org.openhab.core.OpenHAB; +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.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FtpFolderWatcherHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class FtpFolderWatcherHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(FtpFolderWatcherHandler.class); + private FtpFolderWatcherConfiguration config = new FtpFolderWatcherConfiguration(); + private @Nullable File currentFtpListingFile; + private @Nullable ScheduledFuture executionJob, initJob; + private FTPClient ftp = new FTPClient(); + private List previousFtpListing = new ArrayList<>(); + + public FtpFolderWatcherHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Channel {} triggered with command {}", channelUID.getId(), command); + if (command instanceof RefreshType) { + refreshFTPFolderInformation(); + } + } + + @Override + public void initialize() { + File currentFtpListingFile; + config = getConfigAs(FtpFolderWatcherConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); + if (config.connectionTimeout <= 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Connection timeout can't be negative"); + return; + } + if (config.ftpPort < 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "FTP port can't be negative"); + return; + } + if (config.pollInterval <= 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Polling interval can't be null or negative"); + } + + currentFtpListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher" + File.separator + + thing.getUID().getAsString().replace(':', '_') + ".data"); + try { + this.currentFtpListingFile = currentFtpListingFile; + previousFtpListing = WatcherCommon.initStorage(currentFtpListingFile, config.ftpAddress + config.ftpDir); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + logger.debug("Can't write file {}, error message {}", currentFtpListingFile, e.getMessage()); + return; + } + this.initJob = scheduler.scheduleWithFixedDelay(this::connectionKeepAlive, 0, config.pollInterval, + TimeUnit.SECONDS); + } + + @Override + public void dispose() { + ScheduledFuture executionJob = this.executionJob; + ScheduledFuture initJob = this.initJob; + if (executionJob != null) { + executionJob.cancel(true); + } + if (initJob != null) { + initJob.cancel(true); + } + if (ftp.isConnected()) { + try { + ftp.logout(); + ftp.disconnect(); + } catch (IOException e) { + logger.debug("Error terminating FTP connection: ", e); + } + } + } + + private void listDirectory(FTPClient ftpClient, String dirPath, boolean recursive, List dirFiles) + throws IOException { + Instant dateNow = Instant.now(); + for (FTPFile file : ftpClient.listFiles(dirPath)) { + String currentFileName = file.getName(); + if (currentFileName.equals(".") || currentFileName.equals("..")) { + continue; + } + String filePath = dirPath + "/" + currentFileName; + if (file.isDirectory()) { + if (recursive) { + try { + listDirectory(ftpClient, filePath, recursive, dirFiles); + } catch (IOException e) { + logger.debug("Can't read FTP directory: {}", filePath, e); + } + } + } else { + long diff = ChronoUnit.HOURS.between(file.getTimestamp().toInstant(), dateNow); + if (diff < config.diffHours) { + dirFiles.add("ftp:/" + ftpClient.getRemoteAddress() + filePath); + } + } + } + } + + private void connectionKeepAlive() { + if (!ftp.isConnected()) { + switch (config.secureMode) { + case NONE: + ftp = new FTPClient(); + break; + case IMPLICIT: + ftp = new FTPSClient(true); + break; + case EXPLICIT: + ftp = new FTPSClient(false); + break; + } + + int reply = 0; + ftp.setListHiddenFiles(config.listHidden); + ftp.setConnectTimeout(config.connectionTimeout * 1000); + + try { + ftp.connect(config.ftpAddress, config.ftpPort); + reply = ftp.getReplyCode(); + + if (!FTPReply.isPositiveCompletion(reply)) { + ftp.disconnect(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "FTP server refused connection."); + return; + } + } catch (IOException e) { + if (ftp.isConnected()) { + try { + ftp.disconnect(); + } catch (IOException e2) { + logger.debug("Error disconneting, lost connection? : {}", e2.getMessage()); + } + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return; + } + try { + if (!ftp.login(config.ftpUsername, config.ftpPassword)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ftp.getReplyString()); + ftp.logout(); + return; + } + updateStatus(ThingStatus.ONLINE); + ScheduledFuture executionJob = this.executionJob; + if (executionJob != null) { + executionJob.cancel(true); + } + this.executionJob = scheduler.scheduleWithFixedDelay(this::refreshFTPFolderInformation, 0, + config.pollInterval, TimeUnit.SECONDS); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + } + + private void refreshFTPFolderInformation() { + String ftpRootDir = config.ftpDir; + final File currentFtpListingFile = this.currentFtpListingFile; + if (ftp.isConnected()) { + ftp.enterLocalPassiveMode(); + try { + if (ftpRootDir.endsWith("/")) { + ftpRootDir = ftpRootDir.substring(0, ftpRootDir.length() - 1); + } + if (!ftpRootDir.startsWith("/")) { + ftpRootDir = "/" + ftpRootDir; + } + List currentFtpListing = new ArrayList<>(); + listDirectory(ftp, ftpRootDir, config.listRecursiveFtp, currentFtpListing); + List diffFtpListing = new ArrayList<>(currentFtpListing); + diffFtpListing.removeAll(previousFtpListing); + diffFtpListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file)); + if (!diffFtpListing.isEmpty() && currentFtpListingFile != null) { + try { + WatcherCommon.saveNewListing(diffFtpListing, currentFtpListingFile); + } catch (IOException e2) { + logger.debug("Can't save new listing into file: {}", e2.getMessage()); + } + } + previousFtpListing = new ArrayList<>(currentFtpListing); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "FTP connection lost. " + e.getMessage()); + try { + ftp.disconnect(); + } catch (IOException e1) { + logger.debug("Error disconneting, lost connection? {}", e1.getMessage()); + } + } + } else { + logger.debug("FTP connection lost."); + } + } +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java new file mode 100755 index 0000000000000..14e55b91d3c47 --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2010-2021 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.folderwatcher.internal.handler; + +import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.folderwatcher.internal.common.WatcherCommon; +import org.openhab.binding.folderwatcher.internal.config.LocalFolderWatcherConfiguration; +import org.openhab.core.OpenHAB; +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.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LocalFolderWatcherHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexandr Salamatov - Initial contribution + */ +@NonNullByDefault +public class LocalFolderWatcherHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(LocalFolderWatcherHandler.class); + private LocalFolderWatcherConfiguration config = new LocalFolderWatcherConfiguration(); + private File currentLocalListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher" + + File.separator + thing.getUID().getAsString().replace(':', '_') + ".data"); + private @Nullable ScheduledFuture executionJob; + private List previousLocalListing = new ArrayList<>(); + + public LocalFolderWatcherHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Channel {} triggered with command {}", channelUID.getId(), command); + if (command instanceof RefreshType) { + refreshFolderInformation(); + } + } + + @Override + public void initialize() { + config = getConfigAs(LocalFolderWatcherConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); + + if (!Files.isDirectory(Paths.get(config.localDir))) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Local directory is not valid"); + return; + } + try { + previousLocalListing = WatcherCommon.initStorage(currentLocalListingFile, config.localDir); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + logger.debug("Can't write file {}: {}", currentLocalListingFile, e.getMessage()); + return; + } + + if (config.pollIntervalLocal > 0) { + updateStatus(ThingStatus.ONLINE); + executionJob = scheduler.scheduleWithFixedDelay(this::refreshFolderInformation, config.pollIntervalLocal, + config.pollIntervalLocal, TimeUnit.SECONDS); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Polling interval can't be null or negative"); + return; + } + } + + @Override + public void dispose() { + ScheduledFuture executionJob = this.executionJob; + if (executionJob != null) { + executionJob.cancel(true); + } + } + + private void refreshFolderInformation() { + final String rootDir = config.localDir; + try { + List currentLocalListing = new ArrayList<>(); + + Files.walkFileTree(Paths.get(rootDir), new FileVisitor<@Nullable Path>() { + @Override + public FileVisitResult preVisitDirectory(@Nullable Path dir, @Nullable BasicFileAttributes attrs) + throws IOException { + if (dir != null) { + if (!dir.equals(Paths.get(rootDir)) && !config.listRecursiveLocal) { + return FileVisitResult.SKIP_SUBTREE; + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(@Nullable Path file, @Nullable BasicFileAttributes attrs) + throws IOException { + if (file != null) { + if (Files.isHidden(file) && !config.listHiddenLocal) { + return FileVisitResult.CONTINUE; + } + currentLocalListing.add(file.toAbsolutePath().toString()); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(@Nullable Path file, @Nullable IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(@Nullable Path dir, @Nullable IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + }); + + List diffLocalListing = new ArrayList<>(currentLocalListing); + diffLocalListing.removeAll(previousLocalListing); + diffLocalListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file)); + + if (!diffLocalListing.isEmpty()) { + WatcherCommon.saveNewListing(diffLocalListing, currentLocalListingFile); + } + previousLocalListing = new ArrayList<>(currentLocalListing); + } catch (IOException e) { + logger.debug("File manipulation error: {}", e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml new file mode 100755 index 0000000000000..71c6a0647c0bb --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + FolderWatcher Binding + This binding will monitor specified location for new files and trigger event channel with new file names. + + diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100755 index 0000000000000..1a9dc7d18f1fc --- /dev/null +++ b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,126 @@ + + + + + + FTP folder to be watched + + + + + + + + + Address of FTP server + network-address + + + + 21 + FTP server's port + + + + true + + + + + + NONE + FTP Security settings + true + + + + User name + + + + FTP server password + password + + + + Root directory to be watched + + + + false + Allow listing of hidden files + true + + + + false + Allow listing of sub folders + true + + + + Connection timeout for FTP request, sec + 30 + true + + + + Interval for polling folder changes, sec + 60 + true + + + + How many hours back to analyze + 24 + true + + + + + + + trigger + + A new file name + String + + + + + + Local folder to be watched + + + + + + + + + Local directory to be watched + + + + Interval for polling folder changes, sec + 60 + true + + + + false + Allow listing of hidden files + true + + + + false + Allow listing of sub folders + true + + + + diff --git a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/FoobotApiConnector.java b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/FoobotApiConnector.java index 61d95b998a740..48e12ed582754 100644 --- a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/FoobotApiConnector.java +++ b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/FoobotApiConnector.java @@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -156,7 +155,7 @@ protected String request(String url, String apiKey) throws FoobotApiException { apiKeyLimitRemaining = API_RATE_LIMIT_EXCEEDED; throw new FoobotApiException(response.getStatus(), API_RATE_LIMIT_EXCEEDED_MESSAGE); case HttpStatus.OK_200: - if (StringUtils.trimToNull(content) == null) { + if (content == null || content.isBlank()) { throw new FoobotApiException(0, "No data returned"); } return content; diff --git a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotAccountHandler.java b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotAccountHandler.java index 4684b7456749d..0bece5240d56f 100644 --- a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotAccountHandler.java +++ b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotAccountHandler.java @@ -15,12 +15,14 @@ import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.foobot.internal.FoobotApiConnector; @@ -93,10 +95,12 @@ public void initialize() { final FoobotAccountConfiguration accountConfig = getConfigAs(FoobotAccountConfiguration.class); final List missingParams = new ArrayList<>(); - if (StringUtils.trimToNull(accountConfig.apiKey) == null) { + String apiKey = accountConfig.apiKey; + if (apiKey.isBlank()) { missingParams.add("'apikey'"); } - if (StringUtils.trimToNull(accountConfig.username) == null) { + String username = accountConfig.username; + if (username.isBlank()) { missingParams.add("'username'"); } @@ -104,13 +108,13 @@ public void initialize() { final boolean oneParam = missingParams.size() == 1; final String errorMsg = String.format( "Parameter%s [%s] %s mandatory and must be configured and not be empty", oneParam ? "" : "s", - StringUtils.join(missingParams, ", "), oneParam ? "is" : "are"); + String.join(", ", missingParams), oneParam ? "is" : "are"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg); return; } - username = accountConfig.username; - connector.setApiKey(accountConfig.apiKey); + this.username = username; + connector.setApiKey(apiKey); refreshInterval = accountConfig.refreshInterval; if (this.refreshInterval < MINIMUM_REFRESH_PERIOD_MINUTES) { logger.warn( @@ -118,8 +122,7 @@ public void initialize() { accountConfig.refreshInterval, MINIMUM_REFRESH_PERIOD_MINUTES, DEFAULT_REFRESH_PERIOD_MINUTES); refreshInterval = DEFAULT_REFRESH_PERIOD_MINUTES; } - logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", accountConfig.username, - refreshInterval); + logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", username, refreshInterval); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Wait to get associated devices"); diff --git a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotDeviceHandler.java b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotDeviceHandler.java index 46302f7cac200..a9855e1467d9f 100644 --- a/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotDeviceHandler.java +++ b/bundles/org.openhab.binding.foobot/src/main/java/org/openhab/binding/foobot/internal/handler/FoobotDeviceHandler.java @@ -21,7 +21,6 @@ import javax.measure.Unit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.foobot.internal.FoobotApiConnector; @@ -93,7 +92,7 @@ public void initialize() { logger.debug("Initializing Foobot handler."); uuid = (String) getConfig().get(FoobotBindingConstants.CONFIG_UUID); - if (StringUtils.trimToNull(uuid) == null) { + if (uuid.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Parameter 'uuid' is mandatory and must be configured"); return; diff --git a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/api/FreeboxApiManager.java b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/api/FreeboxApiManager.java index e90294af8e8f0..c2b84ad9e6250 100644 --- a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/api/FreeboxApiManager.java +++ b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/api/FreeboxApiManager.java @@ -121,7 +121,8 @@ public FreeboxDiscoveryResponse checkApi(String fqdn, boolean secureHttp) { } } - public boolean authorize(boolean useHttps, String fqdn, String apiBaseUrl, String apiVersion, String appToken) { + public boolean authorize(boolean useHttps, String fqdn, String apiBaseUrl, String apiVersion, String appToken) + throws InterruptedException { String[] versionSplit = apiVersion.split("\\."); String majorVersion = "5"; if (versionSplit.length > 0) { @@ -155,7 +156,7 @@ public boolean authorize(boolean useHttps, String fqdn, String apiBaseUrl, Strin this.appToken = token; openSession(); return true; - } catch (FreeboxException | InterruptedException e) { + } catch (FreeboxException e) { logger.debug("Error while opening a session", e); return false; } diff --git a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/handler/FreeboxHandler.java b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/handler/FreeboxHandler.java index f0a48f8baffd8..5fe71a3f5efe2 100644 --- a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/handler/FreeboxHandler.java +++ b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/handler/FreeboxHandler.java @@ -158,7 +158,13 @@ public void initialize() { logger.debug("Binding will schedule a job to establish a connection..."); if (authorizeJob == null || authorizeJob.isCancelled()) { - authorizeJob = scheduler.schedule(this::authorize, 1, TimeUnit.SECONDS); + authorizeJob = scheduler.schedule(() -> { + try { + authorize(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, 1, TimeUnit.SECONDS); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -196,7 +202,7 @@ private void pollServerState() { } } - private void authorize() { + private void authorize() throws InterruptedException { logger.debug("Authorize job..."); String fqdn = configuration.fqdn; diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusBridgeHandler.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusBridgeHandler.java index 16662d873dfda..274157f0a3fe0 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusBridgeHandler.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusBridgeHandler.java @@ -18,7 +18,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration; import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.thing.Bridge; @@ -62,10 +61,13 @@ public void initialize() { boolean validConfig = true; String errorMsg = null; - if (StringUtils.trimToNull(config.hostname) == null) { + + String hostname = config.hostname; + if (hostname == null || hostname.isBlank()) { errorMsg = "Parameter 'hostname' is mandatory and must be configured"; validConfig = false; } + if (config.refreshInterval != null && config.refreshInterval <= 0) { errorMsg = "Parameter 'refresh' must be at least 1 second"; validConfig = false; diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusMeterHandler.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusMeterHandler.java index 22e62e8706c06..9e590f0f7912c 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusMeterHandler.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusMeterHandler.java @@ -14,7 +14,7 @@ import java.util.Map; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.fronius.internal.FroniusBaseDeviceConfiguration; import org.openhab.binding.fronius.internal.FroniusBindingConstants; import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration; diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java index e156d7d9bd831..ff8592f8d20c2 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.fronius.internal.handler; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.fronius.internal.FroniusBaseDeviceConfiguration; import org.openhab.binding.fronius.internal.FroniusBindingConstants; import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration; diff --git a/bundles/org.openhab.binding.fsinternetradio/src/main/java/org/openhab/binding/fsinternetradio/internal/handler/FSInternetRadioHandler.java b/bundles/org.openhab.binding.fsinternetradio/src/main/java/org/openhab/binding/fsinternetradio/internal/handler/FSInternetRadioHandler.java index a24cb7f043172..548d00a951643 100644 --- a/bundles/org.openhab.binding.fsinternetradio/src/main/java/org/openhab/binding/fsinternetradio/internal/handler/FSInternetRadioHandler.java +++ b/bundles/org.openhab.binding.fsinternetradio/src/main/java/org/openhab/binding/fsinternetradio/internal/handler/FSInternetRadioHandler.java @@ -18,7 +18,6 @@ import java.math.BigDecimal; import java.util.concurrent.ScheduledFuture; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadio; import org.openhab.core.library.types.DecimalType; @@ -128,7 +127,7 @@ public void initialize() { final BigDecimal port = (BigDecimal) getThing().getConfiguration().get(CONFIG_PROPERTY_PORT); final String pin = (String) getThing().getConfiguration().get(CONFIG_PROPERTY_PIN); - if (ip == null || StringUtils.isEmpty(pin) || port.intValue() == 0) { + if (ip == null || pin == null || pin.isEmpty() || port.intValue() == 0) { // configuration incomplete updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration incomplete"); } else { diff --git a/bundles/org.openhab.binding.fsinternetradio/src/test/java/org/openhab/binding/fsinternetradio/test/FSInternetRadioHandlerJavaTest.java b/bundles/org.openhab.binding.fsinternetradio/src/test/java/org/openhab/binding/fsinternetradio/test/FSInternetRadioHandlerJavaTest.java index 479ff037ebbb8..0b28a407ab93f 100644 --- a/bundles/org.openhab.binding.fsinternetradio/src/test/java/org/openhab/binding/fsinternetradio/test/FSInternetRadioHandlerJavaTest.java +++ b/bundles/org.openhab.binding.fsinternetradio/src/test/java/org/openhab/binding/fsinternetradio/test/FSInternetRadioHandlerJavaTest.java @@ -21,9 +21,12 @@ import java.io.IOException; import java.math.BigDecimal; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.servlet.ServletHolder; @@ -818,7 +821,7 @@ private boolean isConfigurationComplete(Configuration config) { BigDecimal port = (BigDecimal) config.get(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PORT.toString()); String pin = (String) config.get(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PIN.toString()); - if (ip == null || port.compareTo(BigDecimal.ZERO) == 0 || StringUtils.isEmpty(pin)) { + if (ip == null || port.compareTo(BigDecimal.ZERO) == 0 || pin == null || pin.isEmpty()) { return false; } return true; diff --git a/bundles/org.openhab.binding.ftpupload/README.md b/bundles/org.openhab.binding.ftpupload/README.md index 5d102e0588818..8ee0f6ac7baaa 100644 --- a/bundles/org.openhab.binding.ftpupload/README.md +++ b/bundles/org.openhab.binding.ftpupload/README.md @@ -18,10 +18,11 @@ Automatic discovery is not supported. The binding has the following configuration options: -| Parameter | Name | Description | Required | Default value | -|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------| -| port | TCP Port | TCP port of the FTP server | no | 2121 | -| idleTimeout | Idle timeout | The number of seconds before an inactive client is disconnected. If this value is set to 0, the idle time is disabled. | no | 60 | +| Parameter | Name | Description | Required | Default value | +|--------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| port | TCP Port | TCP port of the FTP server | no | 2121 | +| idleTimeout | Idle timeout | The number of seconds before an inactive client is disconnected. If this value is set to 0, the idle time is disabled. | no | 60 | +| passivePorts | Passive Ports | A string of passive ports, can contain a single port (as an integer), multiple ports seperated by commas (e.g. 123,124,125) or ranges of ports, including open ended ranges (e.g. 123-125, 30000-, -1023). Combinations for single ports and ranges is also supported. Empty (default) allows all ports as passive ports. | no | | ## Thing Configuration diff --git a/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/FtpUploadHandlerFactory.java b/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/FtpUploadHandlerFactory.java index b888e570a9053..21dd1e83b1375 100644 --- a/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/FtpUploadHandlerFactory.java +++ b/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/FtpUploadHandlerFactory.java @@ -18,7 +18,7 @@ import java.util.Dictionary; import java.util.Set; -import org.apache.commons.lang.StringUtils; +import org.apache.ftpserver.DataConnectionConfigurationFactory; import org.apache.ftpserver.FtpServerConfigurationException; import org.apache.ftpserver.ftplet.FtpException; import org.openhab.binding.ftpupload.internal.ftp.FtpServer; @@ -90,13 +90,14 @@ protected synchronized void deactivate(ComponentContext componentContext) { protected synchronized void modified(ComponentContext componentContext) { stopFtpServer(); Dictionary properties = componentContext.getProperties(); + DataConnectionConfigurationFactory dataConnectionConfigurationFactory = new DataConnectionConfigurationFactory(); int port = DEFAULT_PORT; int idleTimeout = DEFAULT_IDLE_TIMEOUT; if (properties.get("port") != null) { String strPort = properties.get("port").toString(); - if (StringUtils.isNotEmpty(strPort)) { + if (!strPort.isEmpty()) { try { port = Integer.valueOf(strPort); } catch (NumberFormatException e) { @@ -107,7 +108,7 @@ protected synchronized void modified(ComponentContext componentContext) { if (properties.get("idleTimeout") != null) { String strIdleTimeout = properties.get("idleTimeout").toString(); - if (StringUtils.isNotEmpty(strIdleTimeout)) { + if (!strIdleTimeout.isEmpty()) { try { idleTimeout = Integer.valueOf(strIdleTimeout); } catch (NumberFormatException e) { @@ -116,9 +117,21 @@ protected synchronized void modified(ComponentContext componentContext) { } } + if (properties.get("passivePorts") != null) { + String strPassivePorts = properties.get("passivePorts").toString(); + if (!strPassivePorts.isEmpty()) { + try { + dataConnectionConfigurationFactory.setPassivePorts(strPassivePorts); + } catch (IllegalArgumentException e) { + logger.warn("Invalid passive ports '{}' ({})", strPassivePorts, e.getMessage()); + } + } + } + try { logger.debug("Starting FTP server, port={}, idleTimeout={}", port, idleTimeout); - ftpServer.startServer(port, idleTimeout); + ftpServer.startServer(port, idleTimeout, + dataConnectionConfigurationFactory.createDataConnectionConfiguration()); } catch (FtpException | FtpServerConfigurationException e) { logger.warn("FTP server starting failed, reason: {}", e.getMessage()); } diff --git a/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/ftp/FtpServer.java b/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/ftp/FtpServer.java index df1444f85a912..0cec11fdad1a7 100644 --- a/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/ftp/FtpServer.java +++ b/bundles/org.openhab.binding.ftpupload/src/main/java/org/openhab/binding/ftpupload/internal/ftp/FtpServer.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.apache.ftpserver.DataConnectionConfiguration; import org.apache.ftpserver.FtpServerConfigurationException; import org.apache.ftpserver.FtpServerFactory; import org.apache.ftpserver.ftplet.DefaultFtplet; @@ -48,6 +49,7 @@ public class FtpServer { private final Logger logger = LoggerFactory.getLogger(FtpServer.class); private int port; + private DataConnectionConfiguration dataConnectionConfiguration; int idleTimeout; private org.apache.ftpserver.FtpServer server; @@ -61,10 +63,12 @@ public FtpServer() { FTPUserManager = new FTPUserManager(); } - public void startServer(int port, int idleTimeout) throws FtpException { + public void startServer(int port, int idleTimeout, DataConnectionConfiguration dataConnectionConfiguration) + throws FtpException { stopServer(); this.port = port; this.idleTimeout = idleTimeout; + this.dataConnectionConfiguration = dataConnectionConfiguration; FTPUserManager.setIdleTimeout(idleTimeout); initServer(); } @@ -127,8 +131,10 @@ public void printStats() { private void initServer() throws FtpException { FtpServerFactory serverFactory = new FtpServerFactory(); ListenerFactory listenerFactory = new ListenerFactory(); + listenerFactory.setPort(port); listenerFactory.setIdleTimeout(idleTimeout); + listenerFactory.setDataConnectionConfiguration(dataConnectionConfiguration); Listener listener = listenerFactory.createListener(); diff --git a/bundles/org.openhab.binding.ftpupload/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ftpupload/src/main/resources/OH-INF/binding/binding.xml index 891acf55fc0b4..f76a75aabaa59 100644 --- a/bundles/org.openhab.binding.ftpupload/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.ftpupload/src/main/resources/OH-INF/binding/binding.xml @@ -18,5 +18,15 @@ time is disabled. 60 + + + A string of passive ports, can contain a single port (as an integer), multiple ports seperated by + commas + (e.g. 123,124,125) or ranges of ports, including open ended ranges (e.g. 123-125, 30000-, -1023). + Combinations for + single ports and ranges is also supported. Empty (default) allows all ports as passive ports. + + true + diff --git a/bundles/org.openhab.binding.globalcache/src/main/java/org/openhab/binding/globalcache/internal/handler/GlobalCacheHandler.java b/bundles/org.openhab.binding.globalcache/src/main/java/org/openhab/binding/globalcache/internal/handler/GlobalCacheHandler.java index 986b6db1c8a68..119fb83d0efa2 100644 --- a/bundles/org.openhab.binding.globalcache/src/main/java/org/openhab/binding/globalcache/internal/handler/GlobalCacheHandler.java +++ b/bundles/org.openhab.binding.globalcache/src/main/java/org/openhab/binding/globalcache/internal/handler/GlobalCacheHandler.java @@ -36,7 +36,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.globalcache.internal.GlobalCacheBindingConstants.CommandType; import org.openhab.binding.globalcache.internal.command.CommandGetstate; @@ -252,7 +251,7 @@ private String lookupCode(Command command) { } String mapFile = (String) thing.getConfiguration().get(THING_CONFIG_MAP_FILENAME); - if (StringUtils.isEmpty(mapFile)) { + if (mapFile == null || mapFile.isEmpty()) { logger.warn("MAP file is not defined in configuration of thing {}", thingID()); return null; } @@ -266,14 +265,13 @@ private String lookupCode(Command command) { String code; try { code = transformService.transform(mapFile, command.toString()); - } catch (TransformationException e) { logger.error("Failed to transform {} for thing {} using map file '{}', exception={}", command, thingID(), mapFile, e.getMessage()); return null; } - if (StringUtils.isEmpty(code)) { + if (code == null || code.isEmpty()) { logger.warn("No entry for {} in map file '{}' for thing {}", command, mapFile, thingID()); return null; } @@ -638,7 +636,7 @@ public ConnectionManager() { private String getIPAddress() { String ipAddress = ((GlobalCacheHandler) thing.getHandler()).getIP(); - if (StringUtils.isEmpty(ipAddress)) { + if (ipAddress == null || ipAddress.isEmpty()) { logger.debug("Handler for thing {} could not get IP address from config", thingID()); markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "IP address not set"); } @@ -912,7 +910,7 @@ private SerialPortReader startSerialPortReader(CommandType serialDevice, String if (Boolean.TRUE.equals(enableTwoWay)) { // Get the end of message delimiter from the config, URL decode it, and convert it to a byte array String endOfMessageString = (String) thing.getConfiguration().get(endOfMessageDelimiterConfig); - if (StringUtils.isNotEmpty(endOfMessageString)) { + if (endOfMessageString != null && !endOfMessageString.isEmpty()) { logger.debug("End of message is {} for thing {} {}", endOfMessageString, thingID(), serialDevice); byte[] endOfMessage; try { diff --git a/bundles/org.openhab.binding.goecharger/src/main/java/org/openhab/binding/goecharger/internal/handler/GoEChargerHandler.java b/bundles/org.openhab.binding.goecharger/src/main/java/org/openhab/binding/goecharger/internal/handler/GoEChargerHandler.java index 6b54975a07f05..39dfd9fdbfcaa 100644 --- a/bundles/org.openhab.binding.goecharger/src/main/java/org/openhab/binding/goecharger/internal/handler/GoEChargerHandler.java +++ b/bundles/org.openhab.binding.goecharger/src/main/java/org/openhab/binding/goecharger/internal/handler/GoEChargerHandler.java @@ -45,7 +45,7 @@ import javax.measure.quantity.ElectricCurrent; import javax.measure.quantity.Energy; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; diff --git a/bundles/org.openhab.binding.gpio/README.md b/bundles/org.openhab.binding.gpio/README.md index e935a735f753e..622cf483582bd 100644 --- a/bundles/org.openhab.binding.gpio/README.md +++ b/bundles/org.openhab.binding.gpio/README.md @@ -22,12 +22,25 @@ sudo raspi-config -> Interfacing Options --> Remote GPIO --> YES --> OK --> Finish +Note: if you are setting this up on a Raspberry Pi without `raspi-config` you can create the service config file manually: + +``` +sudo mkdir -p /etc/systemd/system/pigpiod.service.d/ +sudo nano /etc/systemd/system/pigpiod.service.d/public.conf +``` + [Service] + ExecStart= + ExecStart=/usr/bin/pigpiod +``` +sudo systemctl daemon-reload +``` +Now that Remote GPIO is enabled, get the daemon going: ``` sudo systemctl enable pigpiod sudo systemctl start pigpiod ``` -Set `host` to the address of the pi and the `port` to the port of pigpio (default: 8888). +In openHAB, set `host` to the address of the pi and the `port` to the port of pigpio (default: 8888). ## Channels diff --git a/bundles/org.openhab.binding.gpstracker/README.md b/bundles/org.openhab.binding.gpstracker/README.md index f5c2ac1d6ced0..80e069431799d 100644 --- a/bundles/org.openhab.binding.gpstracker/README.md +++ b/bundles/org.openhab.binding.gpstracker/README.md @@ -249,8 +249,6 @@ After a location message received from the tracker the log should contain these 2018-10-05 09:27:58.794 [TRACE] [cker.internal.handler.TrackerHandler] - System uses SI measurement units. No conversion is needed. ``` -**Note**: If the binding was restarted or the distance channel is new (this is the first location message for the channel) only the second location update will trigger event as the binding has to know the previous state. - ### External Region and Presence Switch Assumptions: diff --git a/bundles/org.openhab.binding.gpstracker/src/main/java/org/openhab/binding/gpstracker/internal/handler/TrackerHandler.java b/bundles/org.openhab.binding.gpstracker/src/main/java/org/openhab/binding/gpstracker/internal/handler/TrackerHandler.java index 9d4ede111d195..582ed15b92e6f 100644 --- a/bundles/org.openhab.binding.gpstracker/src/main/java/org/openhab/binding/gpstracker/internal/handler/TrackerHandler.java +++ b/bundles/org.openhab.binding.gpstracker/src/main/java/org/openhab/binding/gpstracker/internal/handler/TrackerHandler.java @@ -262,7 +262,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ private void updateTriggerChannelsWithTransition(TransitionMessage message) { String regionName = message.getRegionName(); - triggerRegionChannel(regionName, message.getEvent()); + triggerRegionChannel(regionName, message.getEvent(), true); } /** @@ -270,11 +270,12 @@ private void updateTriggerChannelsWithTransition(TransitionMessage message) { * * @param regionName Region name * @param event Occurred event + * @param forced Force channel triggering in case the transition event is received from the mobile application. */ - private void triggerRegionChannel(@NonNull String regionName, @NonNull String event) { + private void triggerRegionChannel(@NonNull String regionName, @NonNull String event, boolean forced) { Boolean lastState = lastTriggeredStates.get(regionName); Boolean newState = EVENT_ENTER.equals(event); - if (!newState.equals(lastState) && lastState != null) { + if (!newState.equals(lastState) || forced) { String payload = regionName + "/" + event; triggerChannel(CHANNEL_REGION_TRIGGER, payload); lastTriggeredStates.put(regionName, newState); @@ -327,9 +328,9 @@ private void updateDistanceChannelFromMessage(LocationMessage message, Channel c // convert into meters which is the unit of the calculated distance double radiusMeter = convertToMeters(ConfigHelper.getRegionRadius(c.getConfiguration())); if (radiusMeter > newDistance) { - triggerRegionChannel(regionName, EVENT_ENTER); + triggerRegionChannel(regionName, EVENT_ENTER, false); } else { - triggerRegionChannel(regionName, EVENT_LEAVE); + triggerRegionChannel(regionName, EVENT_LEAVE, false); } } } diff --git a/bundles/org.openhab.binding.gpstracker/src/main/resources/OH-INF/thing/tracker.xml b/bundles/org.openhab.binding.gpstracker/src/main/resources/OH-INF/thing/tracker.xml index 9221356941657..acda2134cd8b7 100644 --- a/bundles/org.openhab.binding.gpstracker/src/main/resources/OH-INF/thing/tracker.xml +++ b/bundles/org.openhab.binding.gpstracker/src/main/resources/OH-INF/thing/tracker.xml @@ -31,7 +31,7 @@ Number:Length GPS accuracy - + DateTime diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java index 1869a1e54e10a..73ccda0c8ee46 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.DatagramSocket; import java.time.Instant; import java.util.List; @@ -567,7 +568,7 @@ private String logInfo(String msgKey, Object... arg) { public static QuantityType toQuantityType(Number value, int digits, Unit unit) { BigDecimal bd = new BigDecimal(value.doubleValue()); - return new QuantityType<>(bd.setScale(digits, BigDecimal.ROUND_HALF_EVEN), unit); + return new QuantityType<>(bd.setScale(digits, RoundingMode.HALF_EVEN), unit); } private void stopRefreshTask() { diff --git a/bundles/org.openhab.binding.groheondus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.groheondus/src/main/resources/OH-INF/thing/thing-types.xml index b7fef7a5fb6c8..745038ce43948 100644 --- a/bundles/org.openhab.binding.groheondus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.groheondus/src/main/resources/OH-INF/thing/thing-types.xml @@ -13,11 +13,9 @@ Username as used in the GROHE ONDUS App, usually your e-mail address. - true - true password Password as used in the GROHE ONDUS App. diff --git a/bundles/org.openhab.binding.harmonyhub/src/main/java/org/openhab/binding/harmonyhub/internal/handler/HarmonyHubHandler.java b/bundles/org.openhab.binding.harmonyhub/src/main/java/org/openhab/binding/harmonyhub/internal/handler/HarmonyHubHandler.java index 43013400baa80..eb974d0f74133 100644 --- a/bundles/org.openhab.binding.harmonyhub/src/main/java/org/openhab/binding/harmonyhub/internal/handler/HarmonyHubHandler.java +++ b/bundles/org.openhab.binding.harmonyhub/src/main/java/org/openhab/binding/harmonyhub/internal/handler/HarmonyHubHandler.java @@ -25,7 +25,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.harmonyhub.internal.HarmonyHubHandlerFactory; @@ -263,9 +262,9 @@ private synchronized void connect() { // earlier versions required a name and used network discovery to find the hub and retrieve the host, // this section is to not break that and also update older configurations to use the host configuration // option instead of name - if (StringUtils.isBlank(host)) { + if (host == null || host.isBlank()) { host = getThing().getProperties().get(HUB_PROPERTY_HOST); - if (StringUtils.isNotBlank(host)) { + if (host != null && !host.isBlank()) { Configuration genericConfig = getConfig(); genericConfig.put(HUB_PROPERTY_HOST, host); updateConfiguration(genericConfig); diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java index ebaba9f9e3c79..8e3d1d9919ad6 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java @@ -101,7 +101,6 @@ public void dispose() { @Override public void initialize() { - updateStatus(ThingStatus.UNKNOWN); initializeFuture = scheduler.schedule(this::scheduledInitialize, 1, TimeUnit.SECONDS); return; } @@ -143,7 +142,10 @@ public void scheduledInitialize() { return; } - updateStatus(ThingStatus.ONLINE); + if (this.thing.getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + logger.debug("Succesfully opened connection to Hayward's server: {} Username:{}", config.endpointUrl, config.username); @@ -350,6 +352,7 @@ private synchronized void initPolling(int initalDelay) { commFailureCount++; return; } + updateStatus(ThingStatus.ONLINE); } catch (HaywardException e) { logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage()); } catch (InterruptedException e) { @@ -424,15 +427,15 @@ public synchronized String httpXmlResponse(String urlParameters) throws HaywardE int status = httpResponse.getStatus(); String xmlResponse = httpResponse.getContentAsString(); - List statusMessages = evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", - xmlResponse); - if (!(statusMessages.isEmpty())) { - statusMessage = statusMessages.get(0); - } else { - statusMessage = httpResponse.getReason(); - } - if (status == 200) { + List statusMessages = evaluateXPath( + "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse); + if (!(statusMessages.isEmpty())) { + statusMessage = statusMessages.get(0); + } else { + statusMessage = httpResponse.getReason(); + } + if (logger.isTraceEnabled()) { logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(), urlParameters); @@ -444,7 +447,8 @@ public synchronized String httpXmlResponse(String urlParameters) throws HaywardE if (logger.isDebugEnabled()) { logger.debug("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(), urlParameters); - logger.debug("Hayward Connection thing: {} Hayward http response: {}", getCallingMethod(), status); + logger.debug("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(), + status, xmlResponse); } return ""; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bridge.xml index 1b5c8f9534bfd..51d0a90868fcf 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bridge.xml @@ -13,7 +13,7 @@ url - https://app1.haywardomnilogic.com/HAAPI/HomeAutomation/API.ashx + https://www.haywardomnilogic.com/HAAPI/HomeAutomation/API.ashx The URL of the Hayward API Server diff --git a/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/Mhub4K431Handler.java b/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/Mhub4K431Handler.java index 417ee7a367a1e..79b114d45698d 100644 --- a/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/Mhub4K431Handler.java +++ b/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/Mhub4K431Handler.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.hdanywhere.internal.handler; -import static org.apache.commons.lang.StringUtils.isNotBlank; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -93,27 +91,25 @@ public void dispose() { String content = "{tag:ptn}"; InputStream stream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - if (isNotBlank(httpMethod) && isNotBlank(url)) { - String response = HttpUtil.executeUrl(httpMethod, url, null, stream, null, timeout); - response = response.trim(); - response = response.substring(1, response.length() - 1); + String response = HttpUtil.executeUrl(httpMethod, url, null, stream, null, timeout); + response = response.trim(); + response = response.substring(1, response.length() - 1); - if (response != null) { - updateStatus(ThingStatus.ONLINE); + if (response != null) { + updateStatus(ThingStatus.ONLINE); - java.lang.reflect.Type type = new TypeToken>() { - }.getType(); - Map map = gson.fromJson(response, type); + java.lang.reflect.Type type = new TypeToken>() { + }.getType(); + Map map = gson.fromJson(response, type); - String inputChannel = map.get("Inputchannel"); + String inputChannel = map.get("Inputchannel"); - for (int i = 0; i < numberOfPorts; i++) { - DecimalType decimalType = new DecimalType(String.valueOf(inputChannel.charAt(i))); - updateState(new ChannelUID(getThing().getUID(), Port.get(i + 1).channelID()), decimalType); - } - } else { - updateStatus(ThingStatus.OFFLINE); + for (int i = 0; i < numberOfPorts; i++) { + DecimalType decimalType = new DecimalType(String.valueOf(inputChannel.charAt(i))); + updateState(new ChannelUID(getThing().getUID(), Port.get(i + 1).channelID()), decimalType); } + } else { + updateStatus(ThingStatus.OFFLINE); } } catch (Exception e) { logger.debug("An exception occurred while polling the HDanwywhere matrix: '{}'", e.getMessage()); diff --git a/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/MultiroomPlusHandler.java b/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/MultiroomPlusHandler.java index 70a4672d5c52b..a2cdc8c9539c6 100644 --- a/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/MultiroomPlusHandler.java +++ b/bundles/org.openhab.binding.hdanywhere/src/main/java/org/openhab/binding/hdanywhere/internal/handler/MultiroomPlusHandler.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.hdanywhere.internal.handler; -import static org.apache.commons.lang.StringUtils.isNotBlank; - import java.io.IOException; import java.math.BigDecimal; import java.util.concurrent.ScheduledFuture; @@ -69,24 +67,22 @@ public MultiroomPlusHandler(Thing thing) { String httpMethod = "GET"; String url = "http://" + host + "/status_show.shtml"; - if (isNotBlank(httpMethod) && isNotBlank(url)) { - String response = HttpUtil.executeUrl(httpMethod, url, null, null, null, timeout); + String response = HttpUtil.executeUrl(httpMethod, url, null, null, null, timeout); - if (response != null) { - updateStatus(ThingStatus.ONLINE); + if (response != null) { + updateStatus(ThingStatus.ONLINE); - for (int i = 1; i <= numberOfPorts; i++) { - Pattern p = Pattern.compile("var out" + i + "var = (.*);"); - Matcher m = p.matcher(response); + for (int i = 1; i <= numberOfPorts; i++) { + Pattern p = Pattern.compile("var out" + i + "var = (.*);"); + Matcher m = p.matcher(response); - while (m.find()) { - DecimalType decimalType = new DecimalType(m.group(1)); - updateState(new ChannelUID(getThing().getUID(), Port.get(i).channelID()), decimalType); - } + while (m.find()) { + DecimalType decimalType = new DecimalType(m.group(1)); + updateState(new ChannelUID(getThing().getUID(), Port.get(i).channelID()), decimalType); } - } else { - updateStatus(ThingStatus.OFFLINE); } + } else { + updateStatus(ThingStatus.OFFLINE); } } catch (Exception e) { logger.warn("An exception occurred while polling the HDanwywhere matrix: '{}'", e.getMessage()); diff --git a/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/mhub4k431.xml b/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/mhub4k431.xml index 4295ba194cfd1..5bdbeecc7796f 100644 --- a/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/mhub4k431.xml +++ b/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/mhub4k431.xml @@ -17,10 +17,9 @@ - + Network address of the Matrix - true diff --git a/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/multiroomplus.xml b/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/multiroomplus.xml index 334c82be757b3..d221ed27d8b51 100644 --- a/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/multiroomplus.xml +++ b/bundles/org.openhab.binding.hdanywhere/src/main/resources/OH-INF/thing/multiroomplus.xml @@ -21,10 +21,9 @@ - + Network address of the Matrix - true diff --git a/bundles/org.openhab.binding.hdpowerview/noEmbedDependencies.profile b/bundles/org.openhab.binding.hdpowerview/noEmbedDependencies.profile deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/bundles/org.openhab.binding.hdpowerview/pom.xml b/bundles/org.openhab.binding.hdpowerview/pom.xml index 3aabeea2eab87..e66e270a3f839 100644 --- a/bundles/org.openhab.binding.hdpowerview/pom.xml +++ b/bundles/org.openhab.binding.hdpowerview/pom.xml @@ -14,9 +14,15 @@ openHAB Add-ons :: Bundles :: Hunter Douglas PowerView Binding + + + !jcifs.* + + + - org.samba.jcifs + jcifs jcifs 1.3.17 compile diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/feature/feature.xml b/bundles/org.openhab.binding.hdpowerview/src/main/feature/feature.xml index 589480fba40c6..71ed90ed0ecf4 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.hdpowerview/src/main/feature/feature.xml @@ -4,7 +4,6 @@ openhab-runtime-base - mvn:org.samba.jcifs/jcifs/1.3.17 mvn:org.openhab.addons.bundles/org.openhab.binding.hdpowerview/${project.version} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewHandlerFactory.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewHandlerFactory.java index 6fb6c4747296b..8b4dc8aeb8be1 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewHandlerFactory.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewHandlerFactory.java @@ -14,14 +14,14 @@ import java.util.Hashtable; -import javax.ws.rs.client.ClientBuilder; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.hdpowerview.internal.discovery.HDPowerViewShadeDiscoveryService; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler; import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; @@ -41,11 +41,12 @@ @NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.hdpowerview") public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory { - private final ClientBuilder clientBuilder; + + private final HttpClient httpClient; @Activate - public HDPowerViewHandlerFactory(@Reference ClientBuilder clientBuilder) { - this.clientBuilder = clientBuilder; + public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); } @Override @@ -58,7 +59,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_HUB)) { - HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, clientBuilder); + HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient); registerService(new HDPowerViewShadeDiscoveryService(handler)); return handler; } else if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_SHADE)) { diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java index 94174ba16348a..4d40d9927bee2 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java @@ -13,17 +13,18 @@ package org.openhab.binding.hdpowerview.internal; import java.time.Instant; - -import javax.ws.rs.ProcessingException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.Invocation; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove; import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop; @@ -45,14 +46,6 @@ @NonNullByDefault public class HDPowerViewWebTargets { - private static final String PUT = "PUT"; - private static final String GET = "GET"; - private static final String SCENE_ID = "sceneId"; - private static final String ID = "id"; - private static final String REFRESH = "refresh"; - private static final String CONN_HDR = "Connection"; - private static final String CONN_VAL = "close"; // versus "keep-alive" - private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class); /* @@ -64,128 +57,159 @@ public class HDPowerViewWebTargets { private final int maintenancePeriod = 300; private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod); - private WebTarget base; - private WebTarget shades; - private WebTarget shade; - private WebTarget sceneActivate; - private WebTarget scenes; + private final String base; + private final String shades; + private final String sceneActivate; + private final String scenes; private final Gson gson = new Gson(); + private final HttpClient httpClient; + + /** + * private helper class for passing http url query parameters + */ + private static class Query { + private final String key; + private final String value; + + private Query(String key, String value) { + this.key = key; + this.value = value; + } + + public static Query of(String key, String value) { + return new Query(key, value); + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + } /** * Initialize the web targets - * - * @param client the Javax RS client (the binding) + * + * @param httpClient the HTTP client (the binding) * @param ipAddress the IP address of the server (the hub) */ - public HDPowerViewWebTargets(Client client, String ipAddress) { - base = client.target("http://" + ipAddress + "/api"); - shades = base.path("shades/"); - shade = base.path("shades/{id}"); - sceneActivate = base.path("scenes"); - scenes = base.path("scenes/"); + public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) { + base = "http://" + ipAddress + "/api/"; + shades = base + "shades/"; + sceneActivate = base + "scenes"; + scenes = base + "scenes/"; + this.httpClient = httpClient; } /** * Fetches a JSON package that describes all shades in the hub, and wraps it in * a Shades class instance - * + * * @return Shades class instance * @throws JsonParseException if there is a JSON parsing error - * @throws ProcessingException if there is any processing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public @Nullable Shades getShades() throws JsonParseException, ProcessingException, HubMaintenanceException { - String json = invoke(shades.request().header(CONN_HDR, CONN_VAL).buildGet(), shades, null); + public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException { + String json = invoke(HttpMethod.GET, shades, null, null); return gson.fromJson(json, Shades.class); } /** * Instructs the hub to move a specific shade - * + * * @param shadeId id of the shade to be moved * @param position instance of ShadePosition containing the new position - * @throws ProcessingException if there is any processing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public void moveShade(int shadeId, ShadePosition position) throws ProcessingException, HubMaintenanceException { - WebTarget target = shade.resolveTemplate(ID, shadeId); + public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException { String json = gson.toJson(new ShadeMove(shadeId, position)); - invoke(target.request().header(CONN_HDR, CONN_VAL) - .buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json); - return; + invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json); } /** * Fetches a JSON package that describes all scenes in the hub, and wraps it in * a Scenes class instance - * + * * @return Scenes class instance * @throws JsonParseException if there is a JSON parsing error - * @throws ProcessingException if there is any processing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public @Nullable Scenes getScenes() throws JsonParseException, ProcessingException, HubMaintenanceException { - String json = invoke(scenes.request().header(CONN_HDR, CONN_VAL).buildGet(), scenes, null); + public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException { + String json = invoke(HttpMethod.GET, scenes, null, null); return gson.fromJson(json, Scenes.class); } /** * Instructs the hub to execute a specific scene - * + * * @param sceneId id of the scene to be executed - * @throws ProcessingException if there is any processing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public void activateScene(int sceneId) throws ProcessingException, HubMaintenanceException { - WebTarget target = sceneActivate.queryParam(SCENE_ID, sceneId); - invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null); + public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException { + invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null); } - private synchronized String invoke(Invocation invocation, WebTarget target, @Nullable String jsonCommand) - throws ProcessingException, HubMaintenanceException { + /** + * Invoke a call on the hub server to retrieve information or send a command + * + * @param method GET or PUT + * @param url the host url to be called + * @param query the http query parameter + * @param jsonCommand the request command content (as a json string) + * @return the response content (as a json string) + * @throws HubProcessingException + * @throws HubMaintenanceException + * @throws HubProcessingException + */ + private synchronized String invoke(HttpMethod method, String url, @Nullable Query query, + @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException { if (logger.isTraceEnabled()) { - logger.trace("API command {} {}", jsonCommand == null ? GET : PUT, target.getUri()); + logger.trace("API command {} {}", method, url); if (jsonCommand != null) { logger.trace("JSON command = {}", jsonCommand); } } - Response response; + Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*"); + if (query != null) { + request.param(query.getKey(), query.getValue()); + } + if (jsonCommand != null) { + request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand)); + } + ContentResponse response; try { - response = invocation.invoke(); - } catch (ProcessingException e) { + response = request.send(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { if (Instant.now().isBefore(maintenanceScheduledEnd)) { // throw "softer" exception during maintenance window logger.debug("Hub still undergoing maintenance"); throw new HubMaintenanceException("Hub still undergoing maintenance"); } - throw e; + throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage())); } int statusCode = response.getStatus(); - if (statusCode == 423) { + if (statusCode == HttpStatus.LOCKED_423) { // set end of maintenance window, and throw a "softer" exception maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod); logger.debug("Hub undergoing maintenance"); - if (response.hasEntity()) { - response.readEntity(String.class); - } - response.close(); throw new HubMaintenanceException("Hub undergoing maintenance"); } - if (statusCode != 200) { - logger.warn("Hub returned HTTP error '{}'", statusCode); - if (response.hasEntity()) { - response.readEntity(String.class); - } - response.close(); - throw new ProcessingException(String.format("HTTP %d error", statusCode)); + if (statusCode != HttpStatus.OK_200) { + logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason()); + throw new HubProcessingException(String.format("HTTP %d error", statusCode)); } - if (!response.hasEntity()) { + String jsonResponse = response.getContentAsString(); + if ("".equals(jsonResponse)) { logger.warn("Hub returned no content"); - response.close(); - throw new ProcessingException("Missing response entity"); + throw new HubProcessingException("Missing response entity"); } - String jsonResponse = response.readEntity(String.class); if (logger.isTraceEnabled()) { logger.trace("JSON response = {}", jsonResponse); } @@ -195,15 +219,16 @@ private synchronized String invoke(Invocation invocation, WebTarget target, @Nul /** * Fetches a JSON package that describes a specific shade in the hub, and wraps it * in a Shade class instance - * + * * @param shadeId id of the shade to be fetched * @return Shade class instance - * @throws ProcessingException if there is any processing error + * @throws JsonParseException if there is a JSON parsing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public @Nullable Shade getShade(int shadeId) throws ProcessingException, HubMaintenanceException { - WebTarget target = shade.resolveTemplate(ID, shadeId); - String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null); + public @Nullable Shade getShade(int shadeId) + throws JsonParseException, HubProcessingException, HubMaintenanceException { + String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null); return gson.fromJson(json, Shade.class); } @@ -211,30 +236,29 @@ private synchronized String invoke(Invocation invocation, WebTarget target, @Nul * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on * a specific shade; fetches a JSON package that describes that shade, and wraps * it in a Shade class instance - * + * * @param shadeId id of the shade to be refreshed * @return Shade class instance - * @throws ProcessingException if there is any processing error + * @throws JsonParseException if there is a JSON parsing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public @Nullable Shade refreshShade(int shadeId) throws ProcessingException, HubMaintenanceException { - WebTarget target = shade.resolveTemplate(ID, shadeId).queryParam(REFRESH, true); - String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null); + public @Nullable Shade refreshShade(int shadeId) + throws JsonParseException, HubProcessingException, HubMaintenanceException { + String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), + Query.of("refresh", Boolean.toString(true)), null); return gson.fromJson(json, Shade.class); } /** * Tells the hub to stop movement of a specific shade - * + * * @param shadeId id of the shade to be stopped - * @throws ProcessingException if there is any processing error + * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance */ - public void stopShade(int shadeId) throws ProcessingException, HubMaintenanceException { - WebTarget target = shade.resolveTemplate(ID, shadeId); + public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException { String json = gson.toJson(new ShadeStop(shadeId)); - invoke(target.request().header(CONN_HDR, CONN_VAL) - .buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json); - return; + invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json); } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HubProcessingException.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HubProcessingException.java new file mode 100644 index 0000000000000..87594d1af770d --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HubProcessingException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.hdpowerview.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HubProcessingException} is a custom exception for the HD PowerView hub + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class HubProcessingException extends Exception { + + private static final long serialVersionUID = 4307088023775166450L; + + public HubProcessingException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java index e733ff3c41eca..ad4ad4e390cf5 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java @@ -17,13 +17,12 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.ws.rs.ProcessingException; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; +import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.api.responses.Shades; import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; @@ -85,7 +84,7 @@ private Runnable createScanner() { try { HDPowerViewWebTargets webTargets = hub.getWebTargets(); if (webTargets == null) { - throw new ProcessingException("Web targets not initialized"); + throw new HubProcessingException("Web targets not initialized"); } Shades shades = webTargets.getShades(); if (shades != null && shades.shadeData != null) { @@ -107,7 +106,7 @@ private Runnable createScanner() { } } } - } catch (ProcessingException | JsonParseException e) { + } catch (HubProcessingException | JsonParseException e) { logger.warn("Unexpected error: {}", e.getMessage()); } catch (HubMaintenanceException e) { // exceptions are logged in HDPowerViewWebTargets diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java index b67d4a20d6755..0827a682e31e4 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java @@ -21,13 +21,14 @@ import java.util.concurrent.TimeUnit; import javax.ws.rs.ProcessingException; -import javax.ws.rs.client.ClientBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; +import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; import org.openhab.binding.hdpowerview.internal.api.responses.Shades; @@ -63,7 +64,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class); - private final ClientBuilder clientBuilder; + private final HttpClient httpClient; private long refreshInterval; private long hardRefreshInterval; @@ -75,9 +76,9 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE); - public HDPowerViewHubHandler(Bridge bridge, ClientBuilder clientBuilder) { + public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) { super(bridge); - this.clientBuilder = clientBuilder; + this.httpClient = httpClient; } @Override @@ -98,7 +99,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { webTargets.activateScene(Integer.parseInt(channelUID.getId())); } catch (HubMaintenanceException e) { // exceptions are logged in HDPowerViewWebTargets - } catch (NumberFormatException | ProcessingException e) { + } catch (NumberFormatException | HubProcessingException e) { logger.debug("Unexpected error {}", e.getMessage()); } } @@ -116,7 +117,7 @@ public void initialize() { return; } - webTargets = new HDPowerViewWebTargets(clientBuilder.build(), host); + webTargets = new HDPowerViewWebTargets(httpClient, host); refreshInterval = config.refresh; hardRefreshInterval = config.hardRefresh; schedulePoll(); @@ -178,7 +179,7 @@ private synchronized void poll() { pollScenes(); } catch (JsonParseException e) { logger.warn("Bridge returned a bad JSON response: {}", e.getMessage()); - } catch (ProcessingException e) { + } catch (HubProcessingException e) { logger.warn("Error connecting to bridge: {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage()); } catch (HubMaintenanceException e) { @@ -186,7 +187,7 @@ private synchronized void poll() { } } - private void pollShades() throws JsonParseException, ProcessingException, HubMaintenanceException { + private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException { HDPowerViewWebTargets webTargets = this.webTargets; if (webTargets == null) { throw new ProcessingException("Web targets not initialized"); @@ -229,7 +230,7 @@ private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData s thingHandler.onReceiveUpdate(shadeData); } - private void pollScenes() throws JsonParseException, ProcessingException, HubMaintenanceException { + private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException { HDPowerViewWebTargets webTargets = this.webTargets; if (webTargets == null) { throw new ProcessingException("Web targets not initialized"); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java index 5bbf5fa294c0d..3856de9d7561f 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java @@ -19,12 +19,11 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.ws.rs.ProcessingException; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; +import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.api.ActuatorClass; import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; import org.openhab.binding.hdpowerview.internal.api.ShadePosition; @@ -128,7 +127,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Update the state of the channels based on the ShadeData provided - * + * * @param shadeData the ShadeData to be used; may be null */ protected void onReceiveUpdate(@Nullable ShadeData shadeData) { @@ -157,11 +156,11 @@ private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, i try { HDPowerViewHubHandler bridge; if ((bridge = getBridgeHandler()) == null) { - throw new ProcessingException("Missing bridge handler"); + throw new HubProcessingException("Missing bridge handler"); } HDPowerViewWebTargets webTargets = bridge.getWebTargets(); if (webTargets == null) { - throw new ProcessingException("Web targets not initialized"); + throw new HubProcessingException("Web targets not initialized"); } int shadeId = getShadeId(); @@ -190,7 +189,7 @@ private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, i webTargets.moveShade(shadeId, ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent)); } - } catch (ProcessingException | NumberFormatException e) { + } catch (HubProcessingException | NumberFormatException e) { logger.warn("Unexpected error: {}", e.getMessage()); return; } catch (HubMaintenanceException e) { @@ -211,16 +210,16 @@ private void stopShade() { try { HDPowerViewHubHandler bridge; if ((bridge = getBridgeHandler()) == null) { - throw new ProcessingException("Missing bridge handler"); + throw new HubProcessingException("Missing bridge handler"); } HDPowerViewWebTargets webTargets = bridge.getWebTargets(); if (webTargets == null) { - throw new ProcessingException("Web targets not initialized"); + throw new HubProcessingException("Web targets not initialized"); } int shadeId = getShadeId(); webTargets.stopShade(shadeId); requestRefreshShade(); - } catch (ProcessingException | NumberFormatException e) { + } catch (HubProcessingException | NumberFormatException e) { logger.warn("Unexpected error: {}", e.getMessage()); return; } catch (HubMaintenanceException e) { @@ -242,11 +241,11 @@ private void doRefreshShade() { try { HDPowerViewHubHandler bridge; if ((bridge = getBridgeHandler()) == null) { - throw new ProcessingException("Missing bridge handler"); + throw new HubProcessingException("Missing bridge handler"); } HDPowerViewWebTargets webTargets = bridge.getWebTargets(); if (webTargets == null) { - throw new ProcessingException("Web targets not initialized"); + throw new HubProcessingException("Web targets not initialized"); } int shadeId = getShadeId(); Shade shade = webTargets.refreshShade(shadeId); @@ -258,7 +257,7 @@ private void doRefreshShade() { } } } - } catch (ProcessingException | NumberFormatException e) { + } catch (HubProcessingException | NumberFormatException e) { logger.warn("Unexpected error: {}", e.getMessage()); } catch (HubMaintenanceException e) { // exceptions are logged in HDPowerViewWebTargets diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java index cb0d41910896a..7f6891dd3d281 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java @@ -22,15 +22,13 @@ import java.util.List; import java.util.regex.Pattern; -import javax.ws.rs.ProcessingException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.junit.jupiter.api.Test; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; +import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; @@ -76,7 +74,7 @@ private String loadJson(String fileName) { /** * Run a series of ONLINE tests on the communication with a hub - * + * * @param hubIPAddress must be a valid hub IP address to run the * tests on; or an INVALID IP address to * suppress the tests @@ -99,10 +97,18 @@ public void testOnlineCommunication() { boolean allowShadeMovementCommands = false; if (VALID_IP_V4_ADDRESS.matcher(hubIPAddress).matches()) { - // initialize stuff - Client client = ClientBuilder.newClient(); + // ==== initialize stuff ==== + HttpClient client = new HttpClient(); assertNotNull(client); - // client.register(new Logger()); + + // ==== start the client ==== + try { + client.start(); + assertTrue(client.isStarted()); + } catch (Exception e) { + fail(e.getMessage()); + } + HDPowerViewWebTargets webTargets = new HDPowerViewWebTargets(client, hubIPAddress); assertNotNull(webTargets); @@ -180,7 +186,7 @@ public void testOnlineCommunication() { String shadeName = shadexData.getName(); assertNotNull(shadeName); } - } catch (JsonParseException | ProcessingException | HubMaintenanceException e) { + } catch (JsonParseException | HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); } @@ -203,7 +209,7 @@ public void testOnlineCommunication() { String sceneName = scene.getName(); assertNotNull(sceneName); } - } catch (JsonParseException | ProcessingException | HubMaintenanceException e) { + } catch (JsonParseException | HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); } @@ -214,7 +220,7 @@ public void testOnlineCommunication() { assertNotEquals(0, shadeId); shade = webTargets.refreshShade(shadeId); assertNotNull(shade); - } catch (ProcessingException | HubMaintenanceException e) { + } catch (HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); } @@ -245,7 +251,7 @@ public void testOnlineCommunication() { if (allowShadeMovementCommands) { webTargets.moveShade(shadeId, newPos); } - } catch (ProcessingException | HubMaintenanceException e) { + } catch (HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); } @@ -254,7 +260,26 @@ public void testOnlineCommunication() { try { assertNotNull(sceneId); webTargets.activateScene(sceneId); - } catch (ProcessingException | HubMaintenanceException e) { + } catch (HubProcessingException | HubMaintenanceException e) { + fail(e.getMessage()); + } + } + + // ==== test stop command ==== + if (allowShadeMovementCommands) { + try { + assertNotNull(sceneId); + webTargets.stopShade(shadeId); + } catch (HubProcessingException | HubMaintenanceException e) { + fail(e.getMessage()); + } + } + + // ==== stop the client ==== + if (client.isRunning()) { + try { + client.stop(); + } catch (Exception e) { fail(e.getMessage()); } } diff --git a/bundles/org.openhab.binding.helios/src/main/java/org/openhab/binding/helios/internal/handler/HeliosHandler221.java b/bundles/org.openhab.binding.helios/src/main/java/org/openhab/binding/helios/internal/handler/HeliosHandler221.java index cca5cbf13353a..1b3d10b6cef67 100644 --- a/bundles/org.openhab.binding.helios/src/main/java/org/openhab/binding/helios/internal/handler/HeliosHandler221.java +++ b/bundles/org.openhab.binding.helios/src/main/java/org/openhab/binding/helios/internal/handler/HeliosHandler221.java @@ -130,7 +130,6 @@ public class HeliosHandler221 extends BaseThingHandler { private String ipAddress; // JSON variables - private JsonParser parser = new JsonParser(); private Gson gson = new Gson(); private ScheduledFuture logJob; @@ -203,7 +202,7 @@ public boolean verify(String hostname, javax.net.ssl.SSLSession sslSession) { return; } - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("initialize() Request : {}", systemTarget.resolveTemplate("ip", ipAddress) @@ -303,7 +302,7 @@ private long subscribe() { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("subscribe() Request : {}", @@ -365,7 +364,7 @@ private void unsubscribe() { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("unsubscribe() Request : {}", @@ -426,7 +425,7 @@ private List pullLog(long logSubscriptionID) { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("pullLog() Request : {}", @@ -488,7 +487,7 @@ private List getSwitches() { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("getSwitches() Request : {}", switchTarget.resolveTemplate("ip", ipAddress) @@ -506,7 +505,7 @@ private List getSwitches() { getThing().getUID().toString()); String result = jsonObject.get("result").toString(); result = result.replace("switch", "id"); - JsonObject js = parser.parse(result).getAsJsonObject(); + JsonObject js = JsonParser.parseString(result).getAsJsonObject(); RESTSwitch[] switchArray = gson.fromJson(js.getAsJsonArray("ides"), RESTSwitch[].class); if (switchArray != null) { return Arrays.asList(switchArray); @@ -554,7 +553,7 @@ private void triggerSwitch(String id) { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("triggerSwitch() Request : {}", @@ -609,7 +608,7 @@ private void enableSwitch(String id, boolean flag) { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("enableSwitch() Request : {}", @@ -664,7 +663,7 @@ private List getPorts() { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("getPorts() Request : {}", portTarget.resolveTemplate("ip", ipAddress) @@ -732,7 +731,7 @@ private List getPorts() { } if (response != null) { - JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); if (logger.isTraceEnabled()) { logger.trace("configureRunnable Request : {}", systemTarget.resolveTemplate("ip", ipAddress) diff --git a/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/api/HeosFacade.java b/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/api/HeosFacade.java index 221783e907ccc..0aeecce3c3c89 100644 --- a/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/api/HeosFacade.java +++ b/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/api/HeosFacade.java @@ -23,7 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.jetbrains.annotations.NotNull; import org.openhab.binding.heos.internal.json.dto.HeosResponseObject; import org.openhab.binding.heos.internal.json.payload.BrowseResult; import org.openhab.binding.heos.internal.json.payload.Group; @@ -69,7 +68,6 @@ public List getPlaylists() throws IOException, ReadException { return getBrowseResults(PLAYLISTS_SID); } - @NotNull private List getBrowseResults(String sourceIdentifier) throws IOException, ReadException { HeosResponseObject response = browseSource(sourceIdentifier); logger.debug("Response: {}", response); @@ -353,7 +351,7 @@ public HeosResponseObject logIn(String name, String password) throws IOExc /** * Get all the players known by HEOS - * + * * @return */ public HeosResponseObject getPlayers() throws IOException, ReadException { @@ -362,7 +360,7 @@ public HeosResponseObject getPlayers() throws IOException, ReadExcepti /** * Get all the groups known by HEOS - * + * * @return */ public HeosResponseObject getGroups() throws IOException, ReadException { diff --git a/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/handler/HeosThingBaseHandler.java b/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/handler/HeosThingBaseHandler.java index ce625122fb269..afa7ea927e03b 100644 --- a/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/handler/HeosThingBaseHandler.java +++ b/bundles/org.openhab.binding.heos/src/main/java/org/openhab/binding/heos/internal/handler/HeosThingBaseHandler.java @@ -14,9 +14,9 @@ import static org.openhab.binding.heos.internal.HeosBindingConstants.*; import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel; -import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.GROUP; -import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.PLAYER; +import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.*; import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*; +import static org.openhab.binding.heos.internal.resources.HeosConstants.*; import static org.openhab.core.thing.ThingStatus.*; import java.io.IOException; @@ -30,7 +30,6 @@ import javax.measure.quantity.Time; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.heos.internal.HeosChannelHandlerFactory; @@ -38,15 +37,31 @@ import org.openhab.binding.heos.internal.exception.HeosFunctionalException; import org.openhab.binding.heos.internal.exception.HeosNotConnectedException; import org.openhab.binding.heos.internal.exception.HeosNotFoundException; -import org.openhab.binding.heos.internal.json.dto.*; +import org.openhab.binding.heos.internal.json.dto.HeosCommandTuple; +import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute; +import org.openhab.binding.heos.internal.json.dto.HeosError; +import org.openhab.binding.heos.internal.json.dto.HeosEvent; +import org.openhab.binding.heos.internal.json.dto.HeosEventObject; +import org.openhab.binding.heos.internal.json.dto.HeosObject; +import org.openhab.binding.heos.internal.json.dto.HeosResponseObject; import org.openhab.binding.heos.internal.json.payload.Media; import org.openhab.binding.heos.internal.json.payload.Player; import org.openhab.binding.heos.internal.resources.HeosEventListener; import org.openhab.binding.heos.internal.resources.Telnet.ReadException; import org.openhab.core.io.net.http.HttpUtil; -import org.openhab.core.library.types.*; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; import org.openhab.core.library.unit.Units; -import org.openhab.core.thing.*; +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.BaseThingHandler; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; @@ -474,16 +489,17 @@ protected void handleThingMediaUpdate(Media info) { } private void handleImageUrl(Media info) { - if (StringUtils.isNotBlank(info.imageUrl)) { + String imageUrl = info.imageUrl; + if (imageUrl != null && !imageUrl.isBlank()) { try { - URL url = new URL(info.imageUrl); // checks if String is proper URL + URL url = new URL(imageUrl); // checks if String is proper URL RawType cover = HttpUtil.downloadImage(url.toString()); if (cover != null) { updateState(CH_ID_COVER, cover); return; } } catch (MalformedURLException e) { - logger.debug("Cover can't be loaded. No proper URL: {}", info.imageUrl, e); + logger.debug("Cover can't be loaded. No proper URL: {}", imageUrl, e); } } updateState(CH_ID_COVER, UnDefType.NULL); diff --git a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosGroup.xml b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosGroup.xml index 14b79d4ae90d2..a7017db856d0b 100644 --- a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosGroup.xml +++ b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosGroup.xml @@ -36,10 +36,9 @@ Denon - + Shows the player IDs of the members of this group - true diff --git a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosPlayer.xml b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosPlayer.xml index 1317baf7a76a3..f4180c3e26515 100644 --- a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosPlayer.xml +++ b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosPlayer.xml @@ -38,10 +38,9 @@ serialNumber - + The internal Player ID - true diff --git a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosSystem.xml b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosSystem.xml index 30a5e14cf126c..a5a5725c1d7c8 100644 --- a/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosSystem.xml +++ b/bundles/org.openhab.binding.heos/src/main/resources/OH-INF/thing/HeosSystem.xml @@ -13,25 +13,21 @@ vendor - + network-address Network address of the HEOS bridge. - true - + Username for login to the HEOS account. - false - + password Password for login to the HEOS account - false - - false + seconds The time in seconds for the HEOS Heartbeat (default = 60 s) diff --git a/bundles/org.openhab.binding.homematic/README.md b/bundles/org.openhab.binding.homematic/README.md index 7eaf099bc175b..7d2e06ba52d8a 100644 --- a/bundles/org.openhab.binding.homematic/README.md +++ b/bundles/org.openhab.binding.homematic/README.md @@ -130,9 +130,6 @@ Hint for the binding to identify the gateway type (auto|ccu|noccu) (default = "a - **callbackHost** Callback network address of the system runtime, default is auto-discovery -- **bindAddress** -The address the XML-/BINRPC server binds to, default is value of "callbackHost" - - **xmlCallbackPort** Callback port of the binding's XML-RPC server, default is 9125 and counts up for each additional bridge @@ -289,10 +286,12 @@ Dimmer Light "Light [%d %%]" { channel="homematic:HM-LC-Dim1T-Pl-2:cc The GATEWAY-EXTRAS is a virtual device which contains a switch to reload all values from all devices and also a switch to put the gateway in the install mode to add new devices. If the gateway supports variables and scripts, you can handle them with this device too. + The type is generated: `GATEWAY-EXTRAS-[BRIDGE_ID]`. +Example: bridgeId=**ccu** -> type=GATEWAY-EXTRAS-**CCU** -**Example:** bridgeId=ccu, type=GATEWAY-EXTRAS-CCU -Address: fixed GWE00000000 +The address of the virtual device must be the default value `GWE00000000`. +Usage of a custom ID is not supported. ### RELOAD_ALL_FROM_GATEWAY diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/common/HomematicConfig.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/common/HomematicConfig.java index d90cc66addb26..9936668541d13 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/common/HomematicConfig.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/common/HomematicConfig.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.homematic.internal.common; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmGatewayInfo; import org.openhab.binding.homematic.internal.model.HmInterface; @@ -48,7 +46,6 @@ public class HomematicConfig { private int groupPort; private String callbackHost; - private String bindAddress; private int xmlCallbackPort; private int binCallbackPort; @@ -90,30 +87,6 @@ public void setCallbackHost(String callbackHost) { this.callbackHost = callbackHost; } - /** - * Returns the bind address. - */ - public String getBindAddress() { - return bindAddress; - } - - /** - * Sets the bind address. - */ - public void setBindAddress(String bindAddress) { - this.bindAddress = bindAddress; - } - - /** - * Sets the callback host port. - * - * @deprecated use setBinCallbackPort - */ - @Deprecated - public void setCallbackPort(int callbackPort) { - this.binCallbackPort = callbackPort; - } - /** * Returns the XML-RPC callback host port. */ @@ -405,14 +378,12 @@ public boolean isNoCCUType() { @Override public String toString() { - ToStringBuilder tsb = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); - tsb.append("gatewayAddress", gatewayAddress).append("callbackHost", callbackHost) - .append("bindAddress", bindAddress).append("xmlCallbackPort", xmlCallbackPort) - .append("binCallbackPort", binCallbackPort).append("gatewayType", gatewayType) - .append("rfPort", getRfPort()).append("wiredPort", getWiredPort()).append("hmIpPort", getHmIpPort()) - .append("cuxdPort", getCuxdPort()).append("groupPort", getGroupPort()).append("timeout", timeout) - .append("discoveryTimeToLive", discoveryTimeToLive).append("installModeDuration", installModeDuration) - .append("socketMaxAlive", socketMaxAlive); - return tsb.toString(); + return String.format( + "%s[gatewayAddress=%s,callbackHost=%s,xmlCallbackPort=%d,binCallbackPort=%d," + + "gatewayType=%s,rfPort=%d,wiredPort=%d,hmIpPort=%d,cuxdPort=%d,groupPort=%d,timeout=%d," + + "discoveryTimeToLive=%d,installModeDuration=%d,socketMaxAlive=%d]", + getClass().getSimpleName(), gatewayAddress, callbackHost, xmlCallbackPort, binCallbackPort, gatewayType, + getRfPort(), getWiredPort(), getHmIpPort(), getCuxdPort(), getGroupPort(), timeout, discoveryTimeToLive, + installModeDuration, socketMaxAlive); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/AbstractHomematicGateway.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/AbstractHomematicGateway.java index cf4d59f5b09b7..d834ec1f89f76 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/AbstractHomematicGateway.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/AbstractHomematicGateway.java @@ -29,7 +29,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.homematic.internal.common.HomematicConfig; import org.openhab.binding.homematic.internal.communicator.client.BinRpcClient; @@ -800,7 +799,7 @@ public HomematicGatewayAdapter getGatewayAdapter() { * Creates a virtual device for handling variables, scripts and other special gateway functions. */ private HmDevice createGatewayDevice() { - String type = String.format("%s-%s", HmDevice.TYPE_GATEWAY_EXTRAS, StringUtils.upperCase(id)); + String type = String.format("%s-%s", HmDevice.TYPE_GATEWAY_EXTRAS, id.toUpperCase()); HmDevice device = new HmDevice(HmDevice.ADDRESS_GATEWAY_EXTRAS, getDefaultInterface(), type, config.getGatewayInfo().getId(), null, null); device.setName(HmDevice.TYPE_GATEWAY_EXTRAS); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/CcuGateway.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/CcuGateway.java index 7e3835611cab8..5810194cd420e 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/CcuGateway.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/CcuGateway.java @@ -18,10 +18,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.util.StringContentProvider; @@ -123,7 +122,7 @@ protected void setChannelDatapointValues(HmChannel channel, HmParamsetType param HmDevice device = channel.getDevice(); String channelName = String.format("%s.%s:%s.", device.getHmInterface().getName(), device.getAddress(), channel.getNumber()); - String datapointNames = StringUtils.join(dpNames.toArray(), "\\t"); + String datapointNames = String.join("\\t", dpNames); TclScriptDataList resultList = sendScriptByName("getAllChannelValues", TclScriptDataList.class, new String[] { "channel_name", "datapoint_names" }, new String[] { channelName, datapointNames }); @@ -151,7 +150,7 @@ protected void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetTy @Override protected void setVariable(HmDatapoint dp, Object value) throws IOException { - String strValue = StringUtils.replace(ObjectUtils.toString(value), "\"", "\\\""); + String strValue = Objects.toString(value, "").replace("\"", "\\\""); if (dp.isStringType()) { strValue = "\"" + strValue + "\""; } @@ -184,8 +183,10 @@ private T sendScriptByName(String scriptName, Class clazz) throws IOExcep private T sendScriptByName(String scriptName, Class clazz, String[] variableNames, String[] values) throws IOException { String script = tclregaScripts.get(scriptName); - for (int i = 0; i < variableNames.length; i++) { - script = StringUtils.replace(script, "{" + variableNames[i] + "}", values[i]); + if (script != null) { + for (int i = 0; i < variableNames.length; i++) { + script = script.replace("{" + variableNames[i] + "}", values[i]); + } } return sendScript(script, clazz); } @@ -196,8 +197,8 @@ private T sendScriptByName(String scriptName, Class clazz, String[] varia @SuppressWarnings("unchecked") private synchronized T sendScript(String script, Class clazz) throws IOException { try { - script = StringUtils.trim(script); - if (StringUtils.isEmpty(script)) { + script = script == null ? null : script.trim(); + if (script == null || script.isEmpty()) { throw new RuntimeException("Homematic TclRegaScript is empty!"); } if (logger.isTraceEnabled()) { @@ -210,7 +211,10 @@ private synchronized T sendScript(String script, Class clazz) throws IOEx .header(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + config.getEncoding()).send(); String result = new String(response.getContent(), config.getEncoding()); - result = StringUtils.substringBeforeLast(result, ""); + int lastPos = result.lastIndexOf(""); + if (lastPos != -1) { + result = result.substring(0, lastPos); + } if (logger.isTraceEnabled()) { logger.trace("Result TclRegaScript: {}", result); } @@ -231,7 +235,8 @@ private Map loadTclRegaScripts() throws IOException { Map result = new HashMap<>(); if (scriptList.getScripts() != null) { for (TclScript script : scriptList.getScripts()) { - result.put(script.name, StringUtils.trimToNull(script.data)); + String value = script.data.trim(); + result.put(script.name, value.isEmpty() ? null : value); } } return result; diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/client/RpcClient.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/client/RpcClient.java index 244c1891c4707..4f2fab2c823c9 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/client/RpcClient.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/client/RpcClient.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.HomematicBindingConstants; import org.openhab.binding.homematic.internal.common.HomematicConfig; import org.openhab.binding.homematic.internal.communicator.message.RpcRequest; @@ -34,6 +33,7 @@ import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser; import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser; import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser; +import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDevice; @@ -233,7 +233,7 @@ public HmGatewayInfo getGatewayInfo(String id) throws IOException { try { ddParser = getDeviceDescription(HmInterface.RF); - isHomegear = StringUtils.equalsIgnoreCase(ddParser.getType(), "Homegear"); + isHomegear = "Homegear".equalsIgnoreCase(ddParser.getType()); } catch (IOException ex) { // can't load gateway infos via RF interface ddParser = new GetDeviceDescriptionParser(); @@ -247,21 +247,23 @@ public HmGatewayInfo getGatewayInfo(String id) throws IOException { HmGatewayInfo gatewayInfo = new HmGatewayInfo(); gatewayInfo.setAddress(biParser.getGatewayAddress()); + String gwType = biParser.getType(); if (isHomegear) { gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR); gatewayInfo.setType(ddParser.getType()); gatewayInfo.setFirmware(ddParser.getFirmware()); - } else if ((StringUtils.startsWithIgnoreCase(biParser.getType(), "CCU") - || StringUtils.startsWithIgnoreCase(biParser.getType(), "HMIP_CCU") - || StringUtils.startsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType()) + } else if ((MiscUtils.strStartsWithIgnoreCase(gwType, "CCU") + || MiscUtils.strStartsWithIgnoreCase(gwType, "HMIP_CCU") + || MiscUtils.strStartsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType()) && !config.isNoCCUType()) { gatewayInfo.setId(HmGatewayInfo.ID_CCU); - String type = StringUtils.isBlank(biParser.getType()) ? "CCU" : biParser.getType(); + String type = gwType.isBlank() ? "CCU" : gwType; gatewayInfo.setType(type); - gatewayInfo.setFirmware(ddParser.getFirmware() != null ? ddParser.getFirmware() : biParser.getFirmware()); + gatewayInfo + .setFirmware(!ddParser.getFirmware().isEmpty() ? ddParser.getFirmware() : biParser.getFirmware()); } else { gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT); - gatewayInfo.setType(biParser.getType()); + gatewayInfo.setType(gwType); gatewayInfo.setFirmware(biParser.getFirmware()); } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/BinRpcMessage.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/BinRpcMessage.java index 8f7b3b36849e8..a94f38d2d4490 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/BinRpcMessage.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/BinRpcMessage.java @@ -29,7 +29,6 @@ import java.util.Map; import java.util.TreeMap; -import org.apache.commons.lang.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,7 +83,7 @@ public BinRpcMessage(InputStream is, boolean methodHeader, String encoding) thro if (length != 4) { throw new EOFException("Only " + length + " bytes received reading message length"); } - int datasize = (new BigInteger(ArrayUtils.subarray(sig, 4, 8))).intValue(); + int datasize = (new BigInteger(Arrays.copyOfRange(sig, 4, 8))).intValue(); byte payload[] = new byte[datasize]; int offset = 0; int currentLength; @@ -96,7 +95,10 @@ public BinRpcMessage(InputStream is, boolean methodHeader, String encoding) thro throw new EOFException("Only " + offset + " bytes received while reading message payload, expected " + datasize + " bytes"); } - byte[] message = ArrayUtils.addAll(sig, payload); + byte[] message = new byte[sig.length + payload.length]; + System.arraycopy(sig, 0, message, 0, sig.length); + System.arraycopy(payload, 0, message, sig.length, payload.length); + decodeMessage(message, methodHeader); } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/XmlRpcRequest.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/XmlRpcRequest.java index 43ea90b47c727..874ed7d560701 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/XmlRpcRequest.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/message/XmlRpcRequest.java @@ -23,8 +23,6 @@ import java.util.Map; import java.util.Map.Entry; -import org.apache.commons.lang.StringEscapeUtils; - /** * A XML-RPC request for sending data to the Homematic server. * @@ -124,7 +122,7 @@ private void generateValue(Object value) { } else { Class clazz = value.getClass(); if (clazz == String.class || clazz == Character.class) { - sb.append(StringEscapeUtils.escapeXml(value.toString())); + sb.append(escapeXml(value.toString())); } else if (clazz == Long.class || clazz == Integer.class || clazz == Short.class || clazz == Byte.class) { tag("int", value.toString()); } else if (clazz == Double.class) { @@ -176,4 +174,30 @@ private void generateValue(Object value) { } } } + + private StringBuilder escapeXml(String inValue) { + StringBuilder outValue = new StringBuilder(inValue.length()); + for (int i = 0; i < inValue.length(); i++) { + switch (inValue.charAt(i)) { + case '<': + outValue.append("<"); + break; + case '>': + outValue.append(">"); + break; + case '&': + outValue.append("&"); + break; + case '\'': + outValue.append("&apost;"); + break; + case '"': + outValue.append("""); + break; + default: + outValue.append(inValue.charAt(i)); + } + } + return outValue; + } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuParamsetDescriptionParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuParamsetDescriptionParser.java index ec35af77bdb21..84badc29fefe5 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuParamsetDescriptionParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuParamsetDescriptionParser.java @@ -14,7 +14,6 @@ import java.io.IOException; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmInterface; @@ -52,7 +51,7 @@ public Void parse(TclScriptDataList resultList) throws IOException { } private String[] toOptionList(String options) { - String[] result = StringUtils.splitByWholeSeparatorPreserveAllTokens(options, ";"); + String[] result = options == null ? null : options.split(";"); return result == null || result.length == 0 ? null : result; } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuVariablesAndScriptsParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuVariablesAndScriptsParser.java index 4795120a3422c..27d5dc559494d 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuVariablesAndScriptsParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CcuVariablesAndScriptsParser.java @@ -14,7 +14,6 @@ import java.io.IOException; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmParamsetType; @@ -51,14 +50,13 @@ public Void parse(TclScriptDataList resultList) throws IOException { if (dp.isIntegerType()) { dp.setMinValue(toInteger(entry.minValue)); dp.setMaxValue(toInteger(entry.maxValue)); - } else { + } else if (dp.isFloatType()) { dp.setMinValue(toDouble(entry.minValue)); dp.setMaxValue(toDouble(entry.maxValue)); } dp.setReadOnly(entry.readOnly); dp.setUnit(entry.unit); - - String[] result = StringUtils.splitByWholeSeparatorPreserveAllTokens(entry.options, ";"); + String[] result = entry.options == null ? null : entry.options.split(";"); dp.setOptions(result == null || result.length == 0 ? null : result); if (dp.getOptions() != null) { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java index aa87ae4e2cf0f..1c2179e428177 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java @@ -13,11 +13,10 @@ package org.openhab.binding.homematic.internal.communicator.parser; import java.io.IOException; +import java.util.Objects; -import org.apache.commons.lang.BooleanUtils; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.math.NumberUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.homematic.internal.misc.HomematicConstants; import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmParamsetType; @@ -38,7 +37,8 @@ public abstract class CommonRpcParser implements RpcParser { * Converts the object to a string. */ protected String toString(Object object) { - return StringUtils.trimToNull(ObjectUtils.toString(object)); + String value = Objects.toString(object, "").trim(); + return value.isEmpty() ? null : value; } /** @@ -49,7 +49,7 @@ protected Integer toInteger(Object object) { return (Integer) object; } try { - return Double.valueOf(ObjectUtils.toString(object)).intValue(); + return Double.valueOf(object.toString()).intValue(); } catch (NumberFormatException ex) { logger.debug("Failed converting {} to a Double", object, ex); return null; @@ -64,7 +64,7 @@ protected Double toDouble(Object object) { return (Double) object; } try { - return Double.valueOf(ObjectUtils.toString(object)); + return Double.valueOf(object.toString()); } catch (NumberFormatException ex) { logger.debug("Failed converting {} to a Double", object, ex); return null; @@ -79,7 +79,12 @@ protected Number toNumber(Object object) { return (Number) object; } try { - return NumberUtils.createNumber(ObjectUtils.toString(object)); + String value = object.toString(); + if (value.contains(".")) { + return Float.parseFloat(value); + } else { + return Integer.parseInt(value); + } } catch (NumberFormatException ex) { logger.debug("Failed converting {} to a Number", object, ex); return null; @@ -93,7 +98,7 @@ protected Boolean toBoolean(Object object) { if (object == null || object instanceof Boolean) { return (Boolean) object; } - return BooleanUtils.toBoolean(ObjectUtils.toString(object)); + return "true".equals(object.toString().toLowerCase()); } /** @@ -114,9 +119,10 @@ protected String[] toOptionList(Object optionList) { /** * Returns the address of a device, replacing group address identifier and illegal characters. */ + @NonNull protected String getSanitizedAddress(Object object) { - String address = StringUtils.trimToNull(StringUtils.replaceOnce(toString(object), "*", "T-")); - return MiscUtils.validateCharacters(address, "Address", "_"); + String address = Objects.toString(object, "").trim().replaceFirst("\\*", "T-"); + return MiscUtils.validateCharacters(address.isEmpty() ? null : address, "Address", "_"); } /** @@ -167,10 +173,16 @@ protected HmDatapoint assembleDatapoint(String name, String unit, String type, S HmDatapoint dp = new HmDatapoint(); dp.setName(name); dp.setDescription(name); - dp.setUnit(StringUtils.replace(StringUtils.trimToNull(unit), "\ufffd", "°")); - if (dp.getUnit() == null && StringUtils.startsWith(dp.getName(), "RSSI_")) { + if (unit != null) { + unit = unit.trim().replace("\ufffd", "°"); + } + dp.setUnit(unit == null || unit.isEmpty() ? null : unit); + if (dp.getUnit() == null && dp.getName() != null && dp.getName().startsWith("RSSI_")) { dp.setUnit("dBm"); } + // Bypass: For at least one device the CCU does not send a unit together with the value + if (dp.getUnit() == null && dp.getName().startsWith(HomematicConstants.DATAPOINT_NAME_OPERATING_VOLTAGE)) + dp.setUnit("V"); HmValueType valueType = HmValueType.parse(type); if (valueType == null || valueType == HmValueType.UNKNOWN) { @@ -210,7 +222,7 @@ protected HmDatapoint assembleDatapoint(String name, String unit, String type, S * Converts a string value to the type. */ protected Object convertToType(String value) { - if (StringUtils.isBlank(value)) { + if (value == null || value.isBlank()) { return null; } if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("on")) { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DeleteDevicesParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DeleteDevicesParser.java index d038b78f65797..5a7701b2e09ae 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DeleteDevicesParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DeleteDevicesParser.java @@ -16,7 +16,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang.StringUtils; +import org.openhab.binding.homematic.internal.misc.MiscUtils; /** * Parses a delete device event received from a Homematic gateway. @@ -31,12 +31,9 @@ public List parse(Object[] message) throws IOException { Object[] data = (Object[]) message[1]; for (int i = 0; i < message.length; i++) { String address = getSanitizedAddress(data[i]); - boolean isDevice = !StringUtils.contains(address, ":") - && !StringUtils.startsWithIgnoreCase(address, "BidCos"); - if (isDevice) { + if (MiscUtils.isDevice(address)) { adresses.add(address); } - } } return adresses; diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DisplayOptionsParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DisplayOptionsParser.java index 9ad2bbb92db37..6ee848e4db7f6 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DisplayOptionsParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/DisplayOptionsParser.java @@ -16,10 +16,9 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDatapointInfo; @@ -36,7 +35,7 @@ public class DisplayOptionsParser extends CommonRpcParser { private final Logger logger = LoggerFactory.getLogger(DisplayOptionsParser.class); private static final String[] onOff = new String[] { "ON", "OFF" }; - + private static final int IDX_NOT_FOUND = -1; private HmChannel channel; private String text; private int beep = 0; @@ -50,7 +49,8 @@ public DisplayOptionsParser(HmChannel channel) { @Override public Void parse(Object value) throws IOException { - String optionsString = StringUtils.remove(toString(value), ' '); + String valueString = toString(value); + String optionsString = valueString == null ? null : valueString.replace(" ", ""); if (optionsString != null) { int idxFirstSep = optionsString.indexOf(","); if (idxFirstSep == -1) { @@ -61,7 +61,7 @@ public Void parse(Object value) throws IOException { optionsString = optionsString.substring(idxFirstSep + 1); } - String[] options = StringUtils.split(optionsString, ","); + String[] options = optionsString.split(","); String[] availableSymbols = getAvailableSymbols(channel); String[] availableBeepOptions = getAvailableOptions(channel, DATAPOINT_NAME_BEEP); @@ -87,7 +87,7 @@ public Void parse(Object value) throws IOException { DATAPOINT_NAME_BACKLIGHT, deviceAddress); unit = getIntParameter(availableUnitOptions, unit, parameter, DATAPOINT_NAME_UNIT, deviceAddress); - if (ArrayUtils.contains(availableSymbols, parameter)) { + if (findInArray(availableSymbols, parameter) != IDX_NOT_FOUND) { logger.debug("Symbol '{}' found for remote control '{}'", parameter, deviceAddress); symbols.add(parameter); } @@ -102,8 +102,8 @@ public Void parse(Object value) throws IOException { */ private int getIntParameter(String[] options, int currentValue, String parameter, String parameterName, String deviceAddress) { - int idx = ArrayUtils.indexOf(options, parameter); - if (idx != -1) { + int idx = findInArray(options, parameter); + if (idx != IDX_NOT_FOUND) { if (currentValue == 0) { logger.debug("{} option '{}' found at index {} for remote control '{}'", parameterName, parameter, idx + 1, deviceAddress); @@ -125,10 +125,12 @@ private String[] getAvailableOptions(HmChannel channel, String datapointName) { HmDatapointInfo dpInfo = HmDatapointInfo.createValuesInfo(channel, datapointName); HmDatapoint dp = channel.getDatapoint(dpInfo); if (dp != null) { - String[] options = (String[]) ArrayUtils.remove(dp.getOptions(), 0); + String[] dpOpts = dp.getOptions(); + String[] options = new String[dpOpts.length - 1]; + options = Arrays.copyOfRange(dpOpts, 1, dpOpts.length); for (String onOffString : onOff) { - int onIdx = ArrayUtils.indexOf(options, onOffString); - if (onIdx != -1) { + int onIdx = findInArray(options, onOffString); + if (onIdx != IDX_NOT_FOUND) { options[onIdx] = datapointName + "_" + onOffString; } } @@ -137,6 +139,18 @@ private String[] getAvailableOptions(HmChannel channel, String datapointName) { return new String[0]; } + private int findInArray(String[] arr, String searchString) { + if (arr.length == 0) { + return IDX_NOT_FOUND; + } + for (int i = 0; i < arr.length; i++) { + if (arr[i].equals(searchString)) { + return i; + } + } + return IDX_NOT_FOUND; + } + /** * Returns all possible symbols from the remote control. */ diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/EventParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/EventParser.java index 237bc28373ebd..7cf06ce6863d7 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/EventParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/EventParser.java @@ -14,8 +14,6 @@ import java.io.IOException; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.math.NumberUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapointInfo; import org.openhab.binding.homematic.internal.model.HmDevice; @@ -38,10 +36,11 @@ public HmDatapointInfo parse(Object[] message) throws IOException { address = HmDevice.ADDRESS_GATEWAY_EXTRAS; channel = HmChannel.CHANNEL_NUMBER_VARIABLE; } else { - String[] configParts = StringUtils.trimToEmpty(addressWithChannel).split(":"); + String addrChannel = addressWithChannel == null ? "" : addressWithChannel.trim(); + String[] configParts = addrChannel.split(":"); address = getSanitizedAddress(configParts[0]); if (configParts.length > 1) { - channel = NumberUtils.createInteger(configParts[1]); + channel = configParts[1] == null ? null : Integer.valueOf(configParts[1]); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetAllScriptsParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetAllScriptsParser.java index d33229280b3b8..14b11cb185378 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetAllScriptsParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetAllScriptsParser.java @@ -13,8 +13,8 @@ package org.openhab.binding.homematic.internal.communicator.parser; import java.io.IOException; +import java.util.Objects; -import org.apache.commons.lang.ObjectUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmParamsetType; @@ -36,7 +36,7 @@ public GetAllScriptsParser(HmChannel channel) { public Void parse(Object[] message) throws IOException { message = (Object[]) message[0]; for (int i = 0; i < message.length; i++) { - String scriptName = ObjectUtils.toString(message[i]); + String scriptName = Objects.toString(message[i], ""); HmDatapoint dpScript = new HmDatapoint(scriptName, scriptName, HmValueType.BOOL, Boolean.FALSE, false, HmParamsetType.VALUES); dpScript.setInfo(scriptName); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java index 6c63dd5330166..53446f024cfea 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java @@ -50,7 +50,7 @@ public ListBidcosInterfacesParser parse(Object[] message) throws IOException { * Returns the parsed type. */ public String getType() { - return type; + return type == null ? "" : type; } /** diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListDevicesParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListDevicesParser.java index 41e168f1b0604..fb1ee5cffc35d 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListDevicesParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListDevicesParser.java @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.Map; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.common.HomematicConfig; import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmChannel; @@ -48,9 +47,7 @@ public Collection parse(Object[] message) throws IOException { for (int i = 0; i < message.length; i++) { Map data = (Map) message[i]; - boolean isDevice = !StringUtils.contains(toString(data.get("ADDRESS")), ":"); - - if (isDevice) { + if (MiscUtils.isDevice(toString(data.get("ADDRESS")), true)) { String address = getSanitizedAddress(data.get("ADDRESS")); String type = MiscUtils.validateCharacters(toString(data.get("TYPE")), "Device type", "-"); String id = toString(data.get("ID")); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/NewDevicesParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/NewDevicesParser.java index 92b97056692e9..8ec4bee27115b 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/NewDevicesParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/NewDevicesParser.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.StringUtils; +import org.openhab.binding.homematic.internal.misc.MiscUtils; /** * Parses a new device event received from a Homematic gateway. @@ -35,9 +35,7 @@ public List parse(Object[] message) throws IOException { Map data = (Map) message[i]; String address = toString(data.get("ADDRESS")); - boolean isDevice = !StringUtils.contains(address, ":") - && !StringUtils.startsWithIgnoreCase(address, "BidCos"); - if (isDevice) { + if (MiscUtils.isDevice(address)) { adresses.add(getSanitizedAddress(address)); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/RpcResponseParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/RpcResponseParser.java index 241b48257d7de..ca796488e7c4e 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/RpcResponseParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/RpcResponseParser.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.util.Map; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException; import org.openhab.binding.homematic.internal.communicator.client.UnknownRpcFailureException; import org.openhab.binding.homematic.internal.communicator.message.RpcRequest; @@ -43,9 +42,9 @@ public Object[] parse(Object[] message) throws IOException { Number faultCode = toNumber(map.get("faultCode")); String faultString = toString(map.get("faultString")); String faultMessage = String.format("%s %s (sending %s)", faultCode, faultString, request); - if (faultCode.intValue() == -1 && StringUtils.equals("Failure", faultString)) { + if (faultCode.intValue() == -1 && "Failure".equals(faultString)) { throw new UnknownRpcFailureException(faultMessage); - } else if (faultCode.intValue() == -3 && StringUtils.equals("Unknown paramset", faultString)) { + } else if (faultCode.intValue() == -3 && "Unknown paramset".equals(faultString)) { throw new UnknownParameterSetException(faultMessage); } throw new IOException(faultMessage); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/BinRpcNetworkService.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/BinRpcNetworkService.java index b01817fb2ea65..8d69f3c6f6530 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/BinRpcNetworkService.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/BinRpcNetworkService.java @@ -47,7 +47,7 @@ public BinRpcNetworkService(RpcEventListener listener, HomematicConfig config) t serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true); - serverSocket.bind(new InetSocketAddress(config.getBindAddress(), config.getBinCallbackPort())); + serverSocket.bind(new InetSocketAddress(config.getBinCallbackPort())); this.rpcResponseHandler = new RpcResponseHandler(listener) { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/RpcResponseHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/RpcResponseHandler.java index 431f3e8bae1c2..010dc88df3700 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/RpcResponseHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/RpcResponseHandler.java @@ -18,8 +18,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; -import org.apache.commons.lang.ObjectUtils; import org.openhab.binding.homematic.internal.communicator.message.RpcRequest; import org.openhab.binding.homematic.internal.communicator.parser.DeleteDevicesParser; import org.openhab.binding.homematic.internal.communicator.parser.EventParser; @@ -63,7 +63,7 @@ public T handleMethodCall(String methodName, Object[] responseData) throws IOExc for (Object o : (Object[]) responseData[0]) { Map call = (Map) o; if (call != null) { - String method = ObjectUtils.toString(call.get("methodName")); + String method = Objects.toString(call.get("methodName"), ""); Object[] data = (Object[]) call.get("params"); handleMethodCall(method, data); } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/XmlRpcServer.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/XmlRpcServer.java index 375363561f7ed..a6b2171098f0b 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/XmlRpcServer.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/server/XmlRpcServer.java @@ -79,7 +79,7 @@ protected RpcRequest createRpcRequest() { public void start() throws IOException { logger.debug("Initializing XML-RPC server at port {}", config.getXmlCallbackPort()); - InetSocketAddress callbackAddress = new InetSocketAddress(config.getBindAddress(), config.getXmlCallbackPort()); + InetSocketAddress callbackAddress = new InetSocketAddress(config.getXmlCallbackPort()); xmlRpcHTTPD = new Server(callbackAddress); xmlRpcHTTPD.setHandler(jettyResponseHandler); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/ButtonVirtualDatapointHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/ButtonVirtualDatapointHandler.java index 43fce06e2fba5..b4feca685fad3 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/ButtonVirtualDatapointHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/ButtonVirtualDatapointHandler.java @@ -14,13 +14,13 @@ import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_BUTTON; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDevice; import org.openhab.binding.homematic.internal.model.HmValueType; import org.openhab.core.thing.CommonTriggerEvents; +import org.openhab.core.thing.DefaultSystemChannelTypeProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,7 +59,8 @@ public boolean canHandleEvent(HmDatapoint dp) { public void handleEvent(VirtualGateway gateway, HmDatapoint dp) { HmDatapoint vdp = getVirtualDatapoint(dp.getChannel()); if (MiscUtils.isTrueValue(dp.getValue())) { - String pressType = StringUtils.substringAfter(dp.getName(), "_"); + int usPos = dp.getName().indexOf("_"); + String pressType = usPos == -1 ? dp.getName() : dp.getName().substring(usPos + 1); switch (pressType) { case "SHORT": if (dp.getValue() == null || !dp.getValue().equals(dp.getPreviousValue())) { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DeleteDeviceModeVirtualDatapointHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DeleteDeviceModeVirtualDatapointHandler.java index dbbaf8bac314c..c79263dd57a8d 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DeleteDeviceModeVirtualDatapointHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DeleteDeviceModeVirtualDatapointHandler.java @@ -16,7 +16,6 @@ import java.io.IOException; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.misc.HomematicClientException; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDatapointConfig; @@ -61,7 +60,7 @@ public boolean canHandleCommand(HmDatapoint dp, Object value) { public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value) throws IOException, HomematicClientException { dp.setValue(value); - if (!StringUtils.equals(dp.getOptionValue(), MODE_LOCKED)) { + if (!MODE_LOCKED.equals(dp.getOptionValue())) { gateway.disableDatapoint(dp, DELETE_MODE_DURATION); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayOptionsVirtualDatapointHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayOptionsVirtualDatapointHandler.java index ce45dcaa58994..eaf1a8b39e552 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayOptionsVirtualDatapointHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayOptionsVirtualDatapointHandler.java @@ -16,7 +16,6 @@ import java.io.IOException; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.communicator.parser.DisplayOptionsParser; import org.openhab.binding.homematic.internal.misc.HomematicClientException; import org.openhab.binding.homematic.internal.model.HmChannel; @@ -60,8 +59,9 @@ public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointCon DisplayOptionsParser rcOptionsParser = new DisplayOptionsParser(channel); rcOptionsParser.parse(value); - if (StringUtils.isNotBlank(rcOptionsParser.getText())) { - sendDatapoint(gateway, channel, DATAPOINT_NAME_TEXT, rcOptionsParser.getText()); + String dpNameText = rcOptionsParser.getText(); + if (dpNameText != null && !dpNameText.isBlank()) { + sendDatapoint(gateway, channel, DATAPOINT_NAME_TEXT, dpNameText); } sendDatapoint(gateway, channel, DATAPOINT_NAME_BEEP, rcOptionsParser.getBeep()); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayTextVirtualDatapoint.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayTextVirtualDatapoint.java index 4c33ae02337ec..8a23ecb4423e3 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayTextVirtualDatapoint.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/virtual/DisplayTextVirtualDatapoint.java @@ -21,9 +21,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.homematic.internal.misc.HomematicClientException; import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmChannel; @@ -328,9 +327,9 @@ public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointCon } for (int i = 1; i <= getLineCount(channel.getDevice()); i++) { - String line = ObjectUtils.toString( - channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_LINE + i).getValue()); - if (StringUtils.isEmpty(line)) { + String line = Objects.toString( + channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_LINE + i).getValue(), ""); + if (line.isEmpty()) { line = " "; } message.add(LINE); @@ -340,12 +339,12 @@ public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointCon .getOptionValue(); message.add(COLOR); String colorCode = Color.getCode(color); - message.add(StringUtils.isBlank(colorCode) ? Color.WHITE.getCode() : colorCode); + message.add(colorCode == null || colorCode.isBlank() ? Color.WHITE.getCode() : colorCode); } String icon = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_ICON + i) .getOptionValue(); String iconCode = Icon.getCode(icon); - if (StringUtils.isNotBlank(iconCode)) { + if (iconCode != null && !iconCode.isBlank()) { message.add(ICON); message.add(iconCode); } @@ -374,7 +373,7 @@ public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointCon message.add(STOP); gateway.sendDatapoint(channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_SUBMIT), - new HmDatapointConfig(), StringUtils.join(message, ","), null); + new HmDatapointConfig(), String.join(",", message), null); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/discovery/eq3udp/Eq3UdpResponse.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/discovery/eq3udp/Eq3UdpResponse.java index 5a6dd08934aed..6956c049e5103 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/discovery/eq3udp/Eq3UdpResponse.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/discovery/eq3udp/Eq3UdpResponse.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.homematic.internal.discovery.eq3udp; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; - /** * Extracts a UDP response from a Homematic CCU gateway. * @@ -86,7 +83,7 @@ private int readInt(byte[] data, int index, int length) throws IndexOutOfBoundsE @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("deviceTypeId", deviceTypeId) - .append("serialNumber", serialNumber).toString(); + return String.format("%s[deviceTypeId=%s,serialNumber=%s]", getClass().getSimpleName(), deviceTypeId, + serialNumber); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicBridgeHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicBridgeHandler.java index dd69e1b5e7cb4..a7f49b0f5a000 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicBridgeHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicBridgeHandler.java @@ -213,9 +213,6 @@ private HomematicConfig createHomematicConfig() { if (homematicConfig.getCallbackHost() == null) { homematicConfig.setCallbackHost(this.ipv4Address); } - if (homematicConfig.getBindAddress() == null) { - homematicConfig.setBindAddress(homematicConfig.getCallbackHost()); - } if (homematicConfig.getXmlCallbackPort() == 0) { homematicConfig.setXmlCallbackPort(portPool.getNextPort()); } else { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicThingHandler.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicThingHandler.java index 0ac53954a681d..ae09a2d2b799a 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicThingHandler.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/handler/HomematicThingHandler.java @@ -23,11 +23,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.concurrent.Future; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.math.NumberUtils; import org.openhab.binding.homematic.internal.HomematicBindingConstants; import org.openhab.binding.homematic.internal.common.HomematicConfig; import org.openhab.binding.homematic.internal.communicator.HomematicGateway; @@ -216,8 +214,8 @@ private boolean updateDynamicChannelList(HmDevice device, List thingCha private static boolean containsChannel(List channels, ChannelUID channelUID) { for (Channel channel : channels) { ChannelUID uid = channel.getUID(); - if (StringUtils.equals(channelUID.getGroupId(), uid.getGroupId()) - && StringUtils.equals(channelUID.getId(), uid.getId())) { + if (Objects.equals(channelUID.getGroupId(), uid.getGroupId()) + && Objects.equals(channelUID.getId(), uid.getId())) { return true; } } @@ -232,7 +230,7 @@ private void setProperty(Map properties, HmChannel channelZero, HmDatapoint dp = channelZero .getDatapoint(new HmDatapointInfo(HmParamsetType.VALUES, channelZero, datapointName)); if (dp != null) { - properties.put(propertyName, ObjectUtils.toString(dp.getValue())); + properties.put(propertyName, Objects.toString(dp.getValue(), "")); } } @@ -375,7 +373,7 @@ private void updateChannelState(final HmDatapoint dp, Channel channel) throws IOException, GatewayNotAvailableException, ConverterException { if (dp.isTrigger()) { if (dp.getValue() != null) { - triggerChannel(channel.getUID(), ObjectUtils.toString(dp.getValue())); + triggerChannel(channel.getUID(), dp.getValue() == null ? "" : dp.getValue().toString()); } } else if (isLinked(channel)) { loadHomematicChannelValues(dp.getChannel()); @@ -419,6 +417,8 @@ private void updateStatus(HmDevice device) throws GatewayNotAvailableException, loadHomematicChannelValues(device.getChannel(0)); ThingStatus oldStatus = thing.getStatus(); + if (oldStatus == ThingStatus.UNINITIALIZED) + return; ThingStatus newStatus = ThingStatus.ONLINE; ThingStatusDetail newDetail = ThingStatusDetail.NONE; @@ -495,9 +495,10 @@ public void handleConfigurationUpdate(Map configurationParameter Object newValue = configurationParameter.getValue(); if (key.startsWith("HMP_")) { - key = StringUtils.removeStart(key, "HMP_"); - Integer channelNumber = NumberUtils.toInt(StringUtils.substringBefore(key, "_")); - String dpName = StringUtils.substringAfter(key, "_"); + key = key.substring(4); + int sepPos = key.indexOf("_"); + Integer channelNumber = Integer.valueOf(key.substring(0, sepPos)); + String dpName = key.substring(sepPos + 1); HmDatapointInfo dpInfo = new HmDatapointInfo(device.getAddress(), HmParamsetType.MASTER, channelNumber, dpName); @@ -514,8 +515,7 @@ public void handleConfigurationUpdate(Map configurationParameter newValue = decimal.doubleValue(); } } - if (ObjectUtils.notEqual(dp.isEnumType() ? dp.getOptionValue() : dp.getValue(), - newValue)) { + if (!Objects.equals(dp.isEnumType() ? dp.getOptionValue() : dp.getValue(), newValue)) { sendDatapoint(dp, new HmDatapointConfig(), newValue); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/HomematicConstants.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/HomematicConstants.java index bebcb052405cc..d1560204afbe4 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/HomematicConstants.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/HomematicConstants.java @@ -83,6 +83,7 @@ public class HomematicConstants { public static final String DATAPOINT_NAME_CALIBRATION = "CALIBRATION"; public static final String DATAPOINT_NAME_LOWBAT_IP = "LOW_BAT"; public static final String DATAPOINT_NAME_CHANNEL_FUNCTION = "CHANNEL_FUNCTION"; + public static final String DATAPOINT_NAME_OPERATING_VOLTAGE = "OPERATING_VOLTAGE"; public static final String VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE = "BATTERY_TYPE"; public static final String VIRTUAL_DATAPOINT_NAME_DELETE_DEVICE_MODE = "DELETE_DEVICE_MODE"; diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/MiscUtils.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/MiscUtils.java index 20f54e5211460..d41aaad9e5a98 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/MiscUtils.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/misc/MiscUtils.java @@ -50,4 +50,62 @@ public static boolean isTrueValue(Object value) { public static boolean isFalseValue(Object value) { return value != null && value == Boolean.FALSE; } + + /** + * Returns true, if str starts with search. Check is done case-insensitive. + */ + public static boolean strStartsWithIgnoreCase(String str, String search) { + if (str == null || search == null || search.length() > str.length()) { + return false; + } + return str.substring(0, search.length()).equalsIgnoreCase(search); + } + + /** + * Returns true if address is a device + */ + public static boolean isDevice(String address) { + return isDevice(address, false); + } + + /** + * Returns true if address is a device. If allowBidCos ist true then addresses starting with "BidCos" classified as + * devices, too. + */ + public static boolean isDevice(String address, boolean allowBidCos) { + if (address == null) { + return false; + } + if (address.contains(":")) { + return false; + } + if (allowBidCos && strStartsWithIgnoreCase(address.trim(), "BidCos")) { + return true; + } + return !strStartsWithIgnoreCase(address.trim(), "BidCos"); + } + + /** + * Changes all characters after whitespace to upper-case and all other character to lower case. + */ + public static String capitalize(String value) { + if (value == null) { + return null; + } + char[] chars = value.toCharArray(); + boolean capitalizeNextChar = true; + for (int i = 0; i < chars.length; i++) { + if (Character.isWhitespace(chars[i])) { + capitalizeNextChar = true; + } else { + if (capitalizeNextChar) { + chars[i] = Character.toTitleCase(chars[i]); + capitalizeNextChar = false; + } else { + chars[i] = Character.toLowerCase(chars[i]); + } + } + } + return new String(chars); + } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmChannel.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmChannel.java index 4214d73b558a1..75eece216b28a 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmChannel.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmChannel.java @@ -18,8 +18,6 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; import org.openhab.binding.homematic.internal.misc.HomematicConstants; /** @@ -216,7 +214,6 @@ public boolean hasPressDatapoint() { @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("number", number).append("type", type) - .append("initialized", initialized).toString(); + return String.format("%s[number=%d,initialized=%b]", getClass().getSimpleName(), number, initialized); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapoint.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapoint.java index d54d2f7fd43ce..7203179bf2765 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapoint.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapoint.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.homematic.internal.model; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; import org.openhab.binding.homematic.internal.misc.MiscUtils; /** @@ -431,11 +428,10 @@ public HmDatapoint clone() { @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("name", name).append("value", value) - .append("defaultValue", defaultValue).append("type", type).append("minValue", minValue) - .append("maxValue", maxValue).append("step", step).append("options", StringUtils.join(options, ";")) - .append("readOnly", readOnly).append("readable", readable).append("unit", unit) - .append("description", description).append("info", info).append("paramsetType", paramsetType) - .append("virtual", virtual).append("trigger", trigger).toString(); + return String.format("%s[name=%s,value=%s,defaultValue=%s,type=%s,minValue=%s,maxValue=%s,step=%s,options=%s," + + "readOnly=%b,readable=%b,unit=%s,description=%s,info=%s,paramsetType=%s,virtual=%b,trigger=%b]", + getClass().getSimpleName(), name, value, defaultValue, type, minValue, maxValue, step, + (options == null ? null : String.join(";", options)), readOnly, readable, unit, description, info, + paramsetType, virtual, trigger); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointConfig.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointConfig.java index 75c9a23a0225d..11f1a0eb021ed 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointConfig.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointConfig.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.homematic.internal.model; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; - /** * Configuration object for sending a datapoint. * @@ -54,7 +51,6 @@ public void setReceiveDelay(Double receiveDelay) { @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("delay", delay) - .append("receiveDelay", receiveDelay).toString(); + return String.format("%s[delay=%f,receiveDelay=%f]", getClass().getSimpleName(), delay, receiveDelay); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointInfo.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointInfo.java index 67ac4a7bdc903..e0374d16243d0 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointInfo.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDatapointInfo.java @@ -12,8 +12,7 @@ */ package org.openhab.binding.homematic.internal.model; -import org.apache.commons.lang.builder.EqualsBuilder; -import org.apache.commons.lang.builder.HashCodeBuilder; +import java.util.Objects; /** * Simple representation of a datapoint. @@ -92,7 +91,7 @@ public boolean isPong() { @Override public int hashCode() { - return new HashCodeBuilder().append(address).append(paramsetType).append(channel).append(name).toHashCode(); + return Objects.hash(address, paramsetType, channel, name); } @Override @@ -101,8 +100,8 @@ public boolean equals(Object obj) { return false; } HmDatapointInfo comp = (HmDatapointInfo) obj; - return new EqualsBuilder().append(address, comp.getAddress()).append(paramsetType, comp.getParamsetType()) - .append(channel, comp.getChannel()).append(name, comp.getName()).isEquals(); + return Objects.equals(address, comp.getAddress()) && Objects.equals(paramsetType, comp.getParamsetType()) + && Objects.equals(channel, comp.getChannel()) && Objects.equals(name, comp.getName()); } @Override diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDevice.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDevice.java index 8f265ced3c2f4..36626549a1997 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDevice.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmDevice.java @@ -16,12 +16,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; -import org.apache.commons.lang.builder.EqualsBuilder; -import org.apache.commons.lang.builder.HashCodeBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; import org.openhab.binding.homematic.internal.misc.MiscUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Object that represents a Homematic device. @@ -29,6 +28,8 @@ * @author Gerhard Riegler - Initial contribution */ public class HmDevice { + private final Logger logger = LoggerFactory.getLogger(HmDevice.class); + public static final String TYPE_GATEWAY_EXTRAS = "GATEWAY-EXTRAS"; public static final String ADDRESS_GATEWAY_EXTRAS = "GWE00000000"; @@ -46,10 +47,15 @@ public HmDevice(String address, HmInterface hmInterface, String type, String gat String firmware) { this.address = address; this.hmInterface = hmInterface; - this.type = type; + this.firmware = firmware; + if ("HM-ES-TX-WM".equals(type) && Float.valueOf(firmware) > 2.0) { + logger.debug("Found HM-ES-TX-WM with firmware version > 2.0, creating virtual type"); + this.type = type + "2"; + } else { + this.type = type; + } this.gatewayId = gatewayId; this.homegearId = homegearId; - this.firmware = firmware; } /** @@ -202,7 +208,7 @@ private boolean isStatusDatapointEnabled(String datapointName) { @Override public int hashCode() { - return new HashCodeBuilder().append(address).toHashCode(); + return Objects.hash(address); } @Override @@ -211,13 +217,12 @@ public boolean equals(Object obj) { return false; } HmDevice comp = (HmDevice) obj; - return new EqualsBuilder().append(address, comp.getAddress()).isEquals(); + return Objects.equals(address, comp.getAddress()); } @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("hmInterface", hmInterface) - .append("address", address).append("type", type).append("name", name).append("firmware", firmware) - .append("gatewayId", gatewayId).toString(); + return String.format("%s[hmInterface=%s,address=%s,type=%s,name=%s,firmware=%s,gatewayId=%s]", + getClass().getSimpleName(), hmInterface, address, type, name, firmware, gatewayId); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmGatewayInfo.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmGatewayInfo.java index 1bfbfb5b9c3b0..fc77be6b29d73 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmGatewayInfo.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmGatewayInfo.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.homematic.internal.model; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; - /** * Info object which holds gateway specific informations. * @@ -184,9 +181,8 @@ public void setRfInterface(boolean rfInterface) { @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("id", id).append("type", type) - .append("firmware", firmware).append("address", address).append("rf", rfInterface) - .append("wired", wiredInterface).append("hmip", hmipInterface).append("cuxd", cuxdInterface) - .append("group", groupInterface).toString(); + return String.format("%s[id=%s,type=%s,firmware=%s,address=%s,rf=%b,wired=%b,hmip=%b,cuxd=%b,group=%b]", + getClass().getSimpleName(), id, type, firmware, address, rfInterface, wiredInterface, hmipInterface, + cuxdInterface, groupInterface); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmRssiInfo.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmRssiInfo.java index ebae8c1c5e13f..2978f4b921f77 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmRssiInfo.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/model/HmRssiInfo.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.homematic.internal.model; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; - /** * Object that holds the rssi infos for a RF device. * @@ -65,7 +62,6 @@ public Integer getPeer() { @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("address", address) - .append("device", device).append("peer", peer).toString(); + return String.format("%s[address=%s,device=%d,peer=%i]", getClass().getSimpleName(), address, device, peer); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java index ce10bca8e6113..a3ea8000e88e7 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java @@ -23,11 +23,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; +import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDevice; @@ -171,7 +170,7 @@ public void generate(HmDevice device) { ChannelGroupType groupType = channelGroupTypeProvider.getInternalChannelGroupType(groupTypeUID); if (groupType == null || device.isGatewayExtras()) { String groupLabel = String.format("%s", - WordUtils.capitalizeFully(StringUtils.replace(channel.getType(), "_", " "))); + MiscUtils.capitalize(channel.getType().replace("_", " "))); groupType = ChannelGroupTypeBuilder.instance(groupTypeUID, groupLabel) .withChannelDefinitions(channelDefinitions).build(); channelGroupTypeProvider.addChannelGroupType(groupType); @@ -195,7 +194,7 @@ public void validateFirmwares() { "Multiple firmware versions for device type '{}' found ({}). " + "Make sure, all devices of the same type have the same firmware version, " + "otherwise you MAY have channel and/or datapoint errors in the logfile", - deviceType, StringUtils.join(firmwares, ", ")); + deviceType, String.join(", ", firmwares)); } } } @@ -204,7 +203,7 @@ public void validateFirmwares() { * Adds the firmware version for validation. */ private void addFirmware(HmDevice device) { - if (!StringUtils.equals(device.getFirmware(), "?") && !DEVICE_TYPE_VIRTUAL.equals(device.getType()) + if (!"?".equals(device.getFirmware()) && !DEVICE_TYPE_VIRTUAL.equals(device.getType()) && !DEVICE_TYPE_VIRTUAL_WIRED.equals(device.getType())) { Set firmwares = firmwaresByType.get(device.getType()); if (firmwares == null) { @@ -237,7 +236,8 @@ private ThingType createThingType(HmDevice device, List groupT List groupDefinitions = new ArrayList<>(); for (ChannelGroupType groupType : groupTypes) { - String id = StringUtils.substringAfterLast(groupType.getUID().getId(), "_"); + int usPos = groupType.getUID().getId().lastIndexOf("_"); + String id = usPos == -1 ? groupType.getUID().getId() : groupType.getUID().getId().substring(usPos + 1); groupDefinitions.add(new ChannelGroupDefinition(id, groupType.getUID())); } @@ -337,7 +337,7 @@ private void generateConfigDescription(HmDevice device, URI configDescriptionURI MetadataUtils.getParameterName(dp), MetadataUtils.getConfigDescriptionParameterType(dp)); builder.withLabel(MetadataUtils.getLabel(dp)); - builder.withDefault(ObjectUtils.toString(dp.getDefaultValue())); + builder.withDefault(Objects.toString(dp.getDefaultValue(), "")); builder.withDescription(MetadataUtils.getDatapointDescription(dp)); if (dp.isEnumType()) { @@ -383,6 +383,11 @@ private URI getConfigDescriptionURI(HmDevice device) { * Returns true, if the given datapoint can be ignored for metadata generation. */ public static boolean isIgnoredDatapoint(HmDatapoint dp) { - return StringUtils.indexOfAny(dp.getName(), IGNORE_DATAPOINT_NAMES) != -1; + for (String testValue : IGNORE_DATAPOINT_NAMES) { + if (dp.getName().indexOf(testValue) > -1) { + return true; + } + } + return false; } } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java index ef5178b285fad..f288b83963954 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -29,8 +30,7 @@ import java.util.ResourceBundle; import java.util.Set; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; +import org.openhab.binding.homematic.internal.misc.MiscUtils; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDevice; import org.openhab.core.config.core.ConfigDescriptionParameter.Type; @@ -77,9 +77,16 @@ private static void loadStandardDatapoints() { BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { String line; while ((line = reader.readLine()) != null) { - if (StringUtils.trimToNull(line) != null && !StringUtils.startsWith(line, "#")) { - String channelType = StringUtils.trimToNull(StringUtils.substringBefore(line, "|")); - String datapointName = StringUtils.trimToNull(StringUtils.substringAfter(line, "|")); + if (!line.trim().isEmpty() && !line.startsWith("#")) { + String[] parts = line.split("\\|"); + String channelType = null; + String datapointName = null; + if (parts.length > 0) { + channelType = parts[0].trim(); + if (parts.length > 1) { + datapointName = parts[1].trim(); + } + } Set channelDatapoints = standardDatapoints.get(channelType); if (channelDatapoints == null) { @@ -142,8 +149,7 @@ public static Type getConfigDescriptionParameterType(HmDatapoint dp) { */ public static String getUnit(HmDatapoint dp) { if (dp.getUnit() != null) { - String unit = StringUtils.replace(dp.getUnit(), "100%", "%"); - return StringUtils.replace(unit, "%", "%%"); + return dp.getUnit().replace("100%", "%").replace("%", "%%"); } return null; } @@ -182,7 +188,7 @@ public static String getStatePattern(HmDatapoint dp) { * Returns the label string for the given Datapoint. */ public static String getLabel(HmDatapoint dp) { - return WordUtils.capitalizeFully(StringUtils.replace(dp.getName(), "_", " ")); + return MiscUtils.capitalize(dp.getName().replace("_", " ")); } /** @@ -198,7 +204,7 @@ public static String getParameterName(HmDatapoint dp) { public static String getDescription(String... keys) { StringBuilder sb = new StringBuilder(); for (int startIdx = 0; startIdx < keys.length; startIdx++) { - String key = StringUtils.join(keys, "|", startIdx, keys.length); + String key = String.join("|", Arrays.copyOfRange(keys, startIdx, keys.length)); if (key.endsWith("|")) { key = key.substring(0, key.length() - 1); } @@ -209,7 +215,7 @@ public static String getDescription(String... keys) { sb.append(key).append(", "); } if (logger.isTraceEnabled()) { - logger.trace("Description not found for: {}", StringUtils.substring(sb.toString(), 0, -2)); + logger.trace("Description not found for: {}", sb.toString().substring(0, sb.length() - 2)); } return null; } @@ -224,9 +230,8 @@ public static String getDeviceName(HmDevice device) { String deviceDescription = null; boolean isTeam = device.getType().endsWith("-Team"); - String type = isTeam ? StringUtils.remove(device.getType(), "-Team") : device.getType(); + String type = isTeam ? device.getType().replace("-Team", "") : device.getType(); deviceDescription = getDescription(type); - if (deviceDescription != null && isTeam) { deviceDescription += " Team"; } @@ -276,7 +281,10 @@ public static BigDecimal createBigDecimal(Number number) { */ public static String getItemType(HmDatapoint dp) { String dpName = dp.getName(); - String channelType = StringUtils.defaultString(dp.getChannel().getType()); + String channelType = dp.getChannel().getType(); + if (channelType == null) { + channelType = ""; + } if (dp.isBooleanType()) { if (((dpName.equals(DATAPOINT_NAME_STATE) || dpName.equals(VIRTUAL_DATAPOINT_NAME_STATE_CONTACT)) @@ -323,6 +331,9 @@ public static String getItemType(HmDatapoint dp) { return ITEM_TYPE_NUMBER + ":Energy"; case "m3": return ITEM_TYPE_NUMBER + ":Volume"; + case "": + if (dpName.startsWith(DATAPOINT_NAME_OPERATING_VOLTAGE)) + return ITEM_TYPE_NUMBER + ":ElectricPotential"; case "s": case "min": case "minutes": @@ -330,7 +341,6 @@ public static String getItemType(HmDatapoint dp) { case "month": case "year": case "100%": - case "": default: return ITEM_TYPE_NUMBER; } @@ -359,7 +369,10 @@ public static boolean isRollerShutter(HmDatapoint dp) { */ public static String getCategory(HmDatapoint dp, String itemType) { String dpName = dp.getName(); - String channelType = StringUtils.defaultString(dp.getChannel().getType()); + String channelType = dp.getChannel().getType(); + if (channelType == null) { + channelType = ""; + } if (dpName.equals(DATAPOINT_NAME_BATTERY_TYPE) || dpName.equals(DATAPOINT_NAME_LOWBAT) || dpName.equals(DATAPOINT_NAME_LOWBAT_IP)) { diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/UidUtils.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/UidUtils.java index c2aa4d161f4cd..b04f28687cde1 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/UidUtils.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/UidUtils.java @@ -14,7 +14,6 @@ import static org.openhab.binding.homematic.internal.HomematicBindingConstants.BINDING_ID; -import org.apache.commons.lang.math.NumberUtils; import org.openhab.binding.homematic.internal.model.HmChannel; import org.openhab.binding.homematic.internal.model.HmDatapoint; import org.openhab.binding.homematic.internal.model.HmDatapointInfo; @@ -83,8 +82,15 @@ public static ChannelUID generateChannelUID(HmDatapoint dp, ThingUID thingUID) { * Generates the HmDatapointInfo for the given thing and channelUID. */ public static HmDatapointInfo createHmDatapointInfo(ChannelUID channelUID) { - return new HmDatapointInfo(channelUID.getThingUID().getId(), HmParamsetType.VALUES, - NumberUtils.toInt(channelUID.getGroupId()), channelUID.getIdWithoutGroup()); + int value; + try { + String groupID = channelUID.getGroupId(); + value = groupID == null ? 0 : Integer.parseInt(groupID); + } catch (NumberFormatException e) { + value = 0; + } + return new HmDatapointInfo(channelUID.getThingUID().getId(), HmParamsetType.VALUES, value, + channelUID.getIdWithoutGroup()); } /** diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java index e232c8d32192a..0dc6f526521e9 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java @@ -25,9 +25,6 @@ import java.util.Map.Entry; import java.util.TreeMap; -import org.apache.commons.lang.StringEscapeUtils; -import org.apache.commons.lang.StringUtils; - /** * The CcuMetadataExtractor loads some JavaScript files from the CCU and generates the device and datapoint * descriptions into the file generated-descriptions.properties. @@ -122,7 +119,7 @@ private void generate() throws IOException { */ private Map loadJsonLangDescriptionFile(String url, String lang) throws IOException { final Map descriptions = new TreeMap<>(); - String descriptionUrl = StringUtils.replace(url, "{LANGUAGE}", lang); + String descriptionUrl = url.replace("{LANGUAGE}", lang); String startLine = " \"" + lang + "\" : {"; String endLine = " },"; @@ -132,7 +129,7 @@ private Map loadJsonLangDescriptionFile(String url, String lang) public void line(String line) { String[] entry = handleStringTable(line); if (entry != null) { - descriptions.put(StringUtils.trim(entry[0]), unescape(StringUtils.trim(entry[1]))); + descriptions.put(entry[0].trim(), unescape(entry[1].trim())); } } }; @@ -150,20 +147,24 @@ private Map loadDeviceKeys() throws IOException { @Override public void line(String line) { if (line.startsWith("elvST['")) { - line = StringUtils.remove(line, "elvST['"); - line = StringUtils.replace(line, "'] = '", "="); - line = StringUtils.remove(line, "';"); - line = StringUtils.remove(line, "$"); - line = StringUtils.remove(line, "{"); - line = StringUtils.remove(line, "}"); - - int count = StringUtils.countMatches(line, "="); + line = line.replace("elvST['", ""); + line = line.replace("'] = '", "="); + line = line.replace("';", ""); + line = line.replace("$", ""); + line = line.replace("{", ""); + line = line.replace("}", ""); + + int count = 0; + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) == '=') { + count++; + } + } if (count > 1) { - line = StringUtils.replace(line, "=", "|", 1); + line = line.replaceFirst("=", "|"); } - - String[] split = StringUtils.split(line, "=", 2); - deviceKeys.put(StringUtils.trim(split[0]), StringUtils.trim(split[1])); + String[] split = line.split("=", 2); + deviceKeys.put(split[0].trim(), split[1].trim()); } } }; @@ -174,11 +175,11 @@ public void line(String line) { * Splits a JSON JavaScript entry. */ private String[] handleStringTable(String line) { - line = StringUtils.remove(line, " \""); - line = StringUtils.remove(line, "\","); - line = StringUtils.remove(line, "\""); + line = line.replace(" \"", ""); + line = line.replace("\",", ""); + line = line.replace("\"", ""); - String[] splitted = StringUtils.split(line, ":", 2); + String[] splitted = line.split(":", 2); return splitted.length != 2 ? null : splitted; } @@ -186,16 +187,21 @@ private String[] handleStringTable(String line) { * Transforms a string for a Java property file. */ private String unescape(String str) { - str = StringUtils.replace(str, "%FC", "ü"); - str = StringUtils.replace(str, "%DC", "Ü"); - str = StringUtils.replace(str, "%E4", "ä"); - str = StringUtils.replace(str, "%C4", "Ä"); - str = StringUtils.replace(str, "%F6", "ö"); - str = StringUtils.replace(str, "%D6", "Ö"); - str = StringUtils.replace(str, "%DF", "ß"); - str = StringUtils.remove(str, " "); - str = StringUtils.replace(str, "
", " "); - str = StringEscapeUtils.unescapeHtml(str); + str = str.replace("%FC", "ü"); + str = str.replace("%DC", "Ü"); + str = str.replace("%E4", "ä"); + str = str.replace("%C4", "Ä"); + str = str.replace("%F6", "ö"); + str = str.replace("%D6", "Ö"); + str = str.replace("%DF", "ß"); + str = str.replace(" ", ""); + str = str.replace("
", " "); + str = str.replace("ü", "ü"); + str = str.replace("ä", "ä"); + str = str.replace("ö", "ö"); + str = str.replace("Ü", "Ü"); + str = str.replace("Ä", "Ä"); + str = str.replace("Ö", "Ö"); return str; } @@ -222,7 +228,7 @@ public UrlLoader(String url, String startLine, String endLine) throws IOExceptio includeLine = false; } } - if ((includeLine == null || includeLine) && StringUtils.isNotBlank(line)) { + if ((includeLine == null || includeLine) && !line.isBlank()) { line(line); } } diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.homematic/src/main/resources/OH-INF/thing/bridge.xml index cd8d9b8bd1705..468b5aa670428 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.homematic/src/main/resources/OH-INF/thing/bridge.xml @@ -37,11 +37,6 @@ Callback network address of the runtime, default is auto-discovery
- - network-address - - The address the XML-/BINRPC server binds to, default is callbackHost - Callback port of the binding's XML-RPC server. If no value is specified, xmlCallbackPort starts with diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/batteries.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/batteries.properties index 3cebb5a73e16b..2aa2c6c1aefe2 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/batteries.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/batteries.properties @@ -82,6 +82,8 @@ HmIP-WTH=2x AAA/Micro/LR03 HmIP-WRC2=2x AAA/Micro/LR03 HmIP-eTRV=2x AA/Mignon/LR06 HmIP-eTRV-B=2x AA/Mignon/LR06 +HmIP-eTRV-C=2x AA/Mignon/LR06 +HmIP-eTRV-C-2=2x AA/Mignon/LR06 HmIP-SMI=2x AA/Mignon/LR06 HmIP-SPI=2x AA/Mignon/LR06 HmIP-SPDR=2x AA/Mignon/LR06 diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties index a8b345d23780b..75630cfa14edf 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties @@ -4,6 +4,7 @@ HM-LC-Sw1-Pl-2=Wireless Switch Actuator 1-channel, socket adapter HM-Sec-SD-2=Wireless Smoke Detector HM-WDS30-OT2-SM-2=Wireless Differential Temperature Sensor HM-Sen-MDIR-O-2=Wireless Motion Detector, outdoor +HM-ES-TX-WM2=Wireless Transmitter for Energy Meter Sensor Version 2 # MAX! BC-RT-TRX-CyN=MAX! Radiator thermostat basic @@ -30,6 +31,7 @@ HMIP-eTRV=Homematic IP Radiator thermostat HMIP-eTRV-2=Homematic IP Radiator thermostat HMIP-eTRV-B=Homematic IP Radiator thermostat basic HMIP-eTRV-C=Homematic IP Radiator thermostat compact +HmIP-eTRV-C-2=Homematic IP Radiator thermostat compact HmIP-SMI=Homematic IP motion detector with brightness sensor HmIP-SMO=Homematic IP motion detector with brightness sensor - outdoor HmIP-KRC4=Homematic IP Key Ring Remote Control - 4 buttons diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties index 92ab34f0597ef..9a6b4893223c3 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties @@ -4,6 +4,7 @@ HM-LC-Sw1-Pl-2=Funk-Schaltaktor 1-fach, Zwischenstecker HM-Sec-SD-2=Funk-Rauchmelder HM-WDS30-OT2-SM-2=Funk-Temperaturdifferenzsensor HM-Sen-MDIR-O-2=Funk-Bewegungsmelder au�en +HM-ES-TX-WM2=Funk-Sender f�r Energiez�hler-Sensor Version 2 # MAX! BC-RT-TRX-CyN=MAX! Heizk�rperthermostat Basic @@ -30,6 +31,7 @@ HMIP-eTRV=Homematic IP Heizk HMIP-eTRV-2=Homematic IP Heizk�rperthermostat HMIP-eTRV-B=Homematic IP Heizk�rperthermostat Basis HMIP-eTRV-C=Homematic IP Heizk�rperthermostat kompakt +HmIP-eTRV-C-2=Homematic IP Heizk�rperthermostat kompakt HmIP-SMI=Homematic IP Bewegungsmelder mit D�mmerungssensor HmIP-SMO=Homematic IP Bewegungsmelder mit D�mmerungssensor au�en HmIP-KRC4=Homematic IP Schl�sselbundfernbedienung - 4 Tasten diff --git a/bundles/org.openhab.binding.homewizard/NOTICE b/bundles/org.openhab.binding.homewizard/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.homewizard/README.md b/bundles/org.openhab.binding.homewizard/README.md new file mode 100644 index 0000000000000..2c848b5961ec4 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/README.md @@ -0,0 +1,57 @@ +# HomeWizard Binding + +The HomeWizard binding retrieves measurements from the HomeWizard Wi-Fi P1 meter. +The meter itself is attached to a DSMR Smart Meter and reads out the telegrams, which it will forward to cloud storage. +However, recently HomeWizard also added an interface that can be queried locally. + +This binding uses that local interface to make the measurements available. + +## Supported Things + +The binding provides the P1 Meter thing. + +## Discovery + +Auto discovery is not available for this binding. + +## Thing Configuration + +The P1 Meter thing can be configured through the web interface. + +| Parameter | Required | Default | Description | +|--------------|----------|---------|---------------------------------------------------------------------------------------------------| +| ipAddress | * | | This specifies the IP address (or host name) where the meter can be found. | +| refreshDelay | | 5 | This specifies the interval in seconds used by the binding to read updated values from the meter. | + +Note that update rate of the P1 Meter itself depends on the frequency of the telegrams it receives from the Smart Meter. +For DSMR5 meters this is generally once per second, for older versions the frequency is much lower. + +Example of configuration through a .thing file: + +``` +Thing homewizard:p1_wifi_meter:my_meter [ ipAddress="192.178.1.67", refreshDelay=5 ] +``` + +## Channels + +| Channel ID | Item Type | Description | +|------------------------|---------------|--------------------------------------------------------------------------------------------| +| total_energy_import_t1 | Number:Energy | The most recently reported total imported energy in kWh by counter 1. | +| total_energy_import_t2 | Number:Energy | The most recently reported total imported energy in kWh by counter 2. | +| total_energy_export_t1 | Number:Energy | The most recently reported total exported energy in kWh by counter 1. | +| total_energy_export_t2 | Number:Energy | The most recently reported total exported energy in kWh by counter 2. | +| active_power | Number:Power | The current net total power in W. It will be below 0 if power is currently being exported. | +| active_power_l1 | Number:Power | The current net total power in W for phase 1. | +| active_power_l2 | Number:Power | The current net total power in W for phase 2. | +| active_power_l3 | Number:Power | The current net total power in W for phase 3. | +| total_gas | Number:Volume | The most recently reported total imported gas in m^3. | +| gas_timestamp | DateTime | The time stamp of the total_gas measurement. | + + +Example of configuration through a .items file: + +``` +Number:Energy Energy_Import_T1 "Imported Energy T1 [%.0f kWh]" {channel="homewizard:p1_wifi_meter:my_meter:total_energy_import_t1" } +Number:Power Active_Power_L1 "Active Power Phase 1 [%.1f W]" {channel="homewizard:p1_wifi_meter:my_meter:active_power_l1" } +DateTime Gas_Update "Gas Update Time [%1$tH:%1$tM]" {channel="homewizard:p1_wifi_meter:my_meter:gas_timestamp" } +``` diff --git a/bundles/org.openhab.binding.homewizard/pom.xml b/bundles/org.openhab.binding.homewizard/pom.xml new file mode 100644 index 0000000000000..d3928339f6cc3 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + org.openhab.binding.homewizard + openHAB Add-ons :: Bundles :: HomeWizard Binding + diff --git a/bundles/org.openhab.binding.homewizard/src/main/feature/feature.xml b/bundles/org.openhab.binding.homewizard/src/main/feature/feature.xml new file mode 100644 index 0000000000000..7e9e4e13a3e2e --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.homewizard/${project.version} + + diff --git a/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardBindingConstants.java b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardBindingConstants.java new file mode 100644 index 0000000000000..928e49b5ba45c --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardBindingConstants.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 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.homewizard.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link HomeWizardBindingConstants} class defines common constants, which are + * used across the full binding. + * + * @author Daniël van Os - Initial contribution + */ +@NonNullByDefault +public class HomeWizardBindingConstants { + + private static final String BINDING_ID = "homewizard"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_P1_WIFI_METER = new ThingTypeUID(BINDING_ID, "p1_wifi_meter"); + + // List of all Channel ids + public static final String CHANNEL_ENERGY_IMPORT_T1 = "total_energy_import_t1"; + public static final String CHANNEL_ENERGY_IMPORT_T2 = "total_energy_import_t2"; + public static final String CHANNEL_ENERGY_EXPORT_T1 = "total_energy_export_t1"; + public static final String CHANNEL_ENERGY_EXPORT_T2 = "total_energy_export_t2"; + public static final String CHANNEL_ACTIVE_POWER = "active_power"; + public static final String CHANNEL_ACTIVE_POWER_L1 = "active_power_l1"; + public static final String CHANNEL_ACTIVE_POWER_L2 = "active_power_l2"; + public static final String CHANNEL_ACTIVE_POWER_L3 = "active_power_l3"; + public static final String CHANNEL_TOTAL_GAS = "total_gas"; + public static final String CHANNEL_GAS_TIMESTAMP = "gas_timestamp"; + + public static final String PROPERTY_METER_MODEL = "meterModel"; + public static final String PROPERTY_METER_VERSION = "meterVersion"; +} diff --git a/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardConfiguration.java b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardConfiguration.java new file mode 100644 index 0000000000000..9385e636462ec --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.homewizard.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HomeWizardConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Daniël van Os - Initial contribution + */ +@NonNullByDefault +public class HomeWizardConfiguration { + + /** + * IP Address or host for the P1 Meter + */ + public String ipAddress = ""; + + /** + * Refresh delay in seconds + */ + public Integer refreshDelay = 5; +} diff --git a/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandler.java b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandler.java new file mode 100644 index 0000000000000..3e47191f37421 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandler.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2010-2021 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.homewizard.internal; + +import java.io.IOException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.net.http.HttpUtil; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +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.binding.BaseThingHandler; +import org.openhab.core.types.Command; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@link HomeWizardHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Daniël van Os - Initial contribution + */ +@NonNullByDefault +public class HomeWizardHandler extends BaseThingHandler { + + private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + + private HomeWizardConfiguration config = new HomeWizardConfiguration(); + private @Nullable ScheduledFuture pollingJob; + + private String apiURL = ""; + private String meterModel = ""; + private int meterVersion = 0; + + /** + * Constructor + * + * @param thing The thing to handle + */ + public HomeWizardHandler(Thing thing) { + super(thing); + } + + /** + * Not listening to any commands. + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + /** + * If a host has been specified start polling it + */ + @Override + public void initialize() { + config = getConfigAs(HomeWizardConfiguration.class); + if (configure()) { + pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, config.refreshDelay, TimeUnit.SECONDS); + } + } + + /** + * Check the current configuration + * + * @return true if the configuration is ok to start polling, false otherwise + */ + private boolean configure() { + if (config.ipAddress.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Missing ipAddress/host configuration"); + return false; + } else { + updateStatus(ThingStatus.UNKNOWN); + apiURL = String.format("http://%s/api/v1/data", config.ipAddress.trim()); + return true; + } + } + + /** + * dispose: stop the poller + */ + @Override + public void dispose() { + var job = pollingJob; + if (job != null) { + job.cancel(true); + } + pollingJob = null; + } + + /** + * The actual polling loop + */ + private void pollingCode() { + final String result; + + try { + result = HttpUtil.executeUrl("GET", apiURL, 30000); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + String.format("Unable to query P1 Meter: %s", e.getMessage())); + return; + } + + if (result.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "P1 Meter API returned empty status"); + return; + } + + P1Payload payload = gson.fromJson(result, P1Payload.class); + if (payload == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unable to parse response from P1 meter"); + return; + } + + if ("".equals(payload.getMeterModel())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Results from API are empty"); + return; + } + + updateStatus(ThingStatus.ONLINE); + + if (!meterModel.equals(payload.getMeterModel())) { + meterModel = payload.getMeterModel(); + updateProperty(HomeWizardBindingConstants.PROPERTY_METER_MODEL, meterModel); + } + + if (meterVersion != payload.getSmrVersion()) { + meterVersion = payload.getSmrVersion(); + updateProperty(HomeWizardBindingConstants.PROPERTY_METER_VERSION, String.format("%d", meterVersion)); + } + + updateState(HomeWizardBindingConstants.CHANNEL_ENERGY_IMPORT_T1, + new QuantityType<>(payload.getTotalEnergyImportT1Kwh(), Units.KILOWATT_HOUR)); + updateState(HomeWizardBindingConstants.CHANNEL_ENERGY_IMPORT_T2, + new QuantityType<>(payload.getTotalEnergyImportT2Kwh(), Units.KILOWATT_HOUR)); + updateState(HomeWizardBindingConstants.CHANNEL_ENERGY_EXPORT_T1, + new QuantityType<>(payload.getTotalEnergyExportT1Kwh(), Units.KILOWATT_HOUR)); + updateState(HomeWizardBindingConstants.CHANNEL_ENERGY_EXPORT_T2, + new QuantityType<>(payload.getTotalEnergyExportT2Kwh(), Units.KILOWATT_HOUR)); + + updateState(HomeWizardBindingConstants.CHANNEL_ACTIVE_POWER, + new QuantityType<>(payload.getActivePowerW(), Units.WATT)); + updateState(HomeWizardBindingConstants.CHANNEL_ACTIVE_POWER_L1, + new QuantityType<>(payload.getActivePowerL1W(), Units.WATT)); + updateState(HomeWizardBindingConstants.CHANNEL_ACTIVE_POWER_L2, + new QuantityType<>(payload.getActivePowerL2W(), Units.WATT)); + updateState(HomeWizardBindingConstants.CHANNEL_ACTIVE_POWER_L3, + new QuantityType<>(payload.getActivePowerL3W(), Units.WATT)); + + updateState(HomeWizardBindingConstants.CHANNEL_TOTAL_GAS, + new QuantityType<>(payload.getTotalGasM3(), SIUnits.CUBIC_METRE)); + + // 210119164000 + long dtv = payload.getGasTimestamp(); + long seconds = dtv % 100; + + dtv /= 100; + long minutes = dtv % 100; + + dtv /= 100; + long hours = dtv % 100; + + dtv /= 100; + long day = dtv % 100; + + dtv /= 100; + long month = dtv % 100; + + dtv /= 100; + long year = dtv + 2000; // Where (When?) have I seen this before? + + DateTimeType dtt = DateTimeType + .valueOf(String.format("%04d-%02d-%02dT%02d:%02d:%02d", year, month, day, hours, minutes, seconds)); + updateState(HomeWizardBindingConstants.CHANNEL_GAS_TIMESTAMP, dtt); + } +} diff --git a/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandlerFactory.java b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandlerFactory.java new file mode 100644 index 0000000000000..9bd4005383f37 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/HomeWizardHandlerFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2021 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.homewizard.internal; + +import static org.openhab.binding.homewizard.internal.HomeWizardBindingConstants.THING_TYPE_P1_WIFI_METER; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link HomeWizardHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Daniël van Os - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.homewizard", service = ThingHandlerFactory.class) +public class HomeWizardHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_P1_WIFI_METER); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_P1_WIFI_METER.equals(thingTypeUID)) { + return new HomeWizardHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/P1Payload.java b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/P1Payload.java new file mode 100644 index 0000000000000..6fd42b7e92ec9 --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/java/org/openhab/binding/homewizard/internal/P1Payload.java @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2010-2021 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.homewizard.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Class that provides storage for the json object obtained from the P1 meter API + * + * @author Daniël van Os - Initial contribution + * + */ +@NonNullByDefault +public class P1Payload { + private int smrVersion = 0; + private String meterModel = ""; + private String wifiSsid = ""; + private int wifiStrength = 0; + + @SerializedName("total_power_import_t1_kwh") + private double totalEnergyImportT1Kwh; + @SerializedName("total_power_import_t2_kwh") + private double totalEnergyImportT2Kwh; + @SerializedName("total_power_export_t1_kwh") + private double totalEnergyExportT1Kwh; + @SerializedName("total_power_export_t2_kwh") + private double totalEnergyExportT2Kwh; + + private double activePowerW; + private double activePowerL1W; + private double activePowerL2W; + private double activePowerL3W; + private double totalGasM3; + private long gasTimestamp; + + /** + * Getter for the smart meter version + * + * @return The most recent smart meter version obtained from the API + */ + public int getSmrVersion() { + return smrVersion; + } + + /** + * Setter for the smart meter version + * + * @param smrVersion The smart meter version to set + */ + public void setSmrVersion(int smrVersion) { + this.smrVersion = smrVersion; + } + + /** + * Getter for the meter model + * + * @return meter model + */ + public String getMeterModel() { + return meterModel; + } + + /** + * Setter for the meter model + * + * @param meterModel meter model + */ + public void setMeterModel(String meterModel) { + this.meterModel = meterModel; + } + + /** + * Getter for the meter's wifi ssid + * + * @return the meter's wifi sid + */ + public String getWifiSsid() { + return wifiSsid; + } + + /** + * Setter for the wifi ssid + * + * @param wifiSsid wifi ssid + */ + public void setWifiSsid(String wifiSsid) { + this.wifiSsid = wifiSsid; + } + + /** + * Getter for the wifi rssi + * + * @return wifi rssi + */ + public int getWifiStrength() { + return wifiStrength; + } + + /** + * Setter for the wifi rssi + * + * @param wifiStrength wifi rssi + */ + public void setWifiStrength(int wifiStrength) { + this.wifiStrength = wifiStrength; + } + + /** + * Getter for the total imported energy on counter 1 + * + * @return total imported energy on counter 1 + */ + public double getTotalEnergyImportT1Kwh() { + return totalEnergyImportT1Kwh; + } + + /** + * Setter for the total imported energy on counter 1 + * + * @param totalEnergyImportT1Kwh total imported energy on counter 1 + */ + public void setTotalEnergyImportT1Kwh(double totalEnergyImportT1Kwh) { + this.totalEnergyImportT1Kwh = totalEnergyImportT1Kwh; + } + + /** + * Getter for the total imported energy on counter 2 + * + * @return total imported energy on counter 2 + */ + public double getTotalEnergyImportT2Kwh() { + return totalEnergyImportT2Kwh; + } + + /** + * Setter for the total imported energy on counter 2 + * + * @param totalEnergyImportT2Kwh + */ + public void setTotalEnergyImportT2Kwh(double totalEnergyImportT2Kwh) { + this.totalEnergyImportT2Kwh = totalEnergyImportT2Kwh; + } + + /** + * Getter for the total exported energy on counter 1 + * + * @return total exported energy on counter 1 + */ + public double getTotalEnergyExportT1Kwh() { + return totalEnergyExportT1Kwh; + } + + /** + * Setter for the total exported energy on counter 1 + * + * @param totalEnergyExportT1Kwh + */ + public void setTotalEnergyExportT1Kwh(double totalEnergyExportT1Kwh) { + this.totalEnergyExportT1Kwh = totalEnergyExportT1Kwh; + } + + /** + * Getter for the total exported energy on counter 2 + * + * @return total exported energy on counter 2 + */ + public double getTotalEnergyExportT2Kwh() { + return totalEnergyExportT2Kwh; + } + + /** + * Setter for the total exported energy on counter 2 + * + * @param totalEnergyExportT2Kwh + */ + public void setTotalEnergyExportT2Kwh(double totalEnergyExportT2Kwh) { + this.totalEnergyExportT2Kwh = totalEnergyExportT2Kwh; + } + + /** + * Getter for the current active total power + * + * @return current active total power + */ + public double getActivePowerW() { + return activePowerW; + } + + /** + * Setter for the current active total power + * + * @param activePowerW + */ + public void setActivePowerW(double activePowerW) { + this.activePowerW = activePowerW; + } + + /** + * Getter for the current active total power on phase 1 + * + * @return current active total power on phase 1 + */ + public double getActivePowerL1W() { + return activePowerL1W; + } + + /** + * Setter for the current active power on phase 1 + * + * @param activePowerL1W current active total power on phase 1 + */ + public void setActivePowerL1W(double activePowerL1W) { + this.activePowerL1W = activePowerL1W; + } + + /** + * Getter for the current active total power on phase 2 + * + * @return current active total power on phase 2 + */ + public double getActivePowerL2W() { + return activePowerL2W; + } + + /** + * Setter for the current active power on phase 2 + * + * @param activePowerL2W current active total power on phase 2 + */ + public void setActivePowerL2W(double activePowerL2W) { + this.activePowerL2W = activePowerL2W; + } + + /** + * Getter for the current active total power on phase 3 + * + * @return current active total power on phase 3 + */ + public double getActivePowerL3W() { + return activePowerL3W; + } + + /** + * Setter for the current active power on phase 3 + * + * @param activePowerL3W current active total power on phase 3 + */ + public void setActivePowerL3W(double activePowerL3W) { + this.activePowerL3W = activePowerL3W; + } + + /** + * Getter for the total imported gas volume + * + * @return total imported gas volume + */ + public double getTotalGasM3() { + return totalGasM3; + } + + /** + * Setter for the total imported gas volume + * + * @param totalGasM3 total imported gas volume + */ + public void setTotalGasM3(double totalGasM3) { + this.totalGasM3 = totalGasM3; + } + + /** + * Getter for the time stamp of the last gas update + * + * @return time stamp of the last gas update + */ + public long getGasTimestamp() { + return gasTimestamp; + } + + /** + * Setter for the time stamp of the last gas update + * + * @param gasTimestamp time stamp of the last gas update + */ + public void setGasTimestamp(long gasTimestamp) { + this.gasTimestamp = gasTimestamp; + } + + @Override + public String toString() { + return String.format("P1 [version: %d model: %s ssid: %s signal: %d" + + " imp1: %f imp2: %f exp1: %f exp2: %f active: %f active1: %f active2: %f active3: %f gas: %f timestamp: %.0f]", + smrVersion, meterModel, wifiSsid, wifiStrength, totalEnergyImportT1Kwh, totalEnergyImportT2Kwh, + totalEnergyExportT1Kwh, totalEnergyExportT2Kwh, activePowerW, activePowerL1W, activePowerL2W, + activePowerL3W, totalGasM3, gasTimestamp); + } +} diff --git a/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..e98bcaa1182cf --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + HomeWizard Binding + This binding provides access to the data provided by the HomeWizard Wi-Fi P1 meter on it's local HTTP + interface. + + diff --git a/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..be54ab3ee034d --- /dev/null +++ b/bundles/org.openhab.binding.homewizard/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,132 @@ + + + + + + This thing provides the measurement data that is available through the http interface of the HomeWizard + Wi-Fi P1 Meter. + + + + + + + + + + + + + + + + Unknown + + + + + + The IP or host name of the P1 Meter. + network-address + + + + The refresh interval in seconds for polling the P1 Meter. + 5 + + + + + + + Number:Energy + + This channel provides the most recently reported total imported energy in kWh by counter 1, most commonly + used for import during the night or weekend. + + + + Number:Energy + + + This channel provides the most recently reported total imported energy in kWh by counter 2, most commonly + used for import during the day. + + + + + Number:Energy + + + This channel provides the most recently reported total exported energy in kWh by counter 1, most commonly + used for export during the night or weekend. + + + + + Number:Energy + + + This channel provides the most recently reported total exported energy in kWh by counter 2, most commonly + used for export during the day. + + + + + Number:Power + + + This channel provides the current net total power in W. It will be below 0 if power is currently being + exported. + + + + + Number:Power + + + This channel provides the current net phase 1 power in W. It will be below 0 if power is currently being + exported. + + + + + Number:Power + + + This channel provides the current net phase 2 power in W. It will be below 0 if power is currently being + exported. It will be 0 for single phase systems. + + + + + Number:Power + + + This channel provides the current net phase 3 power in W. It will be below 0 if power is currently being + exported. It will be 0 for single phase systems. + + + + + Number:Volume + + + This channel provides the most recently reported total imported gas in m^3. It does not get updated as + frequently as the data in the other channels, the gas_timestamp channel provides the time stamp of the most recent + update. + + + + + DateTime + + + This channel provides the time stamp of the total_gas measurement. + + + + diff --git a/bundles/org.openhab.binding.http/README.md b/bundles/org.openhab.binding.http/README.md index 998e394d49ade..c9e5c9ebe4a1e 100644 --- a/bundles/org.openhab.binding.http/README.md +++ b/bundles/org.openhab.binding.http/README.md @@ -23,7 +23,7 @@ It can be extended with different channels. | `commandMethod` | no | GET | Method used for sending commands: `GET`, `PUT`, `POST`. | | `contentType` | yes | - | MIME content-type of the command requests. Only used for `PUT` and `POST`. | | `encoding` | yes | - | Encoding to be used if no encoding is found in responses (advanced parameter). | -| `headers` | yes | - | Additional headers that are sent along with the request. Format is "header=value".| +| `headers` | yes | - | Additional headers that are sent along with the request. Format is "header=value". Multiple values can be stored as `headers="key1=value1", "key2=value2", "key3=value3",`| | `ignoreSSLErrors` | no | false | If set to true ignores invalid SSL certificate errors. This is potentially dangerous.| *Note:* Optional "no" means that you have to configure a value unless a default is provided and you are ok with that setting. @@ -169,3 +169,17 @@ is transformed to ``` http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06 ``` + +## Examples + +### `demo.things` + +``` +Thing http:url:foo "Foo" [ + baseURL="https://example.com/api/v1/metadata-api/web/metadata", + headers="key1=value1", "key2=value2", "key3=value3", + refresh=15] { + Channels: + Type string : text "Text" [ stateTransformation="JSONPATH:$.metadata.data" ] +} +``` diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java index 3318fb9d4385c..064984de0b398 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.http.internal; -import static org.openhab.binding.http.internal.HttpBindingConstants.*; +import static org.openhab.binding.http.internal.HttpBindingConstants.THING_TYPE_URL; import java.util.Set; @@ -59,8 +59,8 @@ public class HttpHandlerFactory extends BaseThingHandlerFactory @Activate public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory, @Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) { - this.secureClient = new HttpClient(new SslContextFactory()); - this.insecureClient = new HttpClient(new SslContextFactory(true)); + this.secureClient = new HttpClient(new SslContextFactory.Client()); + this.insecureClient = new HttpClient(new SslContextFactory.Client(true)); try { this.secureClient.start(); this.insecureClient.start(); diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java index eb2ad57b3bc8d..4a358b04abea6 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java @@ -81,6 +81,16 @@ public String toString(Command command) { @Override public State toState(String string) { + if (string.equals(channelConfig.playValue)) { + return PlayPauseType.PLAY; + } else if (string.equals(channelConfig.pauseValue)) { + return PlayPauseType.PAUSE; + } else if (string.equals(channelConfig.rewindValue)) { + return RewindFastforwardType.REWIND; + } else if (string.equals(channelConfig.fastforwardValue)) { + return RewindFastforwardType.FASTFORWARD; + } + return UnDefType.UNDEF; } } diff --git a/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml index 456cebfd7721a..6a79246e350fc 100644 --- a/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml @@ -7,11 +7,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -44,11 +46,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -113,11 +117,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -158,11 +164,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -230,11 +238,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -272,11 +282,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". @@ -333,11 +345,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values Chain multiple transformations with the mathematical + intersection character "∩".. @@ -386,11 +400,13 @@ - Transformation pattern used when receiving values. + Transformation pattern used when receiving values. Chain multiple transformations with the mathematical + intersection character "∩". - Transformation pattern used when sending values. + Transformation pattern used when sending values. Chain multiple transformations with the mathematical + intersection character "∩". diff --git a/bundles/org.openhab.binding.hue/README.md b/bundles/org.openhab.binding.hue/README.md index 9c2d07e512beb..f809c21f4f3a4 100644 --- a/bundles/org.openhab.binding.hue/README.md +++ b/bundles/org.openhab.binding.hue/README.md @@ -367,3 +367,11 @@ if (receivedEvent == "1000.0")) { //do stuff } ``` + +### UPnP Discovery: Inbox 'Grace Period' + +The Hue Bridge can sometimes be late in sending its UPnP 'ssdp:alive' notifications even though it has not really gone offline. +This means that the Hue Bridge could be repeatedly removed from, and (re)added to, the InBox. +Which would lead to confusion in the UI, and repeated logger messages. +To prevent this, the binding tells the OpenHAB core to wait for a further period of time ('grace period') before actually removing the Bridge from the Inbox. +The 'grace period' has a default value of 50 seconds, but it can be fine tuned in the main UI via Settings | Bindings | Hue | Configure. diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index 8f38f52b9ee29..90949968cb4ed 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -84,6 +84,9 @@ public class HueBindingConstants { public static final String EVENT_DIMMER_SWITCH = "dimmer_switch_event"; public static final String EVENT_TAP_SWITCH = "tap_switch_event"; + // Binding configuration properties + public static final String REMOVAL_GRACE_PERIOD = "removalGracePeriod"; + // Bridge config properties public static final String HOST = "ipAddress"; public static final String PORT = "port"; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java index f914e758d04ce..14b25f8cf17f0 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java @@ -844,7 +844,7 @@ protected Result doNetwork(String address, String requestMethod, @Nullable Strin return super.doNetwork(address, requestMethod, body); } else { String extractedAddress = Util.quickMatch("^http://[^/]+(.+)$", address); - JsonElement commandBody = new JsonParser().parse(body); + JsonElement commandBody = body == null ? null : JsonParser.parseString(body); scheduleCommand = new ScheduleCommand(extractedAddress, requestMethod, commandBody); // Return a fake result that will cause an exception and the callback to end diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java index 3c695720ea36e..ce5aef384cb49 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java @@ -75,10 +75,10 @@ public void execute(String[] args, Console console) { console.println("Bad thing id '" + args[0] + "'"); printUsage(console); } else if (thingHandler == null) { - console.println("No handler initialized for the thing id '" + args[0] + "'"); + console.println("No handler initialized for the thingUID '" + args[0] + "'"); printUsage(console); } else if (bridgeHandler == null && groupHandler == null) { - console.println("'" + args[0] + "' is neither a hue bridge id nor a hue group thing id"); + console.println("'" + args[0] + "' is neither a Hue bridgeUID nor a Hue groupThingUID"); printUsage(console); } else { switch (args[1]) { @@ -87,7 +87,7 @@ public void execute(String[] args, Console console) { String userName = bridgeHandler.getUserName(); console.println("Your user name is " + (userName != null ? userName : "undefined")); } else { - console.println("'" + args[0] + "' is not a hue bridge id"); + console.println("'" + args[0] + "' is not a Hue bridgeUID"); printUsage(console); } break; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java index 278b717ac7186..7018c62917b3a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java @@ -15,7 +15,9 @@ import static org.openhab.binding.hue.internal.HueBindingConstants.*; import static org.openhab.core.thing.Thing.PROPERTY_SERIAL_NUMBER; +import java.io.IOException; import java.util.Collections; +import java.util.Dictionary; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -25,13 +27,20 @@ import org.jupnp.model.meta.DeviceDetails; import org.jupnp.model.meta.ModelDetails; import org.jupnp.model.meta.RemoteDevice; +import org.openhab.binding.hue.internal.HueBindingConstants; 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.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link HueBridgeDiscoveryParticipant} is responsible for discovering new and @@ -44,6 +53,18 @@ @Component(service = UpnpDiscoveryParticipant.class) public class HueBridgeDiscoveryParticipant implements UpnpDiscoveryParticipant { + private final Logger logger = LoggerFactory.getLogger(HueBridgeDiscoveryParticipant.class); + + // Hue bridges have maxAge 100 seconds, so set the default grace period to half of that + private long removalGracePeriodSeconds = 50; + + private final ConfigurationAdmin configAdmin; + + @Activate + public HueBridgeDiscoveryParticipant(final @Reference ConfigurationAdmin configAdmin) { + this.configAdmin = configAdmin; + } + @Override public Set getSupportedThingTypeUIDs() { return Collections.singleton(THING_TYPE_BRIDGE); @@ -92,4 +113,22 @@ public Set getSupportedThingTypeUIDs() { } return null; } + + @Override + public long getRemovalGracePeriodSeconds(RemoteDevice device) { + try { + Configuration conf = configAdmin.getConfiguration("binding.hue"); + Dictionary properties = conf.getProperties(); + if (properties != null) { + Object property = properties.get(HueBindingConstants.REMOVAL_GRACE_PERIOD); + if (property != null) { + removalGracePeriodSeconds = Long.parseLong(property.toString()); + } + } + } catch (IOException | IllegalStateException | NumberFormatException e) { + // fall through to pre-initialised (default) value + } + logger.trace("getRemovalGracePeriodSeconds={}", removalGracePeriodSeconds); + return removalGracePeriodSeconds; + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java index a8e87730afe9b..27cdb5f0af72e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java @@ -16,6 +16,7 @@ import java.math.BigDecimal; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledFuture; @@ -60,6 +61,7 @@ @NonNullByDefault public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener { public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GROUP); + public static final String PROPERTY_MEMBERS = "members"; private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class); private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; @@ -123,6 +125,14 @@ private void initializeThing(@Nullable ThingStatus bridgeStatus) { } } + private synchronized void initializeProperties(@Nullable FullGroup fullGroup) { + if (fullGroup != null) { + Map properties = editProperties(); + properties.put(PROPERTY_MEMBERS, fullGroup.getLightIds().stream().collect(Collectors.joining(","))); + updateProperties(properties); + } + } + @Override public void dispose() { logger.debug("Hue group handler disposes. Unregistering listener."); @@ -379,6 +389,8 @@ public boolean onGroupStateChanged(FullGroup group) { logger.trace("New state for group {}", groupId); + initializeProperties(group); + lastSentColorTemp = null; lastSentBrightness = null; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java index 336791740865f..fcd6d37f43035 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java @@ -81,15 +81,10 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG); - private static final Map> VENDOR_MODEL_MAP = Map.of( // - "Philips", List.of("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010", // - "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007", // - "LWL001"), - "OSRAM", List.of("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01")); - - private static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW"; + public static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW"; private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class); + private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; private @NonNullByDefault({}) String lightId; @@ -170,13 +165,8 @@ private synchronized void initializeProperties(@Nullable FullLight fullLight) { String modelId = fullLight.getNormalizedModelID(); if (modelId != null) { properties.put(PROPERTY_MODEL_ID, modelId); - String vendor = getVendor(modelId); - if (vendor != null) { - properties.put(PROPERTY_VENDOR, vendor); - } - } else { - properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName()); } + properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName()); properties.put(PRODUCT_NAME, fullLight.getProductName()); String uniqueID = fullLight.getUniqueID(); if (uniqueID != null) { @@ -215,15 +205,6 @@ private void initializeCapabilities(@Nullable FullLight fullLight) { } } - private @Nullable String getVendor(String modelId) { - for (String vendor : VENDOR_MODEL_MAP.keySet()) { - if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) { - return vendor; - } - } - return null; - } - @Override public void dispose() { logger.debug("Hue light handler disposes. Unregistering listener."); diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/binding/binding.xml index 90daae7d40426..87ae19a6aa0b8 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/binding/binding.xml @@ -3,8 +3,16 @@ 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"> - hue Binding - The hue Binding integrates the Philips hue system. It - allows to control hue bulbs. + Hue Binding + The Hue Binding integrates the Philips Hue system. It allows to control Hue bulbs. + + + + + Extra grace period (seconds) that UPnP discovery shall wait before removing a lost Bridge from the + Inbox. Default is 50 seconds. + 50 + + diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties index b40602f287905..378433e0837df 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties @@ -103,6 +103,8 @@ channel-type.hue.alert.state.option.SELECT = Einmaliges Blinken channel-type.hue.alert.state.option.LSELECT = Mehrfaches Blinken channel-type.hue.effect.label = Farbeffekt channel-type.hue.effect.description = Erm�glicht einen automatischen Farbwechsels. +channel-type.hue.scene.label = Szene +channel-type.hue.scene.description = Erm�glicht das Anwenden einer Szene f�r alle Lichter, die zur Gruppe geh�ren. channel-type.hue.last_updated.label = Letzte Aktualisierung channel-type.hue.last_updated.description = Zeit, zu der sich dieser Wert ge�ndert hat. diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java index 2d62bbe8a5506..58adabcb26909 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java @@ -17,7 +17,7 @@ import static org.mockito.Mockito.*; import static org.openhab.binding.hue.internal.HueBindingConstants.*; -import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -62,8 +62,9 @@ public class HueLightHandlerTest { private static final int MAX_COLOR_TEMPERATURE = 500; private static final int COLOR_TEMPERATURE_RANGE = MAX_COLOR_TEMPERATURE - MIN_COLOR_TEMPERATURE; - private static final String OSRAM_MODEL_TYPE = "PAR16 50 TW"; - private static final String OSRAM_MODEL_TYPE_ID = "PAR16_50_TW"; + private static final String OSRAM = "OSRAM"; + private static final String OSRAM_MODEL_TYPE = HueLightHandler.OSRAM_PAR16_50_TW_MODEL_ID; + private static final String OSRAM_MODEL_TYPE_ID = HueLightHandler.OSRAM_PAR16_50_TW_MODEL_ID; private Gson gson; @@ -75,25 +76,26 @@ public void setUp() { @Test public void assertCommandForOsramPar1650ForColorTemperatureChannelOn() { String expectedReply = "{\"on\" : true, \"bri\" : 254}"; - assertSendCommandForColorTempForPar16(OnOffType.ON, new HueLightState(OSRAM_MODEL_TYPE), expectedReply); + assertSendCommandForColorTempForPar16(OnOffType.ON, new HueLightState(OSRAM_MODEL_TYPE, OSRAM), expectedReply); } @Test public void assertCommandForOsramPar1650ForColorTemperatureChannelOff() { String expectedReply = "{\"on\" : false, \"transitiontime\" : 0}"; - assertSendCommandForColorTempForPar16(OnOffType.OFF, new HueLightState(OSRAM_MODEL_TYPE), expectedReply); + assertSendCommandForColorTempForPar16(OnOffType.OFF, new HueLightState(OSRAM_MODEL_TYPE, OSRAM), expectedReply); } @Test public void assertCommandForOsramPar1650ForBrightnessChannelOn() { String expectedReply = "{\"on\" : true, \"bri\" : 254}"; - assertSendCommandForBrightnessForPar16(OnOffType.ON, new HueLightState(OSRAM_MODEL_TYPE), expectedReply); + assertSendCommandForBrightnessForPar16(OnOffType.ON, new HueLightState(OSRAM_MODEL_TYPE, OSRAM), expectedReply); } @Test public void assertCommandForOsramPar1650ForBrightnessChannelOff() { String expectedReply = "{\"on\" : false, \"transitiontime\" : 0}"; - assertSendCommandForBrightnessForPar16(OnOffType.OFF, new HueLightState(OSRAM_MODEL_TYPE), expectedReply); + assertSendCommandForBrightnessForPar16(OnOffType.OFF, new HueLightState(OSRAM_MODEL_TYPE, OSRAM), + expectedReply); } @Test @@ -341,12 +343,12 @@ public void assertCommandForEffectChannel() { private void assertSendCommandForColorTempForPar16(Command command, HueLightState currentState, String expectedReply) { - assertSendCommand(CHANNEL_COLORTEMPERATURE, command, currentState, expectedReply, OSRAM_MODEL_TYPE_ID, "OSRAM"); + assertSendCommand(CHANNEL_COLORTEMPERATURE, command, currentState, expectedReply, OSRAM_MODEL_TYPE_ID, OSRAM); } private void assertSendCommandForBrightnessForPar16(Command command, HueLightState currentState, String expectedReply) { - assertSendCommand(CHANNEL_BRIGHTNESS, command, currentState, expectedReply, OSRAM_MODEL_TYPE_ID, "OSRAM"); + assertSendCommand(CHANNEL_BRIGHTNESS, command, currentState, expectedReply, OSRAM_MODEL_TYPE_ID, OSRAM); } private void assertSendCommandForColor(Command command, HueLightState currentState, String expectedReply) { @@ -390,7 +392,7 @@ private void assertSendCommand(String channel, Command command, HueLightState cu when(mockBridge.getStatus()).thenReturn(ThingStatus.ONLINE); Thing mockThing = mock(Thing.class); - when(mockThing.getConfiguration()).thenReturn(new Configuration(Collections.singletonMap(LIGHT_ID, "1"))); + when(mockThing.getConfiguration()).thenReturn(new Configuration(Map.of(LIGHT_ID, "1"))); HueClient mockClient = mock(HueClient.class); when(mockClient.getLightById(any())).thenReturn(light); @@ -423,9 +425,8 @@ protected Bridge getBridge() { } private void assertJson(String expected, String actual) { - JsonParser parser = new JsonParser(); - JsonElement jsonExpected = parser.parse(expected); - JsonElement jsonActual = parser.parse(actual); + JsonElement jsonExpected = JsonParser.parseString(expected); + JsonElement jsonActual = JsonParser.parseString(actual); assertEquals(jsonExpected, jsonActual); } } diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightState.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightState.java index f7dc42f03e5ad..1d99ee7fd1ca5 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightState.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightState.java @@ -33,12 +33,14 @@ public class HueLightState { String effect = "none"; String colorMode = "hs"; String model = "LCT001"; + String vendor = "Philips"; public HueLightState() { } - public HueLightState(String model) { + public HueLightState(String model, String vendor) { this.model = model; + this.vendor = vendor; } public HueLightState bri(int brightness) { @@ -105,6 +107,7 @@ public String toString() { " \"type\": \"Extended color light\"," + // " \"name\": \"Hue Light 1\"," + // " \"modelid\": \"" + model + "\"," + // + " \"manufacturername\": \"" + vendor + "\"," + // " \"swversion\": \"65003148\"," + // " \"uniqueid\": \"00:17:88:01:00:e1:88:29-0b\"," + // " \"pointsymbol\": {" + // diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java index ea64e615fb76e..932d888461519 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java @@ -16,11 +16,14 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException; @@ -210,8 +213,9 @@ protected void updateZones(LocalScheduleResponse status) { updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type)); updateGroupState(group, CHANNEL_ZONE_TIME, r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF); - if (StringUtils.isNotBlank(r.icon)) { - updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + r.icon)); + String icon = r.icon; + if (icon != null && !icon.isBlank()) { + updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon)); } if (r.time >= MAX_RUN_TIME) { updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF); diff --git a/bundles/org.openhab.binding.hyperion/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.hyperion/src/main/resources/OH-INF/thing/thing-types.xml index 936630447f477..99dcfb1911447 100644 --- a/bundles/org.openhab.binding.hyperion/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.hyperion/src/main/resources/OH-INF/thing/thing-types.xml @@ -77,7 +77,6 @@ How often (in seconds) to poll the Hyperion server for value changes. - true 3 diff --git a/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/Iammeter3080THandler.java b/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/Iammeter3080THandler.java index 4a2f380b0788e..3c8787453c16d 100644 --- a/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/Iammeter3080THandler.java +++ b/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/Iammeter3080THandler.java @@ -41,7 +41,7 @@ public Iammeter3080THandler(Thing thing) { @SuppressWarnings("null") @Override protected void resolveData(String response) { - JsonElement iammeterDataElement = new JsonParser().parse(response); + JsonElement iammeterDataElement = JsonParser.parseString(response); JsonObject iammeterData = iammeterDataElement.getAsJsonObject(); String keyWord = "Datas"; if (iammeterData.has("Datas") && iammeterData.has("SN")) { diff --git a/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/IammeterHandler.java b/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/IammeterHandler.java index c26360cdc6780..305a4862c3182 100644 --- a/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/IammeterHandler.java +++ b/bundles/org.openhab.binding.iammeter/src/main/java/org/openhab/binding/iammeter/internal/IammeterHandler.java @@ -37,7 +37,7 @@ public IammeterHandler(Thing thing) { @Override protected void resolveData(String response) { - JsonElement iammeterDataElement = new JsonParser().parse(response); + JsonElement iammeterDataElement = JsonParser.parseString(response); JsonObject iammeterData = iammeterDataElement.getAsJsonObject(); String keyWord = "Data"; if (iammeterData.has("data") || (iammeterData.has("Data") && iammeterData.has("SN"))) { diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java index 50ed769d62beb..c928bd6d04606 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java @@ -32,7 +32,6 @@ import javax.measure.Unit; import javax.measure.quantity.Temperature; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -268,7 +267,7 @@ private void configure() { String confSerialId = configuration.serialId; String confApiKey = configuration.apiKey; - if (StringUtils.isNotBlank(confApiKey)) { + if (confApiKey != null && !confApiKey.isBlank()) { this.apiKey = confApiKey; } else { this.apiKey = DEFAULT_API_KEY; @@ -291,7 +290,7 @@ private void configure() { return; } - if (StringUtils.isNotBlank(confSerialId)) { + if (confSerialId != null && !confSerialId.isBlank()) { serialNumber = confSerialId.replaceAll("[^a-zA-Z0-9]", "").toLowerCase(); if (!Arrays.stream(devices).anyMatch(device -> device.getSerialNumber().equals(serialNumber))) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -438,8 +437,7 @@ private void updatedState(String name, @Nullable String value) { */ private State toState(String name, @Nullable String type, @Nullable String value) { try { - // @nullable checker does not recognize isBlank as checking null here, so must use == null to make happy - if (value == null || StringUtils.isBlank(value)) { + if (value == null || value.isBlank()) { return UnDefType.UNDEF; } diff --git a/bundles/org.openhab.binding.icalendar/README.md b/bundles/org.openhab.binding.icalendar/README.md index 66fc2b8303d24..7a6c3db6898b9 100644 --- a/bundles/org.openhab.binding.icalendar/README.md +++ b/bundles/org.openhab.binding.icalendar/README.md @@ -31,17 +31,17 @@ Each `calendar` thing requires the following configuration parameters: Each `eventfilter` thing requires a bridge of type `calendar` and has following configuration options: -| parameter name | description | optional | -|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| -| `maxEvents` | The count of expected results. | mandatory | -| `refreshTime` | The frequency in minutes the channels get refreshed. | mandatory (default available) | -| `datetimeUnit` | A unit for time settings in this filter. Valid values: `MINUTE`, `HOUR`, `DAY` and `WEEK`. | optional (required for time-based filtering) | -| `datetimeStart` | The start of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. | optional | -| `datetimeEnd` | The end of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. The value must be greater than `datetimeStart` to get results. | optional | -| `datetimeRound` | Whether to round the datetimes of start and end down to the earlier time unit. Example if set: current time is 13:00, timeunit is set to `DAY`. Resulting search will start and end at 0:00. | optional | -| `textEventField` | A field to filter the events text-based. Valid values: `SUMMARY`, `DESCRIPTION`, `COMMENT`, `CONTACT` and `LOCATION` (as described in RFC 5545). | optional/required for text-based filtering | -| `textEventValue` | The text to filter events with. | optional | -| `textValueType` | The type of the text to filter with. Valid values: `TEXT` (field must contain value), `REGEX` (field must match value, completely, dot matches all, case insensetive). | optional/required for text-based filtering | +| parameter name | description | optional | +|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| +| `maxEvents` | The count of expected results. | mandatory | +| `refreshTime` | The frequency in minutes the channels get refreshed. | mandatory (default available) | +| `datetimeUnit` | A unit for time settings in this filter. Valid values: `MINUTE`, `HOUR`, `DAY` and `WEEK`. | optional (required for time-based filtering) | +| `datetimeStart` | The start of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. | optional | +| `datetimeEnd` | The end of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. The value must be greater than `datetimeStart` to get results. | optional | +| `datetimeRound` | Whether to round the datetimes of start and end down to the earlier time unit. Example if set: current time is 13:00, timeunit is set to `DAY`. Resulting search will start and end at 0:00. | optional | +| `textEventField` | A field to filter the events text-based. Valid values: `SUMMARY`, `DESCRIPTION`, `COMMENT`, `CONTACT` and `LOCATION` (as described in RFC 5545). | optional/required for text-based filtering | +| `textEventValue` | The text to filter events with. | optional | +| `textValueType` | The type of the text to filter with. Valid values: `TEXT` (field must contain value, case insensitive), `REGEX` (field must match value, completely, dot matches all, usually case sensitive). | optional/required for text-based filtering | ## Channels @@ -167,11 +167,6 @@ BEGIN:Calendar_Test_Switch:ON END:Calendar_Test_Switch:OFF ``` -### Notes for Nextcloud - -The `url` should be: `https:///remote.php/dav/calendars//?export`, so the `?export` is important to get an `ical` file from the calendar. -Username and password for the nextcloud account have to be set as well. - ## Breaking changes In OH3 `calendar` was changed from Thing to Bridge. You need to recreate calendars (or replace `Thing` by `Bridge` in your `.things` file). diff --git a/bundles/org.openhab.binding.icalendar/pom.xml b/bundles/org.openhab.binding.icalendar/pom.xml index 0dd53f8983d4c..610195fd2cc59 100644 --- a/bundles/org.openhab.binding.icalendar/pom.xml +++ b/bundles/org.openhab.binding.icalendar/pom.xml @@ -11,7 +11,6 @@ openHAB Add-ons :: Bundles :: iCalendar Binding jackson-core,jackson-annotations,jackson-databind - 2.10.3 @@ -20,6 +19,12 @@ biweekly 0.6.4 compile + + + com.fasterxml.jackson.core + * + +
diff --git a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml index 8aaccfde118e8..65c9feb0fd6bc 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml @@ -154,14 +154,12 @@ - + - true - + The frequency in minutes the channels get refreshed - true 15 diff --git a/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties b/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties new file mode 100644 index 0000000000000..2b6beab311a3f --- /dev/null +++ b/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties @@ -0,0 +1,34 @@ +# Binding +icloud.binding.name=iCloud Binding +icloud.binding.description=Die Apple iCloud wird genutzt, um Daten wie den Ladezustand oder den Standort von einem oder mehreren Apple Geräten zu erhalten, die mit einem iCloud Account verknüpft sind. + +# Account Thing +icloud.account-thing.label=iCloud Account +icloud.account-thing.description=Der iCloud Account (Bridge) repräsentiert einen iCloud Account. Du benötigst mehrere iCloud Account Bridges, um mehrere iCloud Accounts zu verwalten. + +icloud.account-thing.parameter.apple-id.label=Apple-ID +icloud.account-thing.parameter.apple-id.description=Apple-ID (E-Mail Adresse), um Zugriff zur iCloud zu erhalten. +icloud.account-thing.parameter.password.label=Passwort +icloud.account-thing.parameter.password.description=Passwort der Apple-ID, um Zugriff zur iCloud zu erhalten. +icloud.account-thing.parameter.refresh.label=Aktualisierungszeit in Minuten +icloud.account-thing.parameter.refresh.description=Zeit in der die iCloud Informationen aktualisiert werden sollen. + +icloud.account-thing.property.owner=Besitzer + +# Device Thing +icloud.device-thing.label=iCloud Gerät +icloud.device-thing.description=Das iCloud Gerät (Thing) repräsentiert ein mit der iCloud verknüpftes Apple Gerät, wie zum Beispiel ein iPhone. Es muss mit einem iCloud Account (Bridge) verknüpft werden, um Aktualisierungen zu erhalten. Mehrere iCloud Geräte können mit einem iCloud Account (Bridge) verknüpft werden. + +icloud.device-thing.parameter.id.label=Geräte-ID + +icloud.device-thing.channel.battery-status.label=Ladezustand +icloud.device-thing.channel.battery-status.state.not-charging=Lädt nicht +icloud.device-thing.channel.battery-status.state.charged=Aufgeladen +icloud.device-thing.channel.battery-status.state.charging=Lädt +icloud.device-thing.channel.battery-status.state.unknown=Unbekannt +icloud.device-thing.channel.find-my-phone.label=Wo ist? +icloud.device-thing.channel.location.label=Standort +icloud.device-thing.channel.location-accuracy=Standort Genauigkeit +icloud.device-thing.channel.location-last-update=Letztes Standort Update + +icloud.device-thing.property.device-name=Gerätename diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ChannelUtils.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ChannelUtils.java index 0ca762a59df10..2bb837d5fc58d 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ChannelUtils.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ChannelUtils.java @@ -19,7 +19,6 @@ import java.util.Set; import java.util.function.Predicate; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.ihc.internal.config.ChannelParams; import org.openhab.binding.ihc.internal.ws.exeptions.ConversionException; import org.openhab.core.config.core.Configuration; @@ -203,7 +202,7 @@ private static void addChannelsFromProjectFile(Thing thing, NodeList nodes, Stri ChannelUID channelUID = new ChannelUID(thing.getUID(), group + resourceId); ChannelTypeUID type = new ChannelTypeUID(BINDING_ID, channelType); Configuration configuration = new Configuration(); - configuration.put(PARAM_RESOURCE_ID, new Integer(resourceId)); + configuration.put(PARAM_RESOURCE_ID, Integer.valueOf(resourceId)); Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withConfiguration(configuration) .withLabel(description).withType(type).build(); @@ -214,16 +213,16 @@ private static void addChannelsFromProjectFile(Thing thing, NodeList nodes, Stri private static String createDescription(String name1, String name2, String name3, String name4) { String description = ""; - if (StringUtils.isNotEmpty(name1)) { + if (name1 != null && !name1.isEmpty()) { description = name1; } - if (StringUtils.isNotEmpty(name2)) { + if (name2 != null && !name2.isEmpty()) { description += String.format(" - %s", name2); } - if (StringUtils.isNotEmpty(name3)) { + if (name3 != null && !name3.isEmpty()) { description += String.format(" - %s", name3); } - if (StringUtils.isNotEmpty(name4)) { + if (name4 != null && !name4.isEmpty()) { description += String.format(" - %s", name4); } return description; diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/services/IhcResourceInteractionService.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/services/IhcResourceInteractionService.java index 8f7b7a8ebd41d..93439b0aa6ac1 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/services/IhcResourceInteractionService.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/services/IhcResourceInteractionService.java @@ -20,7 +20,6 @@ import javax.xml.xpath.XPathExpressionException; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.ihc.internal.ws.datatypes.XPathUtils; import org.openhab.binding.ihc.internal.ws.exeptions.IhcExecption; import org.openhab.binding.ihc.internal.ws.http.IhcConnectionPool; @@ -92,12 +91,12 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // parse resource id String resourceId = XPathUtils.getSpeficValueFromNode(n, "ns1:resourceID"); - if (StringUtils.isNotBlank(resourceId)) { + if (resourceId != null && !resourceId.isBlank()) { int id = Integer.parseInt(resourceId); // Parse floating point value String floatingPointValue = getValue(n, "floatingPointValue"); - if (StringUtils.isNotBlank(floatingPointValue)) { + if (floatingPointValue != null && !floatingPointValue.isBlank()) { String min = getValue(n, "minimumValue"); String max = getValue(n, "maximumValue"); return new WSFloatingPointValue(id, Double.valueOf(floatingPointValue), Double.valueOf(min), @@ -106,13 +105,13 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // Parse boolean value String value = getValue(n, "value"); - if (StringUtils.isNotBlank(value)) { + if (value != null && !value.isBlank()) { return new WSBooleanValue(id, Boolean.valueOf(value)); } // Parse integer value String integer = getValue(n, "integer"); - if (StringUtils.isNotBlank(integer)) { + if (integer != null && !integer.isBlank()) { String min = getValue(n, "minimumValue"); String max = getValue(n, "maximumValue"); return new WSIntegerValue(id, Integer.valueOf(integer), Integer.valueOf(min), Integer.valueOf(max)); @@ -120,13 +119,13 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // Parse timer value String milliseconds = getValue(n, "milliseconds"); - if (StringUtils.isNotBlank(milliseconds)) { + if (milliseconds != null && !milliseconds.isBlank()) { return new WSTimerValue(id, Integer.valueOf(milliseconds)); } // Parse time value String hours = getValue(n, "hours"); - if (StringUtils.isNotBlank(hours)) { + if (hours != null && !hours.isBlank()) { String minutes = getValue(n, "minutes"); String seconds = getValue(n, "seconds"); return new WSTimeValue(id, Integer.valueOf(hours), Integer.valueOf(minutes), Integer.valueOf(seconds)); @@ -134,7 +133,7 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // Parse date value String year = getValue(n, "year"); - if (StringUtils.isNotBlank(year)) { + if (year != null && !year.isBlank()) { String month = getValue(n, "month"); String day = getValue(n, "day"); return new WSDateValue(id, Short.valueOf(year), Byte.valueOf(month), Byte.valueOf(day)); @@ -142,7 +141,7 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // Parse enum value String definitionTypeID = getValue(n, "definitionTypeID"); - if (StringUtils.isNotBlank(definitionTypeID)) { + if (definitionTypeID != null && !definitionTypeID.isBlank()) { String enumValueID = getValue(n, "enumValueID"); String enumName = getValue(n, "enumName"); return new WSEnumValue(id, Integer.valueOf(definitionTypeID), Integer.valueOf(enumValueID), enumName); @@ -150,7 +149,7 @@ private WSResourceValue parseResourceValue(Node n) throws XPathExpressionExcepti // Parse week day value value = getValue(n, "weekdayNumber"); - if (StringUtils.isNotBlank(value)) { + if (value != null && !value.isBlank()) { return new WSWeekdayValue(id, Integer.valueOf(value)); } diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java index 5b0a5caa11a5c..61aa8d650d743 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java @@ -149,7 +149,7 @@ public void onMessage(String msg) { } WebSocketClient startWebSocketClient() throws Exception { - WebSocketClient client = new WebSocketClient(new SslContextFactory()); + WebSocketClient client = new WebSocketClient(new SslContextFactory.Client()); client.setMaxIdleTimeout(this.maxIdleTimeout); client.start(); return client; diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/client/InnogyClient.java b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/client/InnogyClient.java index 3a841962a3270..484d99add167f 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/client/InnogyClient.java +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/client/InnogyClient.java @@ -16,13 +16,14 @@ import java.io.IOException; import java.net.URI; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -180,7 +181,8 @@ public AccessTokenResponse getAccessTokenResponse() throws AuthenticationExcepti } catch (OAuthException | OAuthResponseException e) { throw new AuthenticationException("Error fetching access token: " + e.getMessage()); } - if (accessTokenResponse == null || StringUtils.isBlank(accessTokenResponse.getAccessToken())) { + if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null + || accessTokenResponse.getAccessToken().isBlank()) { throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?"); } return accessTokenResponse; diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/handler/InnogyBridgeHandler.java b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/handler/InnogyBridgeHandler.java index a9e74b1b7faf8..65943b322f4d5 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/handler/InnogyBridgeHandler.java +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/handler/InnogyBridgeHandler.java @@ -20,11 +20,19 @@ import java.net.URI; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; -import java.util.*; -import java.util.concurrent.*; - -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.exception.ExceptionUtils; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -183,7 +191,7 @@ private void initializeClient() { * @return true if success */ private boolean checkOnAuthCode() { - if (StringUtils.isNotBlank(bridgeConfiguration.authcode)) { + if (!bridgeConfiguration.authcode.isBlank()) { logger.debug("Trying to get access and refresh tokens"); try { oAuthService.getAccessTokenResponseByAuthorizationCode(bridgeConfiguration.authcode, @@ -868,7 +876,7 @@ public void commandSetRollerShutterLevel(final String deviceId, final int roller /** * Sends the command to start or stop moving the rollershutter (ISR2) in a specified direction - * + * * @param deviceId * @param action */ @@ -970,7 +978,7 @@ private void refreshAccessToken() { /** * Checks if the job is already (re-)scheduled. - * + * * @param job job to check * @return true, when the job is already (re-)scheduled, otherwise false */ diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/config/config.xml index 144312b14ba18..023d5a5157042 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/config/config.xml @@ -6,10 +6,9 @@ https://openhab.org/schemas/config-description-1.0.0.xsd"> - + The identifier uniquely identifies this device. - true diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/thing/bridge.xml index 97744958fb52b..a6850a5eaaaa7 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/OH-INF/thing/bridge.xml @@ -43,7 +43,6 @@ ... to generate an auth-code and paste it here. After initial authorization, this code is not needed anymore.]]> - false seconds The WebSocket is the connection to the innogy service that listens to status updates. If no data is diff --git a/bundles/org.openhab.binding.intesis/README.md b/bundles/org.openhab.binding.intesis/README.md index 78ec5640adc65..300c6b4b4f5bb 100644 --- a/bundles/org.openhab.binding.intesis/README.md +++ b/bundles/org.openhab.binding.intesis/README.md @@ -36,7 +36,7 @@ The binding uses the following configuration parameters. | mode | String | The heating/cooling mode. | AUTO,HEAT,DRY,FAN,COOL | | fanSpeed | String | Fan speed (if applicable) | AUTO,1-10 | | vanesUpDown | String | Control of up/down vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | -| vanesUpDown | String | Control of left/right vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | +| vanesLeftRight | String | Control of left/right vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | | targetTemperature | Number:Temperature | The currently set target temperature (if applicable) | range between 18°C and 30°C | | ambientTemperature | Number:Temperature | (Readonly) The ambient air temperature (if applicable) | | | outdoorTemperature | Number:Temperature | (Readonly) The outdoor air temperature (if applicable) | | diff --git a/bundles/org.openhab.binding.ipcamera/README.md b/bundles/org.openhab.binding.ipcamera/README.md index fd826ff21c0d0..44ad00c73830d 100644 --- a/bundles/org.openhab.binding.ipcamera/README.md +++ b/bundles/org.openhab.binding.ipcamera/README.md @@ -213,6 +213,7 @@ The channels are kept consistent as much as possible from brand to brand to make | `activateAlarmOutput2` | Switch | Toggles a cameras relay output 2. | | `audioAlarm` | Switch (read only) | When the camera detects noise above a threshold this switch will move to ON. | | `autoLED` | Switch | When ON this sets a cameras IR LED to automatically turn on or off. | +| `carAlarm` | Switch | When a car is detected the switch will turn ON. | | `cellMotionAlarm` | Switch (read only) | ONVIF cameras only will reflect the status of the ONVIF event of the same name. | | `doorBell` | Switch (read only) | Doorbird only, will reflect the status of the doorbell button. | | `enableAudioAlarm` | Switch | Allows the audio alarm to be turned ON or OFF. | @@ -234,6 +235,7 @@ The channels are kept consistent as much as possible from brand to brand to make | `gifHistoryLength` | Number | How many filenames are in the `gifHistory`. | | `gotoPreset` | String | ONVIF cameras that can move only. Will cause the camera to move to a preset location. | | `hlsUrl` | String | The URL for the ipcamera.m3u8 file. | +| `humanAlarm` | Switch | When a camera detects a human this switch will turn ON. | | `imageUrl` | String | The URL for the ipcamera.jpg file. | | `itemLeft` | Switch (read only) | Will turn ON if an API camera detects an item has been left behind. | | `itemTaken` | Switch (read only) | Will turn ON if an API camera detects an item has been stolen. | @@ -379,6 +381,9 @@ See this forum thread for examples of how to use snapshots and streams in a site ## Video Streams +To get video streams working, this forum thread has working widget examples that you can use. + + To get some of the video formats working, you need to install FFmpeg. Visit their site here to learn how diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DahuaHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DahuaHandler.java index 19daffebc565b..65234b506364a 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DahuaHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DahuaHandler.java @@ -14,7 +14,7 @@ import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*; -import java.util.ArrayList; +import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -48,6 +48,150 @@ public DahuaHandler(IpCameraHandler handler, int nvrChannel) { this.nvrChannel = nvrChannel; } + private void processEvent(String content) { + int startIndex = content.indexOf("Code=", 12) + 5;// skip --myboundary + int endIndex = content.indexOf(";", startIndex + 1); + if (startIndex == -1 || endIndex == -1) { + ipCameraHandler.logger.debug("Code= not found in Dahua event. Content was:{}", content); + return; + } + String code = content.substring(startIndex, endIndex); + startIndex = endIndex + 8;// skip ;action= + endIndex = content.indexOf(";", startIndex); + if (startIndex == -1 || endIndex == -1) { + ipCameraHandler.logger.debug(";action= not found in Dahua event. Content was:{}", content); + return; + } + String action = content.substring(startIndex, endIndex); + switch (code) { + case "VideoMotion": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM); + } + break; + case "TakenAwayDetection": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_ITEM_TAKEN); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_ITEM_TAKEN); + } + break; + case "LeftDetection": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_ITEM_LEFT); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_ITEM_LEFT); + } + break; + case "SmartMotionVehicle": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_CAR_ALARM); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_CAR_ALARM); + } + break; + case "SmartMotionHuman": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_HUMAN_ALARM); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_HUMAN_ALARM); + } + break; + case "CrossLineDetection": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM); + } + break; + case "AudioMutation": + if (action.equals("Start")) { + ipCameraHandler.audioDetected(); + } else if (action.equals("Stop")) { + ipCameraHandler.noAudioDetected(); + } + break; + case "FaceDetection": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_FACE_DETECTED); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_FACE_DETECTED); + } + break; + case "ParkingDetection": + if (action.equals("Start")) { + ipCameraHandler.setChannelState(CHANNEL_PARKING_ALARM, OnOffType.ON); + } else if (action.equals("Stop")) { + ipCameraHandler.setChannelState(CHANNEL_PARKING_ALARM, OnOffType.OFF); + } + break; + case "CrossRegionDetection": + if (action.equals("Start")) { + ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM); + } else if (action.equals("Stop")) { + ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM); + } + break; + case "VideoLoss": + case "VideoBlind": + if (action.equals("Start")) { + ipCameraHandler.setChannelState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON); + } else if (action.equals("Stop")) { + ipCameraHandler.setChannelState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF); + } + break; + case "VideoAbnormalDetection": + if (action.equals("Start")) { + ipCameraHandler.setChannelState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON); + } else if (action.equals("Stop")) { + ipCameraHandler.setChannelState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF); + } + break; + case "VideoUnFocus": + if (action.equals("Start")) { + ipCameraHandler.setChannelState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON); + } else if (action.equals("Stop")) { + ipCameraHandler.setChannelState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF); + } + break; + case "AlarmLocal": + if (action.equals("Start")) { + if (content.contains("index=0")) { + ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON); + } else { + ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.ON); + } + } else if (action.equals("Stop")) { + if (content.contains("index=0")) { + ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF); + } else { + ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.OFF); + } + } + break; + case "LensMaskOpen": + ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.ON); + break; + case "LensMaskClose": + ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.OFF); + break; + case "TimeChange": + case "NTPAdjustTime": + case "StorageChange": + case "Reboot": + case "NewFile": + case "VideoMotionInfo": + case "RtspSessionDisconnect": + case "LeFunctionStatusSync": + case "RecordDelete": + break; + default: + ipCameraHandler.logger.debug("Unrecognised Dahua event, Code={}, action={}", code, action); + } + } + // This handles the incoming http replies back from the camera. @Override public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception { @@ -56,6 +200,10 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms } try { String content = msg.toString(); + if (content.startsWith("--myboundary")) { + processEvent(content); + return; + } ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content); // determine if the motion detection is turned on or off. if (content.contains("table.MotionDetect[0].Enable=true")) { @@ -63,77 +211,20 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms } else if (content.contains("table.MotionDetect[" + nvrChannel + "].Enable=false")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF); } - // Handle motion alarm - if (content.contains("Code=VideoMotion;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM); - } else if (content.contains("Code=VideoMotion;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM); - } - // Handle item taken alarm - if (content.contains("Code=TakenAwayDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_ITEM_TAKEN); - } else if (content.contains("Code=TakenAwayDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_ITEM_TAKEN); - } - // Handle item left alarm - if (content.contains("Code=LeftDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_ITEM_LEFT); - } else if (content.contains("Code=LeftDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_ITEM_LEFT); - } - // Handle CrossLineDetection alarm - if (content.contains("Code=CrossLineDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM); - } else if (content.contains("Code=CrossLineDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM); - } + // determine if the audio alarm is turned on or off. if (content.contains("table.AudioDetect[0].MutationDetect=true")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON); } else if (content.contains("table.AudioDetect[0].MutationDetect=false")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF); } - // Handle AudioMutation alarm - if (content.contains("Code=AudioMutation;action=Start;index=0")) { - ipCameraHandler.audioDetected(); - } else if (content.contains("Code=AudioMutation;action=Stop;index=0")) { - ipCameraHandler.noAudioDetected(); - } + // Handle AudioMutationThreshold alarm if (content.contains("table.AudioDetect[0].MutationThreold=")) { String value = ipCameraHandler.returnValueFromString(content, "table.AudioDetect[0].MutationThreold="); ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value)); } - // Handle FaceDetection alarm - if (content.contains("Code=FaceDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_FACE_DETECTED); - } else if (content.contains("Code=FaceDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_FACE_DETECTED); - } - // Handle ParkingDetection alarm - if (content.contains("Code=ParkingDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_PARKING_ALARM); - } else if (content.contains("Code=ParkingDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_PARKING_ALARM); - } - // Handle CrossRegionDetection alarm - if (content.contains("Code=CrossRegionDetection;action=Start;index=0")) { - ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM); - } else if (content.contains("Code=CrossRegionDetection;action=Stop;index=0")) { - ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM); - } - // Handle External Input alarm - if (content.contains("Code=AlarmLocal;action=Start;index=0")) { - ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON); - } else if (content.contains("Code=AlarmLocal;action=Stop;index=0")) { - ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF); - } - // Handle External Input alarm2 - if (content.contains("Code=AlarmLocal;action=Start;index=1")) { - ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.ON); - } else if (content.contains("Code=AlarmLocal;action=Stop;index=1")) { - ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.OFF); - } + // CrossLineDetection alarm on/off if (content.contains("table.VideoAnalyseRule[0][1].Enable=true")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON); @@ -141,10 +232,9 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF); } // Privacy Mode on/off - if (content.contains("Code=LensMaskOpen;") || content.contains("table.LeLensMask[0].Enable=true")) { + if (content.contains("table.LeLensMask[0].Enable=true")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.ON); - } else if (content.contains("Code=LensMaskClose;") - || content.contains("table.LeLensMask[0].Enable=false")) { + } else if (content.contains("table.LeLensMask[0].Enable=false")) { ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.OFF); } } finally { @@ -152,13 +242,10 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms } } - // This handles the commands that come from the Openhab event bus. + // This handles the commands that come from the openHAB event bus. public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { switch (channelUID.getId()) { - case CHANNEL_THRESHOLD_AUDIO_ALARM: - // ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]"); - return; case CHANNEL_ENABLE_AUDIO_ALARM: ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]"); return; @@ -172,8 +259,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=LeLensMask[0]"); return; } - return; // Return as we have handled the refresh command above and don't need to - // continue further. + return; } // end of "REFRESH" switch (channelUID.getId()) { case CHANNEL_TEXT_OVERLAY: @@ -272,8 +358,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { // If a camera does not need to poll a request as often as snapshots, it can be // added here. Binding steps through the list. - public ArrayList getLowPriorityRequests() { - ArrayList lowPriorityRequests = new ArrayList(1); - return lowPriorityRequests; + public List getLowPriorityRequests() { + return List.of(); } } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DoorBirdHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DoorBirdHandler.java index c23295b76f92d..f6ab9bc8d19cb 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DoorBirdHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/DoorBirdHandler.java @@ -53,16 +53,14 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms try { String content = msg.toString(); ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content); - if (content.contains("doorbell:H")) { - ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.ON); - } if (content.contains("doorbell:L")) { ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.OFF); + } else if (content.contains("doorbell:H")) { + ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.ON); } if (content.contains("motionsensor:L")) { ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM); - } - if (content.contains("motionsensor:H")) { + } else if (content.contains("motionsensor:H")) { ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM); } } finally { diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/FoscamHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/FoscamHandler.java index d85680b7ff5ba..ff314392e6804 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/FoscamHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/FoscamHandler.java @@ -14,7 +14,7 @@ import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*; -import java.util.ArrayList; +import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -212,9 +212,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { // If a camera does not need to poll a request as often as snapshots, it can be // added here. Binding steps through the list. - public ArrayList getLowPriorityRequests() { - ArrayList lowPriorityRequests = new ArrayList(1); - lowPriorityRequests.add("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + username + "&pwd=" + password); - return lowPriorityRequests; + public List getLowPriorityRequests() { + return List.of(); } } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/HikvisionHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/HikvisionHandler.java index d99920f865c18..a2e86da0677c2 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/HikvisionHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/HikvisionHandler.java @@ -60,6 +60,56 @@ public HikvisionHandler(ThingHandler handler, int nvrChannel) { this.nvrChannel = nvrChannel; } + private void processEvent(String content) { + // some cameras use or and NVRs use channel 0 to say all channels + if (content.contains("hannelID>" + nvrChannel) || content.contains("0")) { + final int debounce = 3; + String eventType = Helper.fetchXML(content, "", ""); + switch (eventType) { + case "videoloss": + if (content.contains("inactive")) { + if (vmdCount > 1) { + vmdCount = 1; + } + countDown(); + countDown(); + } + break; + case "PIR": + ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM); + pirCount = debounce; + break; + case "attendedBaggage": + ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON); + takenCount = debounce; + break; + case "unattendedBaggage": + ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON); + leftCount = debounce; + break; + case "facedetection": + ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON); + faceCount = debounce; + break; + case "VMD": + ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM); + vmdCount = debounce; + break; + case "fielddetection": + ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM); + fieldCount = debounce; + break; + case "linedetection": + ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM); + lineCount = debounce; + break; + default: + logger.debug("Unrecognised Hikvision eventType={}", eventType); + } + } + countDown(); + } + // This handles the incoming http replies back from the camera. @Override public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception { @@ -67,59 +117,10 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms return; } try { - int debounce = 3; String content = msg.toString(); logger.trace("HTTP Result back from camera is \t:{}:", content); - if (content.contains("--boundary")) {// Alarm checking goes in here// - if (content.contains("" + nvrChannel + " - if (content.contains("linedetection")) { - ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM); - lineCount = debounce; - } - if (content.contains("fielddetection")) { - ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM); - fieldCount = debounce; - } - if (content.contains("VMD")) { - ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM); - vmdCount = debounce; - } - if (content.contains("facedetection")) { - ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON); - faceCount = debounce; - } - if (content.contains("unattendedBaggage")) { - ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON); - leftCount = debounce; - } - if (content.contains("attendedBaggage")) { - ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON); - takenCount = debounce; - } - if (content.contains("PIR")) { - ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM); - pirCount = debounce; - } - if (content.contains("videoloss\r\ninactive")) { - if (vmdCount > 1) { - vmdCount = 1; - } - countDown(); - countDown(); - } - } else if (content.contains("0")) {// NVR uses channel 0 to say all - // channels - if (content.contains("videoloss\r\ninactive")) { - if (vmdCount > 1) { - vmdCount = 1; - } - countDown(); - countDown(); - } - } - countDown(); - } + if (content.startsWith("--boundary")) {// Alarm checking goes in here// + processEvent(content); } else { String replyElement = Helper.fetchXML(content, "", "<"); switch (replyElement) { @@ -196,23 +197,7 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms } break; default: - if (content.contains("" + nvrChannel + "0")) {// some camera use c or - // - if (content.contains( - "videoloss\r\ninactive")) { - if (vmdCount > 1) { - vmdCount = 1; - } - countDown(); - countDown(); - } - countDown(); - } - } else { - logger.debug("Unhandled reply-{}.", content); - } + logger.debug("Unhandled reply-{}.", content); break; } } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java index e01c7118a356c..475f91a53d761 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java @@ -129,4 +129,6 @@ public static enum FFmpegFormat { public static final String CHANNEL_GOTO_PRESET = "gotoPreset"; public static final String CHANNEL_START_STREAM = "startStream"; public static final String CHANNEL_ENABLE_PRIVACY_MODE = "enablePrivacyMode"; + public static final String CHANNEL_CAR_ALARM = "carAlarm"; + public static final String CHANNEL_HUMAN_ALARM = "humanAlarm"; } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/MyNettyAuthHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/MyNettyAuthHandler.java index 58386308ed97f..e59b4055f5b6b 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/MyNettyAuthHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/MyNettyAuthHandler.java @@ -95,7 +95,6 @@ public void processAuth(String authenticate, String httpMethod, String requestUR nonce = Helper.searchString(authenticate, "nonce=\""); opaque = Helper.searchString(authenticate, "opaque=\""); qop = Helper.searchString(authenticate, "qop=\""); - if (!qop.isEmpty() && !realm.isEmpty()) { ipCameraHandler.useDigestAuth = true; } else { @@ -128,8 +127,10 @@ public void processAuth(String authenticate, String httpMethod, String requestUR String digestString = "username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + requestURI + "\", cnonce=\"" + cnonce + "\", nc=" + nc + ", qop=\"" + qop + "\", response=\"" - + response + "\", opaque=\"" + opaque + "\""; - + + response + "\""; + if (!opaque.isEmpty()) { + digestString += ", opaque=\"" + opaque + "\""; + } if (reSend) { ipCameraHandler.sendHttpRequest(httpMethod, requestURI, digestString); return; diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraGroupHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraGroupHandler.java index 6d2ffe0766bc3..c089e77681b95 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraGroupHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraGroupHandler.java @@ -353,7 +353,7 @@ public void initialize() { startStreamServer(true); } updateStatus(ThingStatus.ONLINE); - pollCameraGroupJob = pollCameraGroup.scheduleAtFixedRate(this::pollCameraGroup, 10000, + pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000, groupConfig.getPollTime(), TimeUnit.MILLISECONDS); } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java index ea43387e15c43..ddc7a5d5929b0 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java @@ -206,6 +206,7 @@ private class CommonCameraHandler extends ChannelDuplexHandler { private byte[] incomingJpeg = new byte[0]; private String incomingMessage = ""; private String contentType = "empty"; + private String boundary = ""; private Object reply = new Object(); private String requestUrl = ""; private boolean closeConnection = true; @@ -255,6 +256,8 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms firstStreamedMsg = msg; streamToGroup(firstStreamedMsg, mjpegChannelGroup, true); } + } else { + boundary = Helper.searchString(contentType, "boundary="); } } else if (contentType.contains("image/jp")) { if (bytesToRecieve == 0) { @@ -305,11 +308,32 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms } // Alarm Streams never have a LastHttpContent as they always stay open// else if (contentType.contains("multipart")) { - if (bytesAlreadyRecieved != 0) { - reply = incomingMessage; - incomingMessage = ""; - bytesToRecieve = 0; - bytesAlreadyRecieved = 0; + int beginIndex, endIndex; + if (bytesToRecieve == 0) { + beginIndex = incomingMessage.indexOf("Content-Length:"); + if (beginIndex != -1) { + endIndex = incomingMessage.indexOf("\r\n", beginIndex); + if (endIndex != -1) { + bytesToRecieve = Integer.parseInt( + incomingMessage.substring(beginIndex + 15, endIndex).strip()); + } + } + } + // --boundary and headers are not included in the Content-Length value + if (bytesAlreadyRecieved > bytesToRecieve) { + // Check if message has a second --boundary + endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve); + if (endIndex == -1) { + reply = incomingMessage; + incomingMessage = ""; + bytesToRecieve = 0; + bytesAlreadyRecieved = 0; + } else { + reply = incomingMessage.substring(0, endIndex); + incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length()); + bytesToRecieve = 0;// Triggers search next time for Content-Length: + bytesAlreadyRecieved = incomingMessage.length() - endIndex; + } super.channelRead(ctx, reply); } } @@ -591,8 +615,8 @@ public void operationComplete(@Nullable ChannelFuture future) { } logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port, httpRequestURL); - channelTrackingMap.put(httpRequestURL, new ChannelTracking(ch, httpRequestURL)); + openChannel(ch, httpRequestURL); CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER); commonHandler.setURL(httpRequestURLFull); MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER); @@ -782,6 +806,15 @@ public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) { } } + void openChannel(Channel channel, String httpRequestURL) { + ChannelTracking tracker = channelTrackingMap.get(httpRequestURL); + if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply + tracker.setChannel(channel); + return; + } + channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL)); + } + void closeChannel(String url) { ChannelTracking channelTracking = channelTrackingMap.get(url); if (channelTracking != null) { @@ -1402,7 +1435,7 @@ public void setChannelState(String channelToUpdate, State valueOf) { updateState(channelToUpdate, valueOf); } - void bringCameraOnline() { + private void bringCameraOnline() { isOnline = true; updateStatus(ThingStatus.ONLINE); groupTracker.listOfOnlineCameraHandlers.add(this); @@ -1414,7 +1447,7 @@ void bringCameraOnline() { if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) { snapshotPolling = true; - snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(), + snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(), TimeUnit.MILLISECONDS); } @@ -1537,11 +1570,11 @@ public void startSnapshotPolling() { } if (streamingSnapshotMjpeg || streamingAutoFps) { snapshotPolling = true; - snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(), + snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(), TimeUnit.MILLISECONDS); } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms snapshotPolling = true; - snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(), + snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(), TimeUnit.MILLISECONDS); } } @@ -1610,6 +1643,10 @@ void pollCameraRunnable() { sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor"); } break; + case FOSCAM_THING: + sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd=" + + cameraConfig.getPassword()); + break; } Ffmpeg localHLS = ffmpegHLS; if (localHLS != null) { @@ -1698,8 +1735,8 @@ public void initialize() { onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING)); } - // for poll times above 9 seconds don't display a warning about the Image channel. - if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) { + // for poll times 9 seconds and above don't display a warning about the Image channel. + if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) { logger.warn( "The Image channel is set to update more often than 8 seconds. This is not recommended. The Image channel is best used only for higher poll times. See the readme file on how to display the cameras picture for best results or use a higher poll time."); } @@ -1747,6 +1784,7 @@ public void dispose() { Ffmpeg localFfmpeg = ffmpegHLS; if (localFfmpeg != null) { localFfmpeg.stopConverting(); + localFfmpeg = null; } localFfmpeg = ffmpegRecord; if (localFfmpeg != null) { diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifDiscovery.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifDiscovery.java index 10c744c817b51..1705733152adb 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifDiscovery.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifDiscovery.java @@ -124,9 +124,9 @@ void searchReply(String url, String xml) { void processCameraReplys() { for (DatagramPacket packet : listOfReplys) { - logger.trace("Device replied to discovery with:{}", packet.toString()); String xml = packet.content().toString(CharsetUtil.UTF_8); - String xAddr = Helper.fetchXML(xml, "", ""); + logger.trace("Device replied to discovery with:{}", xml); + String xAddr = Helper.fetchXML(xml, "", "d:XAddrs>");// Foscam and all other brands if (!xAddr.equals("")) { searchReply(xAddr, xml); } else if (xml.contains("onvif")) { diff --git a/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml index 1e354d4a30e9f..12624ebc1f1f1 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml @@ -936,6 +936,11 @@ + + + + + @@ -2542,6 +2547,22 @@
+ + Switch + + A person has triggered the Human Detection. + Alarm + + + + + Switch + + A car has triggered the Vehicle Detection. + Alarm + + + Switch diff --git a/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/BlasterHandler.java b/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/BlasterHandler.java index a209af76cfc0f..d2ea1079a2f2b 100644 --- a/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/BlasterHandler.java +++ b/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/BlasterHandler.java @@ -14,7 +14,7 @@ import static org.openhab.binding.irtrans.internal.IRtransBindingConstants.CHANNEL_IO; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.irtrans.internal.IRtransBindingConstants.Led; import org.openhab.binding.irtrans.internal.IrCommand; import org.openhab.core.library.types.StringType; diff --git a/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/EthernetBridgeHandler.java b/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/EthernetBridgeHandler.java index 4a70874e32691..4e0aae384e9c4 100644 --- a/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/EthernetBridgeHandler.java +++ b/bundles/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/internal/handler/EthernetBridgeHandler.java @@ -35,7 +35,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.irtrans.internal.IRtransBindingConstants; import org.openhab.binding.irtrans.internal.IRtransBindingConstants.Led; diff --git a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/Server.java b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/Server.java index f0d6fcfd69eb0..8b2ac26cce94c 100644 --- a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/Server.java +++ b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/Server.java @@ -85,6 +85,7 @@ public boolean getConnected() { * Starts the server * */ + @Override public void run() { this.startRetries = 0; while (!this.isInterrupted()) { @@ -132,7 +133,7 @@ public void addDataPoint(int id, String knxType, String description) { IDataPoint dp = DataPointFactory.createDataPoint(id, knxType, description); if (dp != null) { - this.dataPoints.put(new Integer(id), dp); + this.dataPoints.put(Integer.valueOf(id), dp); } } diff --git a/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LaCrosseTemperatureSensorHandler.java b/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LaCrosseTemperatureSensorHandler.java index d7a39f554cb53..9befda8961dbb 100644 --- a/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LaCrosseTemperatureSensorHandler.java +++ b/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LaCrosseTemperatureSensorHandler.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.jeelink.internal.JeeLinkSensorHandler; import org.openhab.binding.jeelink.internal.ReadingPublisher; import org.openhab.binding.jeelink.internal.RollingAveragePublisher; diff --git a/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LgwSensorHandler.java b/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LgwSensorHandler.java index 7074f1d57460a..fcba0f17a3c7a 100644 --- a/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LgwSensorHandler.java +++ b/bundles/org.openhab.binding.jeelink/src/main/java/org/openhab/binding/jeelink/internal/lacrosse/LgwSensorHandler.java @@ -18,7 +18,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.jeelink.internal.JeeLinkSensorHandler; import org.openhab.binding.jeelink.internal.ReadingPublisher; diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java index 1c6cd46a6611a..0dbe8727f53e4 100644 --- a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java +++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.kaleidescape.internal.communication; -import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang3.StringEscapeUtils; import org.eclipse.jdt.annotation.NonNullByDefault; /** @@ -48,8 +48,8 @@ public static String formatString(String input) { // I.e. characters with accent, umlaut, etc., they need to be restored to the correct character // example: Noel (with umlaut 'o') comes in as N\d246el input = input.replaceAll("(?i)\\\\d([0-9]{3})", "\\&#$1;"); // first convert to html escaped codes - // then convert with unescapeHtml, not sure how to do this without the Apache libraries :( - return StringEscapeUtils.unescapeHtml(input); + // then convert with unescapeHtml4, not sure how to do this without the Apache libraries :( + return StringEscapeUtils.unescapeHtml4(input); } } return input; diff --git a/bundles/org.openhab.binding.keba/README.md b/bundles/org.openhab.binding.keba/README.md index 2c1c5bae994b5..e6037cfebc3f0 100644 --- a/bundles/org.openhab.binding.keba/README.md +++ b/bundles/org.openhab.binding.keba/README.md @@ -4,46 +4,48 @@ This binding integrates the [Keba KeContact EV Charging Stations](https://www.ke ## Supported Things -The Keba KeContact P20 and P30 stations are supported by this binding, the thing type id is `kecontact`. - +The Keba KeContact P20 and P30 stations which are providing the UDP interface (P20 LSA+ socket, P30 c-series and x-series) are supported by this binding, the thing type id is `kecontact`. ## Thing Configuration -The Keba KeContact P20/30 requires the ip address as the configuration parameter `ipAddress`. Optionally, a refresh interval (in seconds) can be defined as parameter `refreshInterval` that defines the polling of values from the charging station. - +The Keba KeContact P20/30 requires the IP address as the configuration parameter `ipAddress`. +Optionally, a refresh interval (in seconds) can be defined as parameter `refreshInterval` that defines the polling of values from the charging station. ## Channels All devices support the following channels: -| Channel ID | Item Type | Read-only | Description | -|--------------------|-----------|-----------|------------------------------------------------------------------------| -| state | Number | yes | current operational state of the wallbox | -| enabled | Switch | no | activation state of the wallbox | -| maxpresetcurrent | Number | no | maximum current the charging station should deliver to the EV | -| power | Number | yes | active power delivered by the charging station | -| wallbox | Switch | yes | plug state of wallbox | -| vehicle | Switch | yes | plug state of vehicle | -| locked | Switch | yes | lock state of plug at vehicle | -| I1/2/3 | Number | yes | current for the given phase | -| U1/2/3 | Number | yes | voltage for the given phase | -| output | Switch | no | state of the X1 relais | -| input | Switch | yes | state of the X2 contact | -| display | String | yes | display text on wallbox | -| error1 | String | yes | error code state 1, if in error (see the KeContact FAQ) | -| error2 | String | yes | error code state 2, if in error (see the KeContact FAQ) | -| maxsystemcurrent | Number | yes | maximum current the wallbox can deliver | -| failsafecurrent | Number | yes | maximum current the wallbox can deliver, if network is lost | -| uptime | DateTime | yes | system uptime since the last reset of the wallbox | -| sessionconsumption | Number | yes | energy delivered in current session | -| totalconsumption | Number | yes | total energy delivered since the last reset of the wallbox | -| authreq | Switch | yes | authentication required | -| authon | Switch | yes | authentication enabled | -| sessionrfidtag | String | yes | RFID tag used for the last charging session | -| sessionrfidclass | String | yes | RFID tag class used for the last charging session | -| sessionid | Number | yes | session ID of the last charging session | -| setenergylimit | Number | no | set an energy limit for an already running or the next charging session| -| authenticate | String | no | authenticate and start a session using RFID tag+RFID class | +| Channel ID | Item Type | Read-only | Description | +|---------------------------|---------------------------|-----------|---------------------------------------------------------------------------| +| state | Number | yes | current operational state of the wallbox | +| enabled | Switch | no | activation state of the wallbox | +| maxpresetcurrent | Number:ElectricCurrent | no | maximum current the charging station should deliver to the EV in A | +| maxpresetcurrentrange | Number:Dimensionless | no | maximum current the charging station should deliver to the EV in % | +| power | Number:Power | yes | active power delivered by the charging station | +| wallbox | Switch | yes | plug state of wallbox | +| vehicle | Switch | yes | plug state of vehicle | +| locked | Switch | yes | lock state of plug at vehicle | +| I1/2/3 | Number:ElectricCurrent | yes | current for the given phase | +| U1/2/3 | Number:ElectricPotential | yes | voltage for the given phase | +| output | Switch | no | state of the X1 relais | +| input | Switch | yes | state of the X2 contact | +| display | String | no | display text on wallbox | +| error1 | String | yes | error code state 1, if in error (see the KeContact FAQ) | +| error2 | String | yes | error code state 2, if in error (see the KeContact FAQ) | +| maxsystemcurrent | Number:ElectricCurrent | yes | maximum current the wallbox can deliver | +| failsafecurrent | Number:ElectricCurrent | yes | maximum current the wallbox can deliver, if network is lost | +| uptime | Number:Time | yes | system uptime since the last reset of the wallbox | +| sessionconsumption | Number:Energy | yes | energy delivered in current session | +| totalconsumption | Number:Energy | yes | total energy delivered since the last reset of the wallbox | +| authreq | Switch | yes | authentication required | +| authon | Switch | yes | authentication enabled | +| sessionrfidtag | String | yes | RFID tag used for the last charging session | +| sessionrfidclass | String | yes | RFID tag class used for the last charging session | +| sessionid | Number | yes | session ID of the last charging session | +| setenergylimit | Number:Energy | no | set an energy limit for an already running or the next charging session | +| authenticate | String | no | authenticate and start a session using RFID tag+RFID class | +| maxpilotcurrent | Number:ElectricCurrent | yes | current offered to the vehicle via control pilot signalization | +| maxpilotcurrentdutycyle | Number:Dimensionless | yes | duty cycle of the control pilot signal | ## Example @@ -57,28 +59,28 @@ Thing keba:kecontact:1 [ipAddress="192.168.0.64", refreshInterval=30] demo.items: ``` -Dimmer KebaCurrentRange {channel="keba:kecontact:1:maxpresetcurrentrange"} -Number KebaCurrent {channel="keba:kecontact:1:maxpresetcurrent"} -Number KebaSystemCurrent {channel="keba:kecontact:1:maxsystemcurrent"} -Number KebaFailSafeCurrent {channel="keba:kecontact:1:failsafecurrent"} -String KebaState {channel="keba:kecontact:1:state"} -Switch KebaSwitch {channel="keba:kecontact:1:enabled"} -Switch KebaWallboxPlugged {channel="keba:kecontact:1:wallbox"} -Switch KebaVehiclePlugged {channel="keba:kecontact:1:vehicle"} -Switch KebaPlugLocked {channel="keba:kecontact:1:locked"} -DateTime KebaUptime "Uptime [%1$tY Y, %1$tm M, %1$td D, %1$tT]" {channel="keba:kecontact:1:uptime"} -Number KebaI1 {channel="keba:kecontact:1:I1"} -Number KebaI2 {channel="keba:kecontact:1:I2"} -Number KebaI3 {channel="keba:kecontact:1:I3"} -Number KebaU1 {channel="keba:kecontact:1:U1"} -Number KebaU2 {channel="keba:kecontact:1:U2"} -Number KebaU3 {channel="keba:kecontact:1:U3"} -Number KebaPower {channel="keba:kecontact:1:power"} -Number KebaSessionEnergy {channel="keba:kecontact:1:sessionconsumption"} -Number KebaTotalEnergy {channel="keba:kecontact:1:totalconsumption"} -Switch KebaInputSwitch {channel="keba:kecontact:1:input"} -Switch KebaOutputSwitch {channel="keba:kecontact:1:output"} -Number KebaSetEnergyLimit {channel="keba:kecontact:1:setenergylimit"} +Number:Dimensionless KebaCurrentRange "Maximum supply current [%.1f %%]" {channel="keba:kecontact:1:maxpresetcurrentrange"} +Number:ElectricCurrent KebaCurrent "Maximum supply current [%.3f A]" {channel="keba:kecontact:1:maxpresetcurrent"} +Number:ElectricCurrent KebaSystemCurrent "Maximum system supply current [%.3f A]" {channel="keba:kecontact:1:maxsystemcurrent"} +Number:ElectricCurrent KebaFailSafeCurrent "Failsafe supply current [%.3f A]" {channel="keba:kecontact:1:failsafecurrent"} +String KebaState "Operating State [%s]" {channel="keba:kecontact:1:state"} +Switch KebaSwitch "Enabled" {channel="keba:kecontact:1:enabled"} +Switch KebaWallboxPlugged "Plugged into wallbox" {channel="keba:kecontact:1:wallbox"} +Switch KebaVehiclePlugged "Plugged into vehicle" {channel="keba:kecontact:1:vehicle"} +Switch KebaPlugLocked "Plug locked" {channel="keba:kecontact:1:locked"} +DateTime KebaUptime "Uptime [%s s]" {channel="keba:kecontact:1:uptime"} +Number:ElectricCurrent KebaI1 {channel="keba:kecontact:1:I1"} +Number:ElectricCurrent KebaI2 {channel="keba:kecontact:1:I2"} +Number:ElectricCurrent KebaI3 {channel="keba:kecontact:1:I3"} +Number:ElectricPotential KebaU1 {channel="keba:kecontact:1:U1"} +Number:ElectricPotential KebaU2 {channel="keba:kecontact:1:U2"} +Number:ElectricPotential KebaU3 {channel="keba:kecontact:1:U3"} +Number:Power KebaPower "Energy during current session [%.1f Wh]" {channel="keba:kecontact:1:power"} +Number:Energy KebaSessionEnergy {channel="keba:kecontact:1:sessionconsumption"} +Number:Energy KebaTotalEnergy "Energy during all sessions [%.1f Wh]" {channel="keba:kecontact:1:totalconsumption"} +Switch KebaInputSwitch {channel="keba:kecontact:1:input"} +Switch KebaOutputSwitch {channel="keba:kecontact:1:output"} +Number:Energy KebaSetEnergyLimit "Set charge energy limit [%.1f Wh]" {channel="keba:kecontact:1:setenergylimit"} ``` demo.sitemap: @@ -86,20 +88,63 @@ demo.sitemap: ``` sitemap demo label="Main Menu" { - Text label="Charging Station" { - Text item=KebaState label="Operating State [%s]" - Text item=KebaUptime - Switch item=KebaSwitch label="Enabled" mappings=[ON=ON, OFF=OFF ] - Switch item=KebaWallboxPlugged label="Plugged into wallbox" mappings=[ON=ON, OFF=OFF ] - Switch item=KebaVehiclePlugged label="Plugged into vehicle" mappings=[ON=ON, OFF=OFF ] - Switch item=KebaPlugLocked label="Plug locked" mappings=[ON=ON, OFF=OFF ] - Slider item=KebaCurrentRange switchSupport label="Maximum supply current [%.1f %%]" - Text item=KebaCurrent label="Maximum supply current [%.0f mA]" - Text item=KebaSystemCurrent label="Maximum system supply current [%.0f mA]" - Text item=KebaFailSafeCurrent label="Failsafe supply current [%.0f mA]" - Text item=KebaSessionEnergy label="Energy during current session [%.0f Wh]" - Text item=KebaTotalEnergy label="Energy during all sessions [%.0f Wh]" - Switch item=KebaSetEnergyLimit label="Set charge energy limit" mappings=[0="off", 20000="20kWh"] - } + Text label="Charging Station" { + Text item=KebaState + Text item=KebaUptime + Switch item=KebaSwitch + Switch item=KebaWallboxPlugged + Switch item=KebaVehiclePlugged + Switch item=KebaPlugLocked + Slider item=KebaCurrentRange + Text item=KebaCurrent + Text item=KebaSystemCurrent + Text item=KebaFailSafeCurrent + Text item=KebaSessionEnergy + Text item=KebaTotalEnergy + Switch item=KebaSetEnergyLimit + } } ``` + +## Troubleshooting + +### Enable Verbose Logging + +Enable `DEBUG` or `TRACE` (even more verbose) logging for the logger named: + + org.openhab.binding.keba + +If everything is working fine, you see the cyclic reception of `report 1`, `2` & `3` from the station. The frequency is according to the `refreshInterval` configuration. + +### UDP Ports used + + Send port = UDP 7090 + +The Keba station is the server + + Receive port = UDP 7090 + +This binding is providing the server + +UDP port 7090 needs to be available/free on the openHAB server. + + +In order to enable the UDP port 7090 on the Keba station with full functionality, `DIP switch 1.3` must be `ON`. +With `DIP switch 1.3 OFF` only ident-data can be read (`i` and `report 1`) but not the other reports as well as the commands needed for the write access. +After setting the DIP switch, you need to `power OFF` and `ON` the station. SW-reset via WebGUI seems not to be sufficient in order to apply the new configuration. + + +The right configuration can be validated as follows: + +- WebGUI DSW Settings: + - `DIP 1.3 | ON | UDP interface (SmartHome)` +- UDP response of `report 1`: + - `DIP-Sw1` `0x20` Bit is set (enable at least `DEBUG` log-level for the binding) + +### Supported stations + +- KeContact P20 charging station with network connection (LSA+ socket) + - Product code: `KC-P20-xxxxxx2x-xxx` or `KC-P20-xxxxxx3x-xxx` + - Firmware version: 2.5 or higher +- KeContact P30 charging station (c- or x-series) or BMW wallbox + - Firmware version 3.05 or higher diff --git a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/KebaBindingConstants.java b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/KebaBindingConstants.java index fa4b0d227ccfc..8280827d8a974 100644 --- a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/KebaBindingConstants.java +++ b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/KebaBindingConstants.java @@ -15,7 +15,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -44,7 +44,7 @@ public class KebaBindingConstants { public static final String CHANNEL_PLUG_LOCKED = "locked"; public static final String CHANNEL_ENABLED = "enabled"; public static final String CHANNEL_PILOT_CURRENT = "maxpilotcurrent"; - public static final String CHANNEL_PILOT_PWM = "pwmpilotcurrent"; + public static final String CHANNEL_PILOT_PWM = "maxpilotcurrentdutycyle"; public static final String CHANNEL_MAX_SYSTEM_CURRENT = "maxsystemcurrent"; public static final String CHANNEL_MAX_PRESET_CURRENT_RANGE = "maxpresetcurrentrange"; public static final String CHANNEL_MAX_PRESET_CURRENT = "maxpresetcurrent"; @@ -82,7 +82,7 @@ public enum KebaSeries { E('0'), B('1'), C('2', '3'), - X('A', 'B', 'C', 'D'); + X('A', 'B', 'C', 'D', 'E', 'G', 'H'); private final List things = new ArrayList<>(); @@ -104,7 +104,7 @@ public static KebaSeries getSeries(char text) throws IllegalArgumentException { } } - throw new IllegalArgumentException("Not a valid series"); + throw new IllegalArgumentException("Not a valid series: '" + text + "'"); } } } diff --git a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactHandler.java b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactHandler.java index 478c49bf9c24b..af2b999a91516 100644 --- a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactHandler.java +++ b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactHandler.java @@ -16,27 +16,33 @@ import java.io.IOException; import java.math.BigDecimal; -import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; import java.nio.ByteBuffer; -import java.text.SimpleDateFormat; -import java.util.Calendar; import java.util.Map; import java.util.Map.Entry; -import java.util.TimeZone; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; +import javax.measure.quantity.Dimensionless; +import javax.measure.quantity.ElectricCurrent; +import javax.measure.quantity.ElectricPotential; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; +import javax.measure.quantity.Time; + +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.keba.internal.KebaBindingConstants.KebaSeries; import org.openhab.binding.keba.internal.KebaBindingConstants.KebaType; import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.config.core.Configuration; -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.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -63,18 +69,19 @@ public class KeContactHandler extends BaseThingHandler { public static final String IP_ADDRESS = "ipAddress"; public static final String POLLING_REFRESH_INTERVAL = "refreshInterval"; + public static final int POLLING_REFRESH_INTERVAL_DEFAULT = 15; public static final int REPORT_INTERVAL = 3000; - public static final int PING_TIME_OUT = 3000; public static final int BUFFER_SIZE = 1024; public static final int REMOTE_PORT_NUMBER = 7090; private static final String CACHE_REPORT_1 = "REPORT_1"; private static final String CACHE_REPORT_2 = "REPORT_2"; private static final String CACHE_REPORT_3 = "REPORT_3"; private static final String CACHE_REPORT_100 = "REPORT_100"; + public static final int SOCKET_TIME_OUT_MS = 3000; + public static final int SOCKET_CHECK_PORT_NUMBER = 80; private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class); - protected final JsonParser parser = new JsonParser(); private final KeContactTransceiver transceiver; private ScheduledFuture pollingJob; @@ -94,32 +101,46 @@ public KeContactHandler(Thing thing, KeContactTransceiver transceiver) { @Override public void initialize() { - if (getConfig().get(IP_ADDRESS) != null && !getConfig().get(IP_ADDRESS).equals("")) { - transceiver.registerHandler(this); - - cache = new ExpiringCacheMap<>( - Math.max((((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()) - 5, 0) * 1000); - - cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler())); - cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler())); - cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler())); - cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler())); - - if (pollingJob == null || pollingJob.isCancelled()) { - try { - pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, - ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue(), TimeUnit.SECONDS); - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, - "An exception occurred while scheduling the polling job"); + try { + if (isKebaReachable()) { + transceiver.registerHandler(this); + + int refreshInterval = getRefreshInterval(); + cache = new ExpiringCacheMap<>(Math.max(refreshInterval - 5, 0) * 1000); + + cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler())); + cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler())); + cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler())); + cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler())); + + if (pollingJob == null || pollingJob.isCancelled()) { + pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval, + TimeUnit.SECONDS); } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "IP address or port number not set"); } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "IP address or port number not set"); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Exception during initialization of binding: " + e.toString()); } } + private boolean isKebaReachable() throws IOException { + boolean isReachable = false; + SocketAddress sockAddr = new InetSocketAddress(getIPAddress(), SOCKET_CHECK_PORT_NUMBER); + Socket socket = new Socket(); + try { + socket.connect(sockAddr, SOCKET_TIME_OUT_MS); + isReachable = true; + } finally { + socket.close(); + } + logger.debug("isKebaReachable() returns {}", isReachable); + return isReachable; + } + @Override public void dispose() { if (pollingJob != null && !pollingJob.isCancelled()) { @@ -134,6 +155,12 @@ public String getIPAddress() { return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : ""; } + public int getRefreshInterval() { + return getConfig().get(POLLING_REFRESH_INTERVAL) != null + ? ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue() + : POLLING_REFRESH_INTERVAL_DEFAULT; + } + private KeContactHandler getHandler() { return this; } @@ -150,9 +177,10 @@ protected Configuration getConfig() { private void pollingRunnable() { try { + logger.debug("Running pollingRunnable to connect Keba wallbox"); long stamp = System.currentTimeMillis(); - if (!InetAddress.getByName(((String) getConfig().get(IP_ADDRESS))).isReachable(PING_TIME_OUT)) { - logger.debug("Ping timed out after '{}' milliseconds", System.currentTimeMillis() - stamp); + if (!isKebaReachable()) { + logger.debug("isKebaReachable() timed out after '{}' milliseconds", System.currentTimeMillis() - stamp); transceiver.unRegisterHandler(getHandler()); } else { if (getThing().getStatus() == ThingStatus.ONLINE) { @@ -207,7 +235,7 @@ protected void onData(ByteBuffer byteBuffer) { } try { - JsonObject readObject = parser.parse(response).getAsJsonObject(); + JsonObject readObject = JsonParser.parseString(response).getAsJsonObject(); for (Entry entry : readObject.entrySet()) { switch (entry.getKey()) { @@ -300,14 +328,15 @@ protected void onData(ByteBuffer byteBuffer) { case "Curr HW": { int state = entry.getValue().getAsInt(); maxSystemCurrent = state; - State newState = new DecimalType(state); + State newState = new QuantityType(state / 1000.0, Units.AMPERE); updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState); if (maxSystemCurrent != 0) { if (maxSystemCurrent < maxPresetCurrent) { transceiver.send("curr " + String.valueOf(maxSystemCurrent), this); - updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(maxSystemCurrent)); - updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, - new PercentType((maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000))); + updateState(CHANNEL_MAX_PRESET_CURRENT, + new QuantityType(maxSystemCurrent / 1000.0, Units.AMPERE)); + updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType( + (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT)); } } else { logger.debug("maxSystemCurrent is 0. Ignoring."); @@ -317,24 +346,31 @@ protected void onData(ByteBuffer byteBuffer) { case "Curr user": { int state = entry.getValue().getAsInt(); maxPresetCurrent = state; - updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(state)); + State newState = new QuantityType(state / 1000.0, Units.AMPERE); + updateState(CHANNEL_MAX_PRESET_CURRENT, newState); if (maxSystemCurrent != 0) { - updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, - new PercentType(Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)))); + updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType( + Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT)); } break; } case "Curr FS": { int state = entry.getValue().getAsInt(); - State newState = new DecimalType(state); + State newState = new QuantityType(state / 1000.0, Units.AMPERE); updateState(CHANNEL_FAILSAFE_CURRENT, newState); break; } case "Max curr": { int state = entry.getValue().getAsInt(); maxPresetCurrent = state; - updateState(CHANNEL_PILOT_CURRENT, new DecimalType(state)); - updateState(CHANNEL_PILOT_PWM, new DecimalType(state)); + State newState = new QuantityType(state / 1000.0, Units.AMPERE); + updateState(CHANNEL_PILOT_CURRENT, newState); + break; + } + case "Max curr %": { + int state = entry.getValue().getAsInt(); + State newState = new QuantityType(state / 10.0, Units.PERCENT); + updateState(CHANNEL_PILOT_PWM, newState); break; } case "Output": { @@ -367,73 +403,67 @@ protected void onData(ByteBuffer byteBuffer) { } case "Sec": { long state = entry.getValue().getAsLong(); - - Calendar uptime = Calendar.getInstance(); - uptime.setTimeZone(TimeZone.getTimeZone("GMT")); - uptime.setTimeInMillis(state * 1000); - SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - pFormatter.setTimeZone(TimeZone.getTimeZone("GMT")); - - updateState(CHANNEL_UPTIME, new DateTimeType(pFormatter.format(uptime.getTime()))); + State newState = new QuantityType
- Specifies the refresh interval in seconds. + Specifies the refresh interval in seconds 15
@@ -134,82 +134,82 @@ - Number + Number:ElectricCurrent - Preset Current in mA - + Preset Current + - Number + Number:ElectricCurrent - Current in mA - + Current + - Number + Number:ElectricCurrent - Maximal System Current in mA - + Maximal System Current + - Number + Number:ElectricCurrent - Failsafe Current in mA (if network is lost) - + Failsafe Current (if network is lost) + - Dimmer + Number:Dimensionless - Current in % of the 6000-63000 mA range accepted by the wallbox - + Current in % of the 6-63 A range accepted by the wallbox + - Number + Number:ElectricCurrent - Current preset value via Control pilot in mA - + Current value offered to the vehicle via control pilot signalization (PWM) + - - Number - - Current preset value via Control pilot in 0,1% of the PWM value - + + Number:Dimensionless + + Duty cycle of the control pilot signal + - DateTime + Number:Time System uptime since the last reset of the wallbox - + - Number + Number:ElectricPotential - Voltage in V - + Voltage + - Number + Number:Power - Active Power in W - + Active Power + - Number + Number:Dimensionless Power factor (cosphi) - + - Number - - Power consumption in Wh. - + Number:Energy + + Power consumption + - Number - + Number:Energy + Total energy consumption is added up after each completed charging session - + String @@ -248,10 +248,10 @@ - Number + Number:Energy - An energy limit for an already running or the next charging session. - + An energy limit for an already running or the next charging session + String diff --git a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Cryption.java b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Cryption.java index dfb9af331f58f..35261b4ffb190 100644 --- a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Cryption.java +++ b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Cryption.java @@ -23,7 +23,6 @@ import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -131,8 +130,8 @@ private byte[] addZeroPadding(byte[] bdata, int bSize, String cSet) throws Unsup * @author Markus Eckhardt */ public void recreateKeys() { - if (StringUtils.isNotBlank(remoteDevice.getGatewayPassword()) - && StringUtils.isNotBlank(remoteDevice.getPrivatePassword()) && remoteDevice.getMD5Salt().length > 0) { + if (!remoteDevice.getGatewayPassword().isBlank() && !remoteDevice.getPrivatePassword().isBlank() + && remoteDevice.getMD5Salt().length > 0) { byte[] md5K1 = null; byte[] md5K2Init = null; byte[] md5K2Private = null; diff --git a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Device.java b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Device.java index aefc94246a8a2..7a6e73609d472 100644 --- a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Device.java +++ b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/KM200Device.java @@ -19,8 +19,7 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -41,7 +40,6 @@ public class KM200Device { private final Logger logger = LoggerFactory.getLogger(KM200Device.class); - private final JsonParser jsonParser = new JsonParser(); private final KM200Cryption comCryption; private final KM200Comm deviceCommunicator; @@ -84,7 +82,7 @@ public KM200Device(HttpClient httpClient) { } public Boolean isConfigured() { - return StringUtils.isNotBlank(ip4Address) && cryptKeyPriv.length > 0; + return !ip4Address.isBlank() && cryptKeyPriv.length > 0; } public String getIP4Address() { @@ -361,7 +359,7 @@ public Boolean containsService(String service) { logger.debug("{}: SERVICE NOT AVAILABLE", service); return null; } else { - nodeRoot = (JsonObject) jsonParser.parse(decodedData); + nodeRoot = (JsonObject) JsonParser.parseString(decodedData); } } else { logger.debug("Get empty reply"); diff --git a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200DataHandler.java b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200DataHandler.java index ea3c9bc0273b9..e490b59011adc 100644 --- a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200DataHandler.java +++ b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200DataHandler.java @@ -48,7 +48,6 @@ @NonNullByDefault public class KM200DataHandler { private final Logger logger = LoggerFactory.getLogger(KM200DataHandler.class); - private final JsonParser jsonParser = new JsonParser(); private final KM200Device remoteDevice; @@ -539,7 +538,7 @@ public KM200DataHandler(KM200Device remoteDevice) { /* The JSONArray of switch items can be send directly */ try { /* Check whether this input string is a valid JSONArray */ - JsonArray userArray = (JsonArray) jsonParser.parse(val); + JsonArray userArray = (JsonArray) JsonParser.parseString(val); newObject = userArray.getAsJsonObject(); } catch (JsonParseException e) { logger.warn("The input for the switchProgram is not a valid JSONArray : {}", diff --git a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200GatewayHandler.java b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200GatewayHandler.java index 8b11ac6e5e92e..3a125dd152467 100644 --- a/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200GatewayHandler.java +++ b/bundles/org.openhab.binding.km200/src/main/java/org/openhab/binding/km200/internal/handler/KM200GatewayHandler.java @@ -30,7 +30,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -177,7 +176,7 @@ private void getConfiguration() { switch (key) { case "ip4Address": String ip = (String) configuration.get("ip4Address"); - if (StringUtils.isNotBlank(ip)) { + if (ip != null && !ip.isBlank()) { try { InetAddress.getByName(ip); } catch (UnknownHostException e) { @@ -190,25 +189,25 @@ private void getConfiguration() { break; case "privateKey": String privateKey = (String) configuration.get("privateKey"); - if (StringUtils.isNotBlank(privateKey)) { + if (privateKey != null && !privateKey.isBlank()) { getDevice().setCryptKeyPriv(privateKey); } break; case "md5Salt": String md5Salt = (String) configuration.get("md5Salt"); - if (StringUtils.isNotBlank(md5Salt)) { + if (md5Salt != null && !md5Salt.isBlank()) { getDevice().setMD5Salt(md5Salt); } break; case "gatewayPassword": String gatewayPassword = (String) configuration.get("gatewayPassword"); - if (StringUtils.isNotBlank(gatewayPassword)) { + if (gatewayPassword != null && !gatewayPassword.isBlank()) { getDevice().setGatewayPassword(gatewayPassword); } break; case "privatePassword": String privatePassword = (String) configuration.get("privatePassword"); - if (StringUtils.isNotBlank(privatePassword)) { + if (privatePassword != null && !privatePassword.isBlank()) { getDevice().setPrivatePassword(privatePassword); } break; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java index 605f18a7b271c..ef2f93085dca4 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java @@ -13,6 +13,7 @@ package org.openhab.binding.knx.internal.dpt; import java.math.BigDecimal; +import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; @@ -1017,6 +1018,6 @@ private int getMainNumber(String dptID) { */ private int convertPercentToByte(PercentType percent) { return percent.toBigDecimal().multiply(BigDecimal.valueOf(255)) - .divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP).intValue(); + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).intValue(); } } diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/config/channelConfig.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/config/channelConfig.xml index 4f8f11338100c..f4256cfce783f 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/config/channelConfig.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/config/channelConfig.xml @@ -7,37 +7,31 @@ - + The group address(es) in Group Address Notation to toggle the dimmer on or off - false - + The group address(es) in Group Address Notation to set the absolute position of the dimmer - false - + The group address(es) in Group Address Notation to increase or decrease the dimmer - false - + The group address(es) in Group Address Notation to toggle the dimmer on or off - false - + The group address(es) in Group Address Notation to set the absolute position of the dimmer - false - + The group address(es) in Group Address Notation to increase or decrease the dimmer - false @@ -49,47 +43,39 @@ - + The group address(es) in Group Address Notation for the color value - false - + The group address(es) in Group Address Notation to toggle the color on or off - false - + The group address(es) in Group Address Notation to set the absolute position of the color - false - + The group address(es) in Group Address Notation to increase or decrease the color - false - + The group address(es) in Group Address Notation for the color value - false - + The group address(es) in Group Address Notation to toggle the color on or off - false - + The group address(es) in Group Address Notation to set the absolute position of the color - false - + The group address(es) in Group Address Notation to increase or decrease the color - false @@ -101,29 +87,25 @@ - + The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction - false - + The group address(es) in Group Address Notation to start (MOVE) or STOP shutter movement - false - + The group address(es) in Group Address Notation to set the absolute position of the shutter, in % - false - + The group address(es) in Group Address Notation - true diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml index 36fb253ed5669..89e2c6472f22f 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml @@ -9,10 +9,9 @@ This is a KNX IP interface or router - + The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER - true @@ -23,9 +22,8 @@ Network address of the KNX/IP gateway network-address - + Port number of the KNX/IP gateway - false 3671 diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml index 1a5f39510c82e..bb7705928889d 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml @@ -8,35 +8,30 @@ This is a serial interface for accessing the KNX bus - + serial-port The serial port to use for connecting to the KNX bus - true - + Time in milliseconds of how long should be paused between two read requests to the bus during initialization - true 50 - + Seconds to wait for a response from the KNX bus - true 10 - + Limits the read retries while initialization from the KNX bus - true 3 - + Seconds between connect retries when KNX link has been lost, 0 means never retry - true 0 diff --git a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java index 8f77edfce2f28..5226074f33fe0 100644 --- a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java +++ b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java @@ -54,7 +54,6 @@ public class KodiClientSocket { private boolean connected = false; - private final JsonParser parser = new JsonParser(); private final Gson mapper = new Gson(); private final URI uri; private final WebSocketClient client; @@ -130,7 +129,7 @@ public void onConnect(Session wssession) { @OnWebSocketMessage public void onMessage(String message) { logger.trace("Message received from server: {}", message); - final JsonObject json = parser.parse(message).getAsJsonObject(); + final JsonObject json = JsonParser.parseString(message).getAsJsonObject(); if (json.has("id")) { int messageId = json.get("id").getAsInt(); if (messageId == nextMessageId - 1) { diff --git a/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/binding/binding.xml index 227ba6fe3d73a..a72c02b08b06c 100644 --- a/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/binding/binding.xml @@ -7,10 +7,9 @@ This is the binding for Kodi. - + url to use for playing notification sounds, e.g. http://192.168.0.2:8080 - false diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationConfigurationHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationConfigurationHandler.java index e5d064e80a948..d045056555bef 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationConfigurationHandler.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationConfigurationHandler.java @@ -54,12 +54,12 @@ public static void executeConfigurationChanges(HttpClient httpClient, String url String getAuthenticateResponse = httpClient.GET(urlLogin).getContentAsString(); try { - JsonObject getAuthenticateResponseJsonObject = (JsonObject) new JsonParser() - .parse(transformJsonResponse(getAuthenticateResponse)); + JsonObject getAuthenticateResponseJsonObject = (JsonObject) JsonParser + .parseString(transformJsonResponse(getAuthenticateResponse)); sessionId = extractSessionId(getAuthenticateResponseJsonObject); - JsonObject authenticateJsonObject = new JsonParser().parse(getAuthenticateResponse.toString()) + JsonObject authenticateJsonObject = JsonParser.parseString(getAuthenticateResponse.toString()) .getAsJsonObject(); salt = authenticateJsonObject.get("salt").getAsString(); @@ -84,8 +84,8 @@ public static void executeConfigurationChanges(HttpClient httpClient, String url String loginPostResponse = new String(loginPostJsonDataContentResponse.getContent(), StandardCharsets.UTF_8); - JsonObject loginPostJsonObject = (JsonObject) new JsonParser() - .parse(transformJsonResponse(loginPostResponse)); + JsonObject loginPostJsonObject = (JsonObject) JsonParser + .parseString(transformJsonResponse(loginPostResponse)); sessionId = extractSessionId(loginPostJsonObject); diff --git a/bundles/org.openhab.binding.kvv/src/main/resources/OH-INF/thing/stop.xml b/bundles/org.openhab.binding.kvv/src/main/resources/OH-INF/thing/stop.xml index 78ca734fe2099..eca82ca95a25e 100644 --- a/bundles/org.openhab.binding.kvv/src/main/resources/OH-INF/thing/stop.xml +++ b/bundles/org.openhab.binding.kvv/src/main/resources/OH-INF/thing/stop.xml @@ -39,10 +39,9 @@ Train stop for KVV Binding. - + ID of the train stop. - true diff --git a/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/LaMetricTimeHandler.java b/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/LaMetricTimeHandler.java index 887a7dbbf799c..026076918ac32 100644 --- a/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/LaMetricTimeHandler.java +++ b/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/LaMetricTimeHandler.java @@ -25,7 +25,6 @@ import javax.ws.rs.client.ClientBuilder; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.lametrictime.api.Configuration; import org.openhab.binding.lametrictime.api.LaMetricTime; import org.openhab.binding.lametrictime.api.local.ApplicationActivationException; @@ -360,12 +359,12 @@ public Collection getConfigStatus() { String host = config.host; String apiKey = config.apiKey; - if (StringUtils.isEmpty(host)) { + if (host == null || host.isEmpty()) { configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST) .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build()); } - if (StringUtils.isEmpty(apiKey)) { + if (apiKey == null || apiKey.isEmpty()) { configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY) .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY) .build()); diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java index 4cf96729bd006..19370b2c58540 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java @@ -21,7 +21,6 @@ import org.openhab.binding.lcn.internal.LcnBindingConstants; import org.openhab.binding.lcn.internal.LcnModuleHandler; import org.openhab.binding.lcn.internal.common.LcnChannelGroup; -import org.openhab.binding.lcn.internal.common.LcnDefs; import org.openhab.binding.lcn.internal.common.LcnException; import org.openhab.binding.lcn.internal.common.PckGenerator; import org.openhab.binding.lcn.internal.common.Variable; @@ -51,21 +50,7 @@ public LcnModuleRvarSetpointSubHandler(LcnModuleHandler handler, ModInfo info) { @Override public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) throws LcnException { - Variable variable = getVariable(channelGroup, number); - - if (info.hasExtendedMeasurementProcessing()) { - handler.sendPck(PckGenerator.setSetpointAbsolute(number, command.intValue())); - } else { - try { - int relativeVariableChange = getRelativeChange(command, variable); - handler.sendPck( - PckGenerator.setSetpointRelative(number, LcnDefs.RelVarRef.CURRENT, relativeVariableChange)); - } catch (LcnException e) { - // current value unknown for some reason, refresh it in case we come again here - info.refreshVariable(variable); - throw e; - } - } + handler.sendPck(PckGenerator.setSetpointAbsolute(number, command.intValue())); } @Override diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml index e85c984390c33..0e6dfa6c8b431 100644 --- a/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml @@ -10,18 +10,15 @@ The hostname or the IP address of the PCK gateway network-address - true The IP port of the PCK gateway 4114 - true The login username of the PCK gateway - true @@ -36,7 +33,6 @@ - true @@ -91,7 +87,6 @@ - true @@ -99,4 +94,11 @@ 1000 + + + + + Time in seconds until the output's value is reached. + + diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java index a142c771b6742..f1f49d617c8ba 100644 --- a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java @@ -74,7 +74,7 @@ public void testhandleCommandRvar1PositiveLegacy() throws LcnException { when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); when(info.hasExtendedMeasurementProcessing()).thenReturn(false); l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 0); - verify(handler).sendPck("REASA+100"); + verify(handler).sendPck("X2030032100"); } @Test @@ -82,7 +82,7 @@ public void testhandleCommandRvar2PositiveLegacy() throws LcnException { when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); when(info.hasExtendedMeasurementProcessing()).thenReturn(false); l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1); - verify(handler).sendPck("REBSA+100"); + verify(handler).sendPck("X2030096100"); } @Test @@ -90,7 +90,7 @@ public void testhandleCommandRvar1NegativeLegacy() throws LcnException { when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); when(info.hasExtendedMeasurementProcessing()).thenReturn(false); l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 0); - verify(handler).sendPck("REASA-100"); + verify(handler).sendPck("X2030040100"); } @Test @@ -98,7 +98,7 @@ public void testhandleCommandRvar2NegativeLegacy() throws LcnException { when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); when(info.hasExtendedMeasurementProcessing()).thenReturn(false); l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 1); - verify(handler).sendPck("REBSA-100"); + verify(handler).sendPck("X2030104100"); } @Test diff --git a/bundles/org.openhab.binding.leapmotion/src/main/feature/feature.xml b/bundles/org.openhab.binding.leapmotion/src/main/feature/feature.xml index 4292bff68d6d0..75e76a87d0b36 100644 --- a/bundles/org.openhab.binding.leapmotion/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.leapmotion/src/main/feature/feature.xml @@ -5,7 +5,7 @@ openhab-runtime-base mvn:org.openhab.addons.bundles/org.openhab.binding.leapmotion/${project.version} - mvn:${project.groupId}/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/lib/libLeap - mvn:${project.groupId}/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/lib/libLeapJava + mvn:org.openhab.addons.features.karaf/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/lib/libLeap + mvn:org.openhab.addons.features.karaf/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/lib/libLeapJava diff --git a/bundles/org.openhab.binding.lgtvserial/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.lgtvserial/src/main/resources/OH-INF/config/config.xml index 033439cf27f03..69421ba77ac3c 100644 --- a/bundles/org.openhab.binding.lgtvserial/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.lgtvserial/src/main/resources/OH-INF/config/config.xml @@ -4,17 +4,15 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + serial-port false Select serial port (COM1, /dev/ttyS0, ...) - true - + Set ID configured in the TV. If 0, this will send a command to every chained TV. - true 1 diff --git a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/LGWebOSHandlerFactory.java b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/LGWebOSHandlerFactory.java index 4273128faf120..250eda2ac1bfc 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/LGWebOSHandlerFactory.java +++ b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/LGWebOSHandlerFactory.java @@ -81,8 +81,8 @@ protected void activate(ComponentContext componentContext) { // reduce timeout from default 15sec this.webSocketClient.setConnectTimeout(1000); - // channel and app listing are json docs up to 3MB - this.webSocketClient.getPolicy().setMaxTextMessageSize(3 * 1024 * 1024); + // channel and app listing are json docs up to 4MB + this.webSocketClient.getPolicy().setMaxTextMessageSize(4 * 1024 * 1024); // since this is not using openHAB's shared web socket client we need to start and stop try { diff --git a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java index bc34e2e381d03..663318bd3e181 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java +++ b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java @@ -154,8 +154,7 @@ public void launchApplication( @ActionInput(name = "appId", label = "@text/actionLaunchApplicationInputAppIDLabel", description = "@text/actionLaunchApplicationInputAppIDDesc") String appId, @ActionInput(name = "params", label = "@text/actionLaunchApplicationInputParamsLabel", description = "@text/actionLaunchApplicationInputParamsDesc") String params) { try { - JsonParser parser = new JsonParser(); - JsonObject payload = (JsonObject) parser.parse(params); + JsonObject payload = (JsonObject) JsonParser.parseString(params); Optional appInfo = getAppInfos().stream().filter(a -> a.getId().equals(appId)).findFirst(); if (appInfo.isPresent()) { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactory.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactory.java index dca40a8ef1fb5..b507648a71f7d 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactory.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactory.java @@ -19,7 +19,7 @@ /** * The {@link LifxChannelFactory} creates dynamic LIFX channels. * - * @author Wouter Born - Add i18n support + * @author Wouter Born - Initial contribution */ @NonNullByDefault public interface LifxChannelFactory { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactoryImpl.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactoryImpl.java index 7c8c3b12ecca4..333c610a7486f 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactoryImpl.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxChannelFactoryImpl.java @@ -34,7 +34,7 @@ /** * The {@link LifxChannelFactoryImpl} creates dynamic LIFX channels. * - * @author Wouter Born - Add i18n support + * @author Wouter Born - Initial contribution */ @NonNullByDefault @Component(service = LifxChannelFactory.class) diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCommunicationHandler.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCommunicationHandler.java index 375ff33aa8bed..bd27a3a7a83fc 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCommunicationHandler.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCommunicationHandler.java @@ -32,12 +32,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.dto.GetServiceRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.StateServiceResponse; import org.openhab.binding.lifx.internal.fields.MACAddress; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; import org.openhab.binding.lifx.internal.listener.LifxResponsePacketListener; -import org.openhab.binding.lifx.internal.protocol.GetServiceRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.StateServiceResponse; import org.openhab.binding.lifx.internal.util.LifxNetworkUtil; import org.openhab.binding.lifx.internal.util.LifxSelectorUtil; import org.slf4j.Logger; @@ -46,7 +46,7 @@ /** * The {@link LifxLightCommunicationHandler} is responsible for the communications with a light. * - * @author Wouter Born - Extracted class from LifxLightHandler + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightCommunicationHandler { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightConfig.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightConfig.java index f39c6fbf5162d..4b193398c824a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightConfig.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightConfig.java @@ -33,7 +33,7 @@ public class LifxLightConfig { public @Nullable MACAddress getMACAddress() { String localDeviceId = deviceId; - return localDeviceId == null ? null : new MACAddress(localDeviceId, true); + return localDeviceId == null ? null : new MACAddress(localDeviceId); } public @Nullable InetSocketAddress getHost() { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java index 254d87921c8b1..5a65ff3fa412c 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java @@ -15,14 +15,14 @@ import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lifx.internal.LifxProduct.Features; import org.openhab.binding.lifx.internal.handler.LifxLightHandler; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; -import org.openhab.binding.lifx.internal.protocol.Product; /** * The {@link LifxLightContext} shares the context of a light with {@link LifxLightHandler} helper objects. * - * @author Wouter Born - Add optional host configuration parameter + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightContext { @@ -31,14 +31,14 @@ public class LifxLightContext { private final LifxLightConfig configuration; private final CurrentLightState currentLightState; private final LifxLightState pendingLightState; - private final Product product; + private final Features features; private final ScheduledExecutorService scheduler; - public LifxLightContext(String logId, Product product, LifxLightConfig configuration, + public LifxLightContext(String logId, Features features, LifxLightConfig configuration, CurrentLightState currentLightState, LifxLightState pendingLightState, ScheduledExecutorService scheduler) { this.logId = logId; this.configuration = configuration; - this.product = product; + this.features = features; this.currentLightState = currentLightState; this.pendingLightState = pendingLightState; this.scheduler = scheduler; @@ -52,8 +52,8 @@ public LifxLightConfig getConfiguration() { return configuration; } - public Product getProduct() { - return product; + public Features getFeatures() { + return features; } public CurrentLightState getCurrentLightState() { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java index ad94aa26514f5..ef6ad12b69152 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java @@ -13,7 +13,7 @@ package org.openhab.binding.lifx.internal; import static org.openhab.binding.lifx.internal.LifxBindingConstants.MIN_ZONE_INDEX; -import static org.openhab.binding.lifx.internal.protocol.Product.Feature.*; +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.infraredToPercentType; import java.util.concurrent.ScheduledExecutorService; @@ -23,22 +23,22 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.LifxProduct.Features; +import org.openhab.binding.lifx.internal.dto.GetColorZonesRequest; +import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest; +import org.openhab.binding.lifx.internal.dto.GetRequest; +import org.openhab.binding.lifx.internal.dto.GetTileEffectRequest; +import org.openhab.binding.lifx.internal.dto.GetWifiInfoRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.StateLightInfraredResponse; +import org.openhab.binding.lifx.internal.dto.StateLightPowerResponse; +import org.openhab.binding.lifx.internal.dto.StateMultiZoneResponse; +import org.openhab.binding.lifx.internal.dto.StatePowerResponse; +import org.openhab.binding.lifx.internal.dto.StateResponse; +import org.openhab.binding.lifx.internal.dto.StateTileEffectResponse; +import org.openhab.binding.lifx.internal.dto.StateWifiInfoResponse; import org.openhab.binding.lifx.internal.fields.HSBK; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; -import org.openhab.binding.lifx.internal.protocol.GetColorZonesRequest; -import org.openhab.binding.lifx.internal.protocol.GetLightInfraredRequest; -import org.openhab.binding.lifx.internal.protocol.GetRequest; -import org.openhab.binding.lifx.internal.protocol.GetTileEffectRequest; -import org.openhab.binding.lifx.internal.protocol.GetWifiInfoRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.Product; -import org.openhab.binding.lifx.internal.protocol.StateLightInfraredResponse; -import org.openhab.binding.lifx.internal.protocol.StateLightPowerResponse; -import org.openhab.binding.lifx.internal.protocol.StateMultiZoneResponse; -import org.openhab.binding.lifx.internal.protocol.StatePowerResponse; -import org.openhab.binding.lifx.internal.protocol.StateResponse; -import org.openhab.binding.lifx.internal.protocol.StateTileEffectResponse; -import org.openhab.binding.lifx.internal.protocol.StateWifiInfoResponse; import org.openhab.core.library.types.PercentType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,7 +47,7 @@ * The {@link LifxLightCurrentStateUpdater} sends packets to a light in order to update the {@code currentLightState} to * the actual light state. * - * @author Wouter Born - Extracted class from LifxLightHandler + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightCurrentStateUpdater { @@ -57,7 +57,7 @@ public class LifxLightCurrentStateUpdater { private final Logger logger = LoggerFactory.getLogger(LifxLightCurrentStateUpdater.class); private final String logId; - private final Product product; + private final Features features; private final CurrentLightState currentLightState; private final ScheduledExecutorService scheduler; private final LifxLightCommunicationHandler communicationHandler; @@ -71,7 +71,7 @@ public class LifxLightCurrentStateUpdater { public LifxLightCurrentStateUpdater(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) { this.logId = context.getLogId(); - this.product = context.getProduct(); + this.features = context.getFeatures(); this.currentLightState = context.getCurrentLightState(); this.scheduler = context.getScheduler(); this.communicationHandler = communicationHandler; @@ -133,13 +133,13 @@ public void stop() { private void sendLightStateRequests() { communicationHandler.sendPacket(new GetRequest()); - if (product.hasFeature(INFRARED)) { + if (features.hasFeature(INFRARED)) { communicationHandler.sendPacket(new GetLightInfraredRequest()); } - if (product.hasFeature(MULTIZONE)) { + if (features.hasFeature(MULTIZONE)) { communicationHandler.sendPacket(new GetColorZonesRequest()); } - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { communicationHandler.sendPacket(new GetTileEffectRequest()); } if (updateSignalStrength) { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java index 21011010dbe48..5799af1994af7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java @@ -26,18 +26,16 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.dto.GetLabelRequest; +import org.openhab.binding.lifx.internal.dto.GetServiceRequest; +import org.openhab.binding.lifx.internal.dto.GetVersionRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.StateLabelResponse; +import org.openhab.binding.lifx.internal.dto.StateServiceResponse; +import org.openhab.binding.lifx.internal.dto.StateVersionResponse; import org.openhab.binding.lifx.internal.fields.MACAddress; -import org.openhab.binding.lifx.internal.protocol.GetLabelRequest; -import org.openhab.binding.lifx.internal.protocol.GetServiceRequest; -import org.openhab.binding.lifx.internal.protocol.GetVersionRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.Product; -import org.openhab.binding.lifx.internal.protocol.StateLabelResponse; -import org.openhab.binding.lifx.internal.protocol.StateServiceResponse; -import org.openhab.binding.lifx.internal.protocol.StateVersionResponse; import org.openhab.binding.lifx.internal.util.LifxSelectorUtil; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; @@ -87,7 +85,7 @@ private class DiscoveredLight { private InetSocketAddress socketAddress; private String logId; private @Nullable String label; - private @Nullable Product product; + private @Nullable LifxProduct product; private long productVersion; private boolean supportedProduct = true; private LifxSelectorContext selectorContext; @@ -300,8 +298,11 @@ private void handlePacket(Packet packet, InetSocketAddress address) { light.label = ((StateLabelResponse) packet).getLabel().trim(); } else if (packet instanceof StateVersionResponse) { try { - light.product = Product.getProductFromProductID(((StateVersionResponse) packet).getProduct()); + LifxProduct product = LifxProduct + .getProductFromProductID(((StateVersionResponse) packet).getProduct()); + light.product = product; light.productVersion = ((StateVersionResponse) packet).getVersion(); + light.supportedProduct = product.isLight(); } catch (IllegalArgumentException e) { logger.debug("Discovered an unsupported light ({}): {}", light.macAddress.getAsLabel(), e.getMessage()); @@ -310,7 +311,7 @@ private void handlePacket(Packet packet, InetSocketAddress address) { } } - if (light != null && light.isDataComplete()) { + if (light != null && light.supportedProduct && light.isDataComplete()) { try { thingDiscovered(createDiscoveryResult(light)); } catch (IllegalArgumentException e) { @@ -322,7 +323,7 @@ private void handlePacket(Packet packet, InetSocketAddress address) { } private DiscoveryResult createDiscoveryResult(DiscoveredLight light) throws IllegalArgumentException { - Product product = light.product; + LifxProduct product = light.product; if (product == null) { throw new IllegalArgumentException("Product of discovered light is null"); } @@ -331,7 +332,7 @@ private DiscoveryResult createDiscoveryResult(DiscoveredLight light) throws Ille ThingUID thingUID = new ThingUID(product.getThingTypeUID(), macAsLabel); String label = light.label; - if (StringUtils.isBlank(label)) { + if (label == null || label.isBlank()) { label = product.getName(); } diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightOnlineStateUpdater.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightOnlineStateUpdater.java index c40400e3678ba..62ef83091cdf9 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightOnlineStateUpdater.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightOnlineStateUpdater.java @@ -21,17 +21,17 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.dto.GetEchoRequest; +import org.openhab.binding.lifx.internal.dto.GetServiceRequest; +import org.openhab.binding.lifx.internal.dto.Packet; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; -import org.openhab.binding.lifx.internal.protocol.GetEchoRequest; -import org.openhab.binding.lifx.internal.protocol.GetServiceRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link LifxLightOnlineStateUpdater} sets the state of a light offline when it no longer responds to echo packets. * - * @author Wouter Born - Extracted class from LifxLightHandler + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightOnlineStateUpdater { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightPropertiesUpdater.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightPropertiesUpdater.java index 7be78cffbd52c..9de0a75ff6f6a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightPropertiesUpdater.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightPropertiesUpdater.java @@ -27,17 +27,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.dto.GetHostFirmwareRequest; +import org.openhab.binding.lifx.internal.dto.GetVersionRequest; +import org.openhab.binding.lifx.internal.dto.GetWifiFirmwareRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.StateHostFirmwareResponse; +import org.openhab.binding.lifx.internal.dto.StateVersionResponse; +import org.openhab.binding.lifx.internal.dto.StateWifiFirmwareResponse; import org.openhab.binding.lifx.internal.fields.MACAddress; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; import org.openhab.binding.lifx.internal.listener.LifxPropertiesUpdateListener; -import org.openhab.binding.lifx.internal.protocol.GetHostFirmwareRequest; -import org.openhab.binding.lifx.internal.protocol.GetVersionRequest; -import org.openhab.binding.lifx.internal.protocol.GetWifiFirmwareRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.Product; -import org.openhab.binding.lifx.internal.protocol.StateHostFirmwareResponse; -import org.openhab.binding.lifx.internal.protocol.StateVersionResponse; -import org.openhab.binding.lifx.internal.protocol.StateWifiFirmwareResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +44,7 @@ * The {@link LifxLightPropertiesUpdater} updates the light properties when a light goes online. When packets get lost * the requests are resent when the {@code UPDATE_INTERVAL} elapses. * - * @author Wouter Born - Update light properties when online + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightPropertiesUpdater { @@ -157,7 +156,7 @@ public void handleResponsePacket(Packet packet) { properties.put(LifxBindingConstants.PROPERTY_PRODUCT_VERSION, Long.toString(productVersion)); try { - Product product = Product.getProductFromProductID(productId); + LifxProduct product = LifxProduct.getProductFromProductID(productId); properties.put(LifxBindingConstants.PROPERTY_PRODUCT_NAME, product.getName()); properties.put(LifxBindingConstants.PROPERTY_VENDOR_ID, Long.toString(product.getVendor().getID())); properties.put(LifxBindingConstants.PROPERTY_VENDOR_NAME, product.getVendor().getName()); diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightState.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightState.java index a4db52c45cfe6..b3bbefaf201a7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightState.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightState.java @@ -22,11 +22,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.dto.Effect; +import org.openhab.binding.lifx.internal.dto.PowerState; +import org.openhab.binding.lifx.internal.dto.SignalStrength; import org.openhab.binding.lifx.internal.fields.HSBK; import org.openhab.binding.lifx.internal.listener.LifxLightStateListener; -import org.openhab.binding.lifx.internal.protocol.Effect; -import org.openhab.binding.lifx.internal.protocol.PowerState; -import org.openhab.binding.lifx.internal.protocol.SignalStrength; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -34,7 +34,7 @@ /** * The {@link LifxLightState} stores the properties that represent the state of a light. * - * @author Wouter Born - Extracted class from LifxLightHandler, added listener logic + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightState { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java index 4e2ccd16ab8ec..dbccb2113b5d7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java @@ -13,7 +13,7 @@ package org.openhab.binding.lifx.internal; import static org.openhab.binding.lifx.internal.LifxBindingConstants.PACKET_INTERVAL; -import static org.openhab.binding.lifx.internal.protocol.Product.Feature.MULTIZONE; +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.MULTIZONE; import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.*; import java.time.Duration; @@ -29,25 +29,25 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.LifxProduct.Features; +import org.openhab.binding.lifx.internal.dto.AcknowledgementResponse; +import org.openhab.binding.lifx.internal.dto.ApplicationRequest; +import org.openhab.binding.lifx.internal.dto.Effect; +import org.openhab.binding.lifx.internal.dto.GetColorZonesRequest; +import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest; +import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest; +import org.openhab.binding.lifx.internal.dto.GetRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.PowerState; +import org.openhab.binding.lifx.internal.dto.SetColorRequest; +import org.openhab.binding.lifx.internal.dto.SetColorZonesRequest; +import org.openhab.binding.lifx.internal.dto.SetLightInfraredRequest; +import org.openhab.binding.lifx.internal.dto.SetLightPowerRequest; +import org.openhab.binding.lifx.internal.dto.SetPowerRequest; +import org.openhab.binding.lifx.internal.dto.SetTileEffectRequest; +import org.openhab.binding.lifx.internal.dto.SignalStrength; import org.openhab.binding.lifx.internal.fields.HSBK; import org.openhab.binding.lifx.internal.listener.LifxLightStateListener; -import org.openhab.binding.lifx.internal.protocol.AcknowledgementResponse; -import org.openhab.binding.lifx.internal.protocol.ApplicationRequest; -import org.openhab.binding.lifx.internal.protocol.Effect; -import org.openhab.binding.lifx.internal.protocol.GetColorZonesRequest; -import org.openhab.binding.lifx.internal.protocol.GetLightInfraredRequest; -import org.openhab.binding.lifx.internal.protocol.GetLightPowerRequest; -import org.openhab.binding.lifx.internal.protocol.GetRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.PowerState; -import org.openhab.binding.lifx.internal.protocol.Product; -import org.openhab.binding.lifx.internal.protocol.SetColorRequest; -import org.openhab.binding.lifx.internal.protocol.SetColorZonesRequest; -import org.openhab.binding.lifx.internal.protocol.SetLightInfraredRequest; -import org.openhab.binding.lifx.internal.protocol.SetLightPowerRequest; -import org.openhab.binding.lifx.internal.protocol.SetPowerRequest; -import org.openhab.binding.lifx.internal.protocol.SetTileEffectRequest; -import org.openhab.binding.lifx.internal.protocol.SignalStrength; import org.openhab.core.library.types.PercentType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +57,7 @@ * light so the change the actual light state to that of the {@code pendingLightState}. When the light does not * acknowledge a packet, it resends it (max 3 times). * - * @author Wouter Born - Extracted class from LifxLightHandler, added logic for handling packet loss + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxLightStateChanger implements LifxLightStateListener { @@ -75,7 +75,7 @@ public class LifxLightStateChanger implements LifxLightStateListener { private final Logger logger = LoggerFactory.getLogger(LifxLightStateChanger.class); private final String logId; - private final Product product; + private final Features features; private final Duration fadeTime; private final LifxLightState pendingLightState; private final ScheduledExecutorService scheduler; @@ -105,7 +105,7 @@ private boolean hasAcknowledgeIntervalElapsed() { public LifxLightStateChanger(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) { this.logId = context.getLogId(); - this.product = context.getProduct(); + this.features = context.getFeatures(); this.fadeTime = context.getConfiguration().getFadeTime(); this.pendingLightState = context.getPendingLightState(); this.scheduler = context.getScheduler(); @@ -372,7 +372,7 @@ public void handleResponsePacket(Packet packet) { } private void getZonesIfZonesAreSet() { - if (product.hasFeature(MULTIZONE)) { + if (features.hasFeature(MULTIZONE)) { List pending = pendingPacketsMap.get(SetColorZonesRequest.TYPE); if (pending == null || pending.isEmpty()) { GetColorZonesRequest zoneColorPacket = new GetColorZonesRequest(); diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java new file mode 100644 index 0000000000000..3f561393514e1 --- /dev/null +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java @@ -0,0 +1,433 @@ +/** + * Copyright (c) 2010-2021 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.lifx.internal; + +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; +import static org.openhab.binding.lifx.internal.LifxProduct.TemperatureRange.*; +import static org.openhab.binding.lifx.internal.LifxProduct.Vendor.LIFX; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Enumerates the LIFX products, their IDs and feature set. + * + * @see https://lan.developer.lifx.com/docs/lifx-products + * + * @author Wouter Born - Initial contribution + * @author Wouter Born - Add temperature ranges and simplify feature definitions + */ +@NonNullByDefault +public enum LifxProduct { + + PRODUCT_1(1, "LIFX Original 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_3(3, "LIFX Color 650", new Features(TR_2500_9000, COLOR)), + PRODUCT_10(10, "LIFX White 800 (Low Voltage)", new Features(TR_2700_6500)), + PRODUCT_11(11, "LIFX White 800 (High Voltage)", new Features(TR_2700_6500)), + PRODUCT_15(15, "LIFX Color 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_18(18, "LIFX White 900 BR30 (Low Voltage)", new Features(TR_2700_6500)), + PRODUCT_19(19, "LIFX White 900 BR30 (High Voltage)", new Features(TR_2700_6500)), + PRODUCT_20(20, "LIFX Color 1000 BR30", new Features(TR_2500_9000, COLOR)), + PRODUCT_22(22, "LIFX Color 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_27(27, "LIFX A19", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_28(28, "LIFX BR30", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_29(29, "LIFX A19 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_30(30, "LIFX BR30 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_31(31, "LIFX Z", new Features(TR_2500_9000, COLOR, MULTIZONE)), + PRODUCT_32(32, "LIFX Z", new Features(TR_2500_9000, COLOR, MULTIZONE), // + new Upgrade(2, 77, new Features(EXTENDED_MULTIZONE)), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_36(36, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_37(37, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_38(38, "LIFX Beam", new Features(TR_2500_9000, COLOR, MULTIZONE), // + new Upgrade(2, 77, new Features(EXTENDED_MULTIZONE)), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_39(39, "LIFX Downlight White to Warm", new Features(TR_2500_9000), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_40(40, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_43(43, "LIFX A19", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_44(44, "LIFX BR30", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_45(45, "LIFX A19 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_46(46, "LIFX BR30 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_49(49, "LIFX Mini Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_50(50, "LIFX Mini White to Warm", new Features(TR_1500_6500), // + new Upgrade(3, 70, new Features(TR_1500_9000))), + PRODUCT_51(51, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_52(52, "LIFX GU10", new Features(TR_1500_9000, COLOR)), + PRODUCT_53(53, "LIFX GU10", new Features(TR_1500_9000, COLOR)), + PRODUCT_55(55, "LIFX Tile", new Features(TR_2500_9000, CHAIN, COLOR, MATRIX, TILE_EFFECT)), + PRODUCT_57(57, "LIFX Candle", new Features(TR_1500_9000, COLOR, MATRIX)), + PRODUCT_59(59, "LIFX Mini Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_60(60, "LIFX Mini White to Warm", new Features(TR_1500_6500), // + new Upgrade(3, 70, new Features(TR_1500_9000))), + PRODUCT_61(61, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_62(62, "LIFX A19", new Features(TR_1500_9000, COLOR)), + PRODUCT_63(63, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_64(64, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_65(65, "LIFX BR30 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_66(66, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_68(68, "LIFX Candle", new Features(TR_1500_9000, MATRIX)), + PRODUCT_70(70, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_71(71, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_81(81, "LIFX Candle White to Warm", new Features(TR_2200_6500)), + PRODUCT_82(82, "LIFX Filament Clear", new Features(TR_2100_2100)), + PRODUCT_85(85, "LIFX Filament Amber", new Features(TR_2000_2000)), + PRODUCT_87(87, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_88(88, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_89(89, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_90(90, "LIFX Clean", new Features(TR_1500_9000, HEV)), + PRODUCT_91(91, "LIFX Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_92(92, "LIFX Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_94(94, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_96(96, "LIFX Candle White to Warm", new Features(TR_2200_6500)), + PRODUCT_97(97, "LIFX A19", new Features(TR_1500_9000, COLOR)), + PRODUCT_98(98, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_99(99, "LIFX Clean", new Features(TR_1500_9000, HEV)), + PRODUCT_100(100, "LIFX Filament Clear", new Features(TR_2100_2100)), + PRODUCT_101(101, "LIFX Filament Amber", new Features(TR_2000_2000)), + PRODUCT_109(109, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_110(110, "LIFX BR30 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_111(111, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)); + + /** + * Enumerates the product features. + */ + public enum Feature { + BUTTONS, + CHAIN, + COLOR, + EXTENDED_MULTIZONE, + HEV, + INFRARED, + MATRIX, + MULTIZONE, + RELAYS, + TILE_EFFECT + } + + /** + * Enumerates the product vendors. + */ + public enum Vendor { + LIFX(1, "LIFX"); + + private final int id; + private final String name; + + Vendor(int id, String name) { + this.id = id; + this.name = name; + } + + public int getID() { + return id; + } + + public String getName() { + return name; + } + } + + /** + * Enumerates the color temperature ranges of lights. + */ + public enum TemperatureRange { + /** + * When the temperature range is not defined for {@link Upgrade}s it is inherited from the most + * recent firmware version. + */ + NONE(0, 0), + + /** + * 1500-4000K + */ + TR_1500_4000(1500, 4000), + + /** + * 1500-6500K + */ + TR_1500_6500(1500, 6500), + + /** + * 1500-9000K + */ + TR_1500_9000(1500, 9000), + + /** + * 2000-2000K + */ + TR_2000_2000(2000, 2000), + + /** + * 2100-2100K + */ + TR_2100_2100(2100, 2100), + + /** + * 2200-6500K + */ + TR_2200_6500(2200, 6500), + + /** + * 2500-9000K + */ + TR_2500_9000(2500, 9000), + + /** + * 2700-2700K + */ + TR_2700_2700(2700, 2700), + + /** + * 2700-6500K + */ + TR_2700_6500(2700, 6500); + + private final int minimum; + private final int maximum; + + TemperatureRange(int minimum, int maximum) { + this.minimum = minimum; + this.maximum = maximum; + } + + /** + * The minimum color temperature in degrees Kelvin. + * + * @return minimum color temperature (K) + */ + public int getMinimum() { + return minimum; + } + + /** + * The maxiumum color temperature in degrees Kelvin. + * + * @return maximum color temperature (K) + */ + public int getMaximum() { + return maximum; + } + + /** + * The color temperature range in degrees Kelvin. + * + * @return difference between maximum and minimum color temperature values + */ + public int getRange() { + return maximum - minimum; + } + } + + public static class Features { + private TemperatureRange temperatureRange; + private Set features; + + private Features(Feature... features) { + this(NONE, features); + } + + private Features(Features other) { + this(other.temperatureRange, other.features); + } + + private Features(TemperatureRange temperatureRange, Feature... features) { + this.temperatureRange = temperatureRange; + this.features = Set.of(features); + } + + private Features(TemperatureRange temperatureRange, Set features) { + this.temperatureRange = temperatureRange; + this.features = Set.copyOf(features); + } + + public TemperatureRange getTemperatureRange() { + return temperatureRange; + } + + public boolean hasFeature(Feature feature) { + return features.contains(feature); + } + + public void update(Features other) { + temperatureRange = other.temperatureRange; + features = other.features; + } + } + + static class Upgrade { + final long major; + final long minor; + final Features features; + + private Upgrade(long major, long minor, Features features) { + this.major = major; + this.minor = minor; + this.features = features; + } + } + + private final Vendor vendor; + private final long id; + private final String name; + private final Features features; + private final List upgrades; + + private LifxProduct(long id, String name, Features features, Upgrade... upgrades) { + this(LIFX, id, name, features, upgrades); + } + + private LifxProduct(Vendor vendor, long id, String name, Features features, Upgrade... upgrades) { + this.vendor = vendor; + this.id = id; + this.name = name; + this.features = features; + this.upgrades = List.of(upgrades); + } + + @Override + public String toString() { + return name; + } + + public Vendor getVendor() { + return vendor; + } + + public long getID() { + return id; + } + + public String getName() { + return name; + } + + /** + * Returns the features of the initial product firmware version. + * + * @return the initial features + */ + public Features getFeatures() { + return new Features(features); + } + + /** + * Returns the features for a specific product firmware version. + * + * @param version the firmware version + * @return the composite features of all firmware upgrades for the given major and minor firmware version + */ + public Features getFeatures(String version) { + if (upgrades.isEmpty() || !version.contains(".")) { + return new Features(features); + } + + String[] majorMinorVersion = version.split("\\."); + long major = Long.valueOf(majorMinorVersion[0]); + long minor = Long.valueOf(majorMinorVersion[1]); + + TemperatureRange temperatureRange = features.temperatureRange; + Set features = new HashSet<>(this.features.features); + + for (Upgrade upgrade : upgrades) { + if (upgrade.major < major || (upgrade.major == major && upgrade.minor <= minor)) { + Features upgradeFeatures = upgrade.features; + if (upgradeFeatures.temperatureRange != NONE) { + temperatureRange = upgradeFeatures.temperatureRange; + } + features.addAll(upgradeFeatures.features); + } else { + break; + } + } + + return new Features(temperatureRange, features); + } + + public ThingTypeUID getThingTypeUID() { + if (hasFeature(COLOR)) { + if (hasFeature(TILE_EFFECT)) { + return LifxBindingConstants.THING_TYPE_TILELIGHT; + } else if (hasFeature(INFRARED)) { + return LifxBindingConstants.THING_TYPE_COLORIRLIGHT; + } else if (hasFeature(MULTIZONE)) { + return LifxBindingConstants.THING_TYPE_COLORMZLIGHT; + } else { + return LifxBindingConstants.THING_TYPE_COLORLIGHT; + } + } else { + return LifxBindingConstants.THING_TYPE_WHITELIGHT; + } + } + + private boolean hasFeature(Feature feature) { + return features.hasFeature(feature); + } + + /** + * Returns a product that has the given thing type UID. + * + * @param uid a thing type UID + * @return a product that has the given thing type UID + * @throws IllegalArgumentException when uid is not a valid LIFX thing type UID + */ + public static LifxProduct getLikelyProduct(ThingTypeUID uid) throws IllegalArgumentException { + for (LifxProduct product : LifxProduct.values()) { + if (product.getThingTypeUID().equals(uid)) { + return product; + } + } + + throw new IllegalArgumentException(uid + " is not a valid product thing type UID"); + } + + /** + * Returns the product that has the given product ID. + * + * @param id the product ID + * @return the product that has the given product ID + * @throws IllegalArgumentException when id is not a valid LIFX product ID + */ + public static LifxProduct getProductFromProductID(long id) throws IllegalArgumentException { + for (LifxProduct product : LifxProduct.values()) { + if (product.id == id) { + return product; + } + } + + throw new IllegalArgumentException(id + " is not a valid product ID"); + } + + List getUpgrades() { + return upgrades; + } + + public boolean isLight() { + return !features.hasFeature(Feature.BUTTONS) && !features.hasFeature(Feature.RELAYS); + } +} diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSelectorContext.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSelectorContext.java index 0cd6f671959a4..bc7f3ffd3a59c 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSelectorContext.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSelectorContext.java @@ -25,7 +25,7 @@ * The {@link LifxSelectorContext} stores the context that is used for broadcast and unicast communications with a * light using a {@link Selector}. * - * @author Wouter Born - Make selector logic reusable between discovery and handlers + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxSelectorContext { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSequenceNumberSupplier.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSequenceNumberSupplier.java index 5f9e10899153a..1d94e146389b5 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSequenceNumberSupplier.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxSequenceNumberSupplier.java @@ -20,7 +20,7 @@ /** * Supplies sequence numbers for packets in the range [0, 255]. * - * @author Wouter Born - Make selector logic reusable between discovery and handlers + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxSequenceNumberSupplier implements Supplier { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/AcknowledgementResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/AcknowledgementResponse.java similarity index 91% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/AcknowledgementResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/AcknowledgementResponse.java index 79eb57808d134..74ace861ab219 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/AcknowledgementResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/AcknowledgementResponse.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class AcknowledgementResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/ApplicationRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/ApplicationRequest.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/ApplicationRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/ApplicationRequest.java index 233cfde2bda09..9e53718a873b8 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/ApplicationRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/ApplicationRequest.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ public enum ApplicationRequest { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/EchoRequestResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/EchoRequestResponse.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/EchoRequestResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/EchoRequestResponse.java index 8ea752276bbea..058a76031a22b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/EchoRequestResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/EchoRequestResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class EchoRequestResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Effect.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Effect.java similarity index 98% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Effect.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Effect.java index 73f9d6437df6c..2490e22633585 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Effect.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Effect.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -20,7 +20,7 @@ /** * This class represents LIFX Tile effect * - * @author Pawel Pieczul - initial contribution + * @author Pawel Pieczul - Initial contribution */ @NonNullByDefault public class Effect { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericHandler.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericHandler.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericHandler.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericHandler.java index 13127f9fc3168..8447c2d27b2b4 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericHandler.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -27,7 +27,7 @@ * * @param the packet subtype this handler constructs * - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericPacket.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericPacket.java similarity index 90% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericPacket.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericPacket.java index 49667965ad1b2..b0a738054b470 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GenericPacket.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GenericPacket.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GenericPacket extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetColorZonesRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetColorZonesRequest.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetColorZonesRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetColorZonesRequest.java index c53515412db75..fbdc0ddec7c86 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetColorZonesRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetColorZonesRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import static org.openhab.binding.lifx.internal.LifxBindingConstants.*; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt8Field; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ public class GetColorZonesRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetEchoRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetEchoRequest.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetEchoRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetEchoRequest.java index 43b0fc262bff5..be09a903d5ab9 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetEchoRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetEchoRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetEchoRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetGroupRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetGroupRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetGroupRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetGroupRequest.java index 8bb6cf23e4c24..a6f8e134b994b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetGroupRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetGroupRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetGroupRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostFirmwareRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostFirmwareRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostFirmwareRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostFirmwareRequest.java index 9905cecec2553..814f8738e6437 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostFirmwareRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostFirmwareRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetHostFirmwareRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostInfoRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostInfoRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostInfoRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostInfoRequest.java index 290c934c42987..a2f1b5131e17b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetHostInfoRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetHostInfoRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetHostInfoRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetInfoRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetInfoRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetInfoRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetInfoRequest.java index 33e830f987083..47212e8648ec0 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetInfoRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetInfoRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetInfoRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLabelRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLabelRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLabelRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLabelRequest.java index 20d6b5a03b3d1..681ac98ad90f3 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLabelRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLabelRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetLabelRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightInfraredRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightInfraredRequest.java similarity index 89% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightInfraredRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightInfraredRequest.java index 1297026e87b4c..f4b2ce1d88b9e 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightInfraredRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightInfraredRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Wouter Born - Support LIFX 2016 product line-up and infrared functionality + * @author Wouter Born - Initial contribution */ public class GetLightInfraredRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightPowerRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightPowerRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightPowerRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightPowerRequest.java index d8853b7a44a21..2fe69366d0a94 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLightPowerRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLightPowerRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetLightPowerRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLocationRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLocationRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLocationRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLocationRequest.java index abf7b8337fcae..af75939bee782 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetLocationRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetLocationRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetLocationRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetPowerRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetPowerRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetPowerRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetPowerRequest.java index 6e09a2d79dfd6..2b9bfdddbcc01 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetPowerRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetPowerRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetPowerRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetRequest.java index 3f11ae49ecea7..ab64dc6d50f15 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetServiceRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetServiceRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetServiceRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetServiceRequest.java index 400a19bb58dec..b0b8d745df9b9 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetServiceRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetServiceRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetServiceRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagLabelsRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagLabelsRequest.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagLabelsRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagLabelsRequest.java index 39aeb592e01d0..c3c313e95667a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagLabelsRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagLabelsRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetTagLabelsRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagsRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagsRequest.java similarity index 91% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagsRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagsRequest.java index 1b76a2df2f5e7..c0570cca52912 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTagsRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTagsRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetTagsRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTileEffectRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTileEffectRequest.java similarity index 91% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTileEffectRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTileEffectRequest.java index afb4fa641f21e..fde9dcc647d1e 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetTileEffectRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetTileEffectRequest.java @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** * Implementation of GetTileEffect packet - * - * @author Pawel Pieczul - Add support for Tile Effects + * + * @author Pawel Pieczul - Initial contribution */ public class GetTileEffectRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetVersionRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetVersionRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetVersionRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetVersionRequest.java index 9e789f8793844..a278480581579 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetVersionRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetVersionRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetVersionRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiFirmwareRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiFirmwareRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiFirmwareRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiFirmwareRequest.java index ca18a4a15c23f..e08771de08f7c 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiFirmwareRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiFirmwareRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetWifiFirmwareRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiInfoRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiInfoRequest.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiInfoRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiInfoRequest.java index 5740ccea6faab..3b97981a1cce7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/GetWifiInfoRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/GetWifiInfoRequest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class GetWifiInfoRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Packet.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java similarity index 99% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Packet.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java index d9084ab866078..3b65afc370956 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Packet.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -36,7 +36,7 @@ * Field definitions should remain accessible to outside classes in the event * they need to worked with directly elsewhere. * - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public abstract class Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketFactory.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketFactory.java similarity index 98% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketFactory.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketFactory.java index 6718d31f3c040..ba9ea39f0f4c3 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketFactory.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketFactory.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.util.HashMap; import java.util.Map; @@ -24,7 +24,7 @@ * e. Packet handlers (used to construct actual packet * instances) may be retrieved via their packet type. * - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification * @author Wouter Born - Support LIFX 2016 product line-up and infrared functionality */ diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketHandler.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketHandler.java similarity index 92% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketHandler.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketHandler.java index fe8944eb2c8e5..a93b4759be9cb 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PacketHandler.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PacketHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -22,7 +22,7 @@ * * @param the generic packet type * - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PowerState.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PowerState.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PowerState.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PowerState.java index 69d6ad11fd5b5..f12ba58b7d9b3 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/PowerState.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/PowerState.java @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import org.openhab.core.library.types.OnOffType; /** * Represents light power states (on or off). * - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification * @author Wouter Born - Added OnOffType conversion methods */ diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorRequest.java similarity index 96% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorRequest.java index 3a24126e6b83f..2ebe1760fe7a2 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -21,7 +21,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetColorRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorZonesRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorZonesRequest.java similarity index 96% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorZonesRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorZonesRequest.java index 5553877994221..5e40d1b06a8e2 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetColorZonesRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetColorZonesRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import static org.openhab.binding.lifx.internal.LifxBindingConstants.*; @@ -23,7 +23,7 @@ import org.openhab.binding.lifx.internal.fields.UInt8Field; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ public class SetColorZonesRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetDimAbsoluteRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetDimAbsoluteRequest.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetDimAbsoluteRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetDimAbsoluteRequest.java index 648db5845c7d2..ca92dedca713b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetDimAbsoluteRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetDimAbsoluteRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -19,7 +19,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetDimAbsoluteRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLabelRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLabelRequest.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLabelRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLabelRequest.java index e55dbe5ab60b7..ee11b747cb658 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLabelRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLabelRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.StringField; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetLabelRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightInfraredRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightInfraredRequest.java similarity index 91% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightInfraredRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightInfraredRequest.java index e9d601cf321f1..1a0abda764716 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightInfraredRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightInfraredRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt16Field; /** - * @author Wouter Born - Support LIFX 2016 product line-up and infrared functionality + * @author Wouter Born - Initial contribution */ public class SetLightInfraredRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightPowerRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightPowerRequest.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightPowerRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightPowerRequest.java index ed186fd097e43..ecbfb7a1a7f26 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetLightPowerRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetLightPowerRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -19,7 +19,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetLightPowerRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetPowerRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetPowerRequest.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetPowerRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetPowerRequest.java index 8b0d61a30b14d..ff4988b08b645 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetPowerRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetPowerRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt16Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetPowerRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTagsRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTagsRequest.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTagsRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTagsRequest.java index 384ae11df8811..abbf5b58ddfe2 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTagsRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTagsRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class SetTagsRequest extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTileEffectRequest.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTileEffectRequest.java similarity index 99% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTileEffectRequest.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTileEffectRequest.java index f4cef593ae922..21d398e495514 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SetTileEffectRequest.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SetTileEffectRequest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SignalStrength.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SignalStrength.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SignalStrength.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SignalStrength.java index 5a10b8a2da399..716a93f94014e 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/SignalStrength.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/SignalStrength.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; /** * The signal strength of a light. * - * @author Wouter Born - Add signal strength channel + * @author Wouter Born - Initial contribution */ public class SignalStrength { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateGroupResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateGroupResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateGroupResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateGroupResponse.java index 274a9c510a082..2463edacf9bcb 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateGroupResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateGroupResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateGroupResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostFirmwareResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostFirmwareResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostFirmwareResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostFirmwareResponse.java index 4eb17305a73a5..065c06595f766 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostFirmwareResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostFirmwareResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.VersionField; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateHostFirmwareResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostInfoResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostInfoResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostInfoResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostInfoResponse.java index 5b4b712806292..1690bd235183b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateHostInfoResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateHostInfoResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateHostInfoResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateInfoResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateInfoResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateInfoResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateInfoResponse.java index 5b7e9143303c1..ca3ccf276d657 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateInfoResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateInfoResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateInfoResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLabelResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLabelResponse.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLabelResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLabelResponse.java index c41afa3184ea4..409d61f40024a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLabelResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLabelResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.StringField; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateLabelResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightInfraredResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightInfraredResponse.java similarity index 90% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightInfraredResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightInfraredResponse.java index 9012583da0674..53d561be493ff 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightInfraredResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightInfraredResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt16Field; /** - * @author Wouter Born - Support LIFX 2016 product line-up and infrared functionality + * @author Wouter Born - Initial contribution */ public class StateLightInfraredResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightPowerResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightPowerResponse.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightPowerResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightPowerResponse.java index 6b848afb6caa8..9d248a16ee816 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLightPowerResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLightPowerResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt16Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateLightPowerResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLocationResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLocationResponse.java similarity index 96% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLocationResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLocationResponse.java index 35d726aad2bc1..77c014688996b 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateLocationResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateLocationResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateLocationResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateMultiZoneResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateMultiZoneResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateMultiZoneResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateMultiZoneResponse.java index 47f167deb0f11..4e2443bdd2dca 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateMultiZoneResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateMultiZoneResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt8Field; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ public class StateMultiZoneResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StatePowerResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StatePowerResponse.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StatePowerResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StatePowerResponse.java index b6689a6429901..7d86030cfcde5 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StatePowerResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StatePowerResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt16Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StatePowerResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateResponse.java similarity index 96% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateResponse.java index ff3bf40bf2bd8..808577b376f3c 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -22,7 +22,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateServiceResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateServiceResponse.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateServiceResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateServiceResponse.java index 84c6a23826e0a..599da30b7098d 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateServiceResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateServiceResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt8Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateServiceResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateTileEffectResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateTileEffectResponse.java similarity index 98% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateTileEffectResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateTileEffectResponse.java index 6bfb630f595f6..c9b6f40e9d6d3 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateTileEffectResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateTileEffectResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -26,7 +26,7 @@ /** * Implementation of StateTileEffect packet * - * @author Pawel Pieczul - Initial Contribution + * @author Pawel Pieczul - Initial contribution */ public class StateTileEffectResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateVersionResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateVersionResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateVersionResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateVersionResponse.java index 410dc3720b1b7..db0b14e433493 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateVersionResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateVersionResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateVersionResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiFirmwareResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiFirmwareResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiFirmwareResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiFirmwareResponse.java index b0cbc950106b0..6cf177f0e961f 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiFirmwareResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiFirmwareResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.VersionField; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateWifiFirmwareResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiInfoResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiInfoResponse.java similarity index 95% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiInfoResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiInfoResponse.java index 3cb832b1538c3..516562d2dae22 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateWifiInfoResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateWifiInfoResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt32Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class StateWifiInfoResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateZoneResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateZoneResponse.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateZoneResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateZoneResponse.java index e95135fc8e54d..26eaf2824635a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/StateZoneResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/StateZoneResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import org.openhab.binding.lifx.internal.fields.UInt8Field; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ public class StateZoneResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagLabelsResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagLabelsResponse.java similarity index 94% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagLabelsResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagLabelsResponse.java index ef3aa17676f72..5ecd16b0c7d45 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagLabelsResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagLabelsResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -19,7 +19,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class TagLabelsResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagsResponse.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagsResponse.java similarity index 93% rename from bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagsResponse.java rename to bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagsResponse.java index 257f28eab9444..104ad08b26cae 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/TagsResponse.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/TagsResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.lifx.internal.protocol; +package org.openhab.binding.lifx.internal.dto; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ import org.openhab.binding.lifx.internal.fields.UInt64Field; /** - * @author Tim Buckley - Initial Contribution + * @author Tim Buckley - Initial contribution * @author Karel Goderis - Enhancement for the V2 LIFX Firmware and LAN Protocol Specification */ public class TagsResponse extends Packet { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/ByteField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/ByteField.java index 29418f3263a4e..5649a32d81e6f 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/ByteField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/ByteField.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class ByteField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Field.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Field.java index e2bcc7d9e6407..1a1ae2c92c49a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Field.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Field.java @@ -22,7 +22,7 @@ * * @param the field datatype * - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public abstract class Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/FloatField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/FloatField.java index 7e29fd5819025..ffb7bd40f09d8 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/FloatField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/FloatField.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class FloatField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java index e31629cbfa158..23885500c6d43 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java @@ -21,7 +21,7 @@ import org.openhab.core.library.types.PercentType; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class HSBK { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBKField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBKField.java index c254f4bdb7919..75d1e167019ef 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBKField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBKField.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Wouter Born - Add support for MultiZone light control + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class HSBKField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/LittleField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/LittleField.java index 8dfcb257f42ec..5352491dfec37 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/LittleField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/LittleField.java @@ -19,7 +19,7 @@ /** * Reads a wrapped field in reversed byte order. * - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class LittleField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java index bfce15f801418..91190f4242b42 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java @@ -12,30 +12,23 @@ */ package org.openhab.binding.lifx.internal.fields; -import java.io.ByteArrayInputStream; -import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Objects; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.openhab.core.util.HexUtils; /** - * @author Tim Buckley - * @author Karel Goderis + * @author Tim Buckley - Initial contribution + * @author Karel Goderis - Initial contribution */ @NonNullByDefault public class MACAddress { - public static final MACAddress BROADCAST_ADDRESS = new MACAddress("000000000000", true); - - private final Logger logger = LoggerFactory.getLogger(MACAddress.class); + public static final MACAddress BROADCAST_ADDRESS = new MACAddress("000000000000"); private ByteBuffer bytes; private String hex = ""; @@ -54,28 +47,10 @@ public MACAddress(ByteBuffer bytes) { createHex(); } - public MACAddress(String string, boolean isHex) { - if (!isHex) { - this.bytes = ByteBuffer.wrap(string.getBytes()); - createHex(); - } else { - this.bytes = ByteBuffer.wrap(parseHexBinary(string)); - - try { - formatHex(string, 2, ":"); - } catch (IOException e) { - logger.error("An exception occurred while formatting an HEX string : '{}'", e.getMessage()); - } - } - } - - private byte[] parseHexBinary(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); - } - return data; + public MACAddress(String string) { + byte[] byteArray = HexUtils.hexToBytes(string); + this.bytes = ByteBuffer.wrap(byteArray); + this.hex = HexUtils.bytesToHex(byteArray, ":"); } public MACAddress() { @@ -90,7 +65,7 @@ private void createHex() { byteStrings.add(String.format("%02X", bytes.get())); } - hex = StringUtils.join(byteStrings, ':'); + hex = String.join(":", byteStrings); bytes.rewind(); } @@ -108,21 +83,6 @@ public String getAsLabel() { return hex.toString(); } - private void formatHex(String original, int length, String separator) throws IOException { - ByteArrayInputStream bis = new ByteArrayInputStream(original.getBytes()); - byte[] buffer = new byte[length]; - String result = ""; - while (bis.read(buffer) > 0) { - for (byte b : buffer) { - result += (char) b; - } - Arrays.fill(buffer, (byte) 0); - result += separator; - } - - hex = StringUtils.left(result, result.length() - 1); - } - @Override public int hashCode() { int hash = 7; diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddressField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddressField.java index 417348cb71e05..57471120c0279 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddressField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddressField.java @@ -17,8 +17,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley - * @author Karel Goderis + * @author Tim Buckley - Initial contribution + * @author Karel Goderis - Initial contribution */ @NonNullByDefault public class MACAddressField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/StringField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/StringField.java index 360ceb2816126..0184d65c09ee0 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/StringField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/StringField.java @@ -19,7 +19,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class StringField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt16Field.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt16Field.java index 3cdcb44c28bbe..a9c65d2e53008 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt16Field.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt16Field.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class UInt16Field extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt32Field.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt32Field.java index 04b9d7acbee7b..89ce6fb8828e7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt32Field.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt32Field.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class UInt32Field extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt64Field.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt64Field.java index bfc229a4dd354..b72ef6b2db748 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt64Field.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt64Field.java @@ -21,7 +21,7 @@ * unexpected values will likely be shown if exposed to users. Most bit-level * operations should still work (addition, multiplication, shifting, etc). * - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class UInt64Field extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt8Field.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt8Field.java index 23ba2353bde72..969c468061fe6 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt8Field.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/UInt8Field.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Tim Buckley + * @author Tim Buckley - Initial contribution */ @NonNullByDefault public class UInt8Field extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Version.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Version.java index 559bae30ecac8..3d53df7703c92 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Version.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/Version.java @@ -15,7 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Wouter Born - Add Thing properties + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class Version { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/VersionField.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/VersionField.java index 7cec7a8e96add..26af5382c6a61 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/VersionField.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/VersionField.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * @author Wouter Born - Add Thing properties + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class VersionField extends Field { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java index af941d2f96692..fd0684298003c 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java @@ -13,7 +13,7 @@ package org.openhab.binding.lifx.internal.handler; import static org.openhab.binding.lifx.internal.LifxBindingConstants.*; -import static org.openhab.binding.lifx.internal.protocol.Product.Feature.*; +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.*; import java.net.InetSocketAddress; @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -38,18 +39,19 @@ import org.openhab.binding.lifx.internal.LifxLightPropertiesUpdater; import org.openhab.binding.lifx.internal.LifxLightState; import org.openhab.binding.lifx.internal.LifxLightStateChanger; +import org.openhab.binding.lifx.internal.LifxProduct; +import org.openhab.binding.lifx.internal.LifxProduct.Features; +import org.openhab.binding.lifx.internal.dto.Effect; +import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest; +import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest; +import org.openhab.binding.lifx.internal.dto.GetRequest; +import org.openhab.binding.lifx.internal.dto.GetTileEffectRequest; +import org.openhab.binding.lifx.internal.dto.GetWifiInfoRequest; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.PowerState; +import org.openhab.binding.lifx.internal.dto.SignalStrength; import org.openhab.binding.lifx.internal.fields.HSBK; import org.openhab.binding.lifx.internal.fields.MACAddress; -import org.openhab.binding.lifx.internal.protocol.Effect; -import org.openhab.binding.lifx.internal.protocol.GetLightInfraredRequest; -import org.openhab.binding.lifx.internal.protocol.GetLightPowerRequest; -import org.openhab.binding.lifx.internal.protocol.GetRequest; -import org.openhab.binding.lifx.internal.protocol.GetTileEffectRequest; -import org.openhab.binding.lifx.internal.protocol.GetWifiInfoRequest; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.PowerState; -import org.openhab.binding.lifx.internal.protocol.Product; -import org.openhab.binding.lifx.internal.protocol.SignalStrength; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; @@ -90,7 +92,7 @@ public class LifxLightHandler extends BaseThingHandler { private static final Duration MAX_STATE_CHANGE_DURATION = Duration.ofSeconds(4); private final LifxChannelFactory channelFactory; - private @NonNullByDefault({}) Product product; + private @NonNullByDefault({}) Features features; private @Nullable PercentType powerOnBrightness; private @Nullable HSBType powerOnColor; @@ -175,7 +177,7 @@ private void updateColorChannels(@Nullable PowerState powerState, HSBK[] colors) updateStateIfChanged(CHANNEL_COLOR, hsb); updateStateIfChanged(CHANNEL_BRIGHTNESS, hsb.getBrightness()); updateStateIfChanged(CHANNEL_TEMPERATURE, - kelvinToPercentType(updateColor.getKelvin(), product.getTemperatureRange())); + kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange())); updateZoneChannels(powerState, colors); } @@ -210,7 +212,7 @@ public void setTileEffect(Effect effect) { } private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) { - if (!product.hasFeature(MULTIZONE) || colors.length == 0) { + if (!features.hasFeature(MULTIZONE) || colors.length == 0) { return; } @@ -225,7 +227,7 @@ private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) HSBK updateColor = nullSafeUpdateColor(powerState, color); updateStateIfChanged(CHANNEL_COLOR_ZONE + i, updateColor.getHSB()); updateStateIfChanged(CHANNEL_TEMPERATURE_ZONE + i, - kelvinToPercentType(updateColor.getKelvin(), product.getTemperatureRange())); + kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange())); } } } @@ -243,9 +245,12 @@ public void initialize() { LifxLightConfig configuration = getConfigAs(LifxLightConfig.class); logId = getLogId(configuration.getMACAddress(), configuration.getHost()); - product = getProduct(); - logger.debug("{} : Initializing handler for product {}", logId, product.getName()); + if (logger.isDebugEnabled()) { + logger.debug("{} : Initializing handler for product {}", logId, getProduct().getName()); + } + + features = getFeatures(); powerOnBrightness = getPowerOnBrightness(); powerOnColor = getPowerOnColor(); @@ -262,7 +267,7 @@ public void initialize() { currentLightState = new CurrentLightState(); pendingLightState = new LifxLightState(); - LifxLightContext context = new LifxLightContext(logId, product, configuration, currentLightState, + LifxLightContext context = new LifxLightContext(logId, features, configuration, currentLightState, pendingLightState, scheduler); communicationHandler = new LifxLightCommunicationHandler(context); @@ -338,7 +343,7 @@ public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddr private @Nullable PercentType getPowerOnBrightness() { Channel channel = null; - if (product.hasFeature(COLOR)) { + if (features.hasFeature(COLOR)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR); channel = getThing().getChannel(channelUID.getId()); } else { @@ -358,7 +363,7 @@ public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddr private @Nullable HSBType getPowerOnColor() { Channel channel = null; - if (product.hasFeature(COLOR)) { + if (features.hasFeature(COLOR)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR); channel = getThing().getChannel(channelUID.getId()); } @@ -391,7 +396,7 @@ public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddr private @Nullable Double getEffectSpeed(String parameter) { Channel channel = null; - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_EFFECT); channel = getThing().getChannel(channelUID.getId()); } @@ -402,23 +407,36 @@ public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddr Configuration configuration = channel.getConfiguration(); Object speed = configuration.get(parameter); - return speed == null ? null : new Double(speed.toString()); + return speed == null ? null : Double.valueOf(speed.toString()); } - private Product getProduct() { - Object propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID); + private Features getFeatures() { + LifxProduct product = getProduct(); + + String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); if (propertyValue == null) { - return Product.getLikelyProduct(getThing().getThingTypeUID()); + logger.debug("{} : Using features of initial firmware version", logId); + return product.getFeatures(); + } + + logger.debug("{} : Using features of firmware version {}", logId, propertyValue); + return product.getFeatures(propertyValue); + } + + private LifxProduct getProduct() { + String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID); + if (propertyValue == null) { + return LifxProduct.getLikelyProduct(getThing().getThingTypeUID()); } try { // Without first conversion to double, on a very first thing creation from discovery inbox, // the product type is incorrectly parsed, as framework passed it as a floating point number // (e.g. 50.0 instead of 50) - Double d = Double.parseDouble((String) propertyValue); + Double d = Double.valueOf(propertyValue); long productID = d.longValue(); - return Product.getProductFromProductID(productID); + return LifxProduct.getProductFromProductID(productID); } catch (IllegalArgumentException e) { - return Product.getLikelyProduct(getThing().getThingTypeUID()); + return LifxProduct.getLikelyProduct(getThing().getThingTypeUID()); } } @@ -485,7 +503,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { sendPacket(new GetWifiInfoRequest()); break; case CHANNEL_EFFECT: - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { sendPacket(new GetTileEffectRequest()); } break; @@ -538,7 +556,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } break; case CHANNEL_EFFECT: - if (command instanceof StringType && product.hasFeature(TILE_EFFECT)) { + if (command instanceof StringType && features.hasFeature(TILE_EFFECT)) { handleTileEffectCommand((StringType) command); } else { supportedCommand = false; @@ -597,14 +615,14 @@ private boolean isStateChangePending() { private void handleTemperatureCommand(PercentType temperature) { HSBK newColor = getLightStateForCommand().getColor(); newColor.setSaturation(PercentType.ZERO); - newColor.setKelvin(percentTypeToKelvin(temperature, product.getTemperatureRange())); + newColor.setKelvin(percentTypeToKelvin(temperature, features.getTemperatureRange())); getLightStateForCommand().setColor(newColor); } private void handleTemperatureCommand(PercentType temperature, int zoneIndex) { HSBK newColor = getLightStateForCommand().getColor(zoneIndex); newColor.setSaturation(PercentType.ZERO); - newColor.setKelvin(percentTypeToKelvin(temperature, product.getTemperatureRange())); + newColor.setKelvin(percentTypeToKelvin(temperature, features.getTemperatureRange())); getLightStateForCommand().setColor(newColor, zoneIndex); } @@ -633,7 +651,7 @@ private void handleOnOffCommand(OnOffType onOff) { PercentType localPowerOnTemperature = powerOnTemperature; if (localPowerOnTemperature != null && onOff == OnOffType.ON) { getLightStateForCommand() - .setTemperature(percentTypeToKelvin(localPowerOnTemperature, product.getTemperatureRange())); + .setTemperature(percentTypeToKelvin(localPowerOnTemperature, features.getTemperatureRange())); } PercentType powerOnBrightness = this.powerOnBrightness; @@ -658,14 +676,14 @@ private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease) { PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor().getKelvin(), - product.getTemperatureRange()); + features.getTemperatureRange()); PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature); handleTemperatureCommand(newTemperature); } private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) { PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor(zoneIndex).getKelvin(), - product.getTemperatureRange()); + features.getTemperatureRange()); PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature); handleTemperatureCommand(newTemperature, zoneIndex); } @@ -691,7 +709,18 @@ private void handleTileEffectCommand(StringType type) { flameSpeedInMSecs.longValue()); getLightStateForCommand().setTileEffect(effect); } catch (IllegalArgumentException e) { - logger.debug("Wrong effect type received as command: {}", type); + logger.debug("{} : Wrong effect type received as command: {}", logId, type); + } + } + + @Override + protected void updateProperties(Map properties) { + String oldHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); + super.updateProperties(properties); + String newHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); + + if (!Objects.equals(oldHostVersion, newHostVersion)) { + features.update(getFeatures()); } } diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxLightStateListener.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxLightStateListener.java index 68b7c460aaf7c..fab89fa7f1984 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxLightStateListener.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxLightStateListener.java @@ -15,10 +15,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lifx.internal.LifxLightState; +import org.openhab.binding.lifx.internal.dto.Effect; +import org.openhab.binding.lifx.internal.dto.PowerState; +import org.openhab.binding.lifx.internal.dto.SignalStrength; import org.openhab.binding.lifx.internal.fields.HSBK; -import org.openhab.binding.lifx.internal.protocol.Effect; -import org.openhab.binding.lifx.internal.protocol.PowerState; -import org.openhab.binding.lifx.internal.protocol.SignalStrength; import org.openhab.core.library.types.PercentType; /** diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxPropertiesUpdateListener.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxPropertiesUpdateListener.java index 8909ee73bf356..c4dedcdea3e0a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxPropertiesUpdateListener.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxPropertiesUpdateListener.java @@ -21,7 +21,7 @@ * The {@link LifxPropertiesUpdateListener} is notified when the {@link LifxLightPropertiesUpdater} has * updated light properties. * - * @author Wouter Born - Update light properties when online + * @author Wouter Born - Initial contribution */ @NonNullByDefault public interface LifxPropertiesUpdateListener { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxResponsePacketListener.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxResponsePacketListener.java index fa622f6e64b22..10396ac5e8964 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxResponsePacketListener.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/listener/LifxResponsePacketListener.java @@ -14,7 +14,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lifx.internal.LifxLightCommunicationHandler; -import org.openhab.binding.lifx.internal.protocol.Packet; +import org.openhab.binding.lifx.internal.dto.Packet; /** * The {@link LifxResponsePacketListener} is notified when the {@link LifxLightCommunicationHandler} receives a response diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Product.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Product.java deleted file mode 100644 index cee8e8d04dd79..0000000000000 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/protocol/Product.java +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.lifx.internal.protocol; - -import static org.openhab.binding.lifx.internal.protocol.Product.Feature.*; -import static org.openhab.binding.lifx.internal.protocol.Product.TemperatureRange.*; -import static org.openhab.binding.lifx.internal.protocol.Product.Vendor.LIFX; - -import java.util.Arrays; -import java.util.EnumSet; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.lifx.internal.LifxBindingConstants; -import org.openhab.core.thing.ThingTypeUID; - -/** - * Enumerates the LIFX products, their IDs and feature set. - * - * @see https://lan.developer.lifx.com/docs/lifx-products - * - * @author Wouter Born - Support LIFX 2016 product line-up and infrared functionality - * @author Wouter Born - Add temperature ranges and simplify feature definitions - */ -@NonNullByDefault -public enum Product { - - PRODUCT_1(1, "LIFX Original 1000", TR_2500_9000, COLOR), - PRODUCT_3(3, "LIFX Color 650", TR_2500_9000, COLOR), - PRODUCT_10(10, "LIFX White 800 (Low Voltage)", TR_2700_6500), - PRODUCT_11(11, "LIFX White 800 (High Voltage)", TR_2700_6500), - PRODUCT_15(15, "LIFX Color 1000", TR_2500_9000, COLOR), - PRODUCT_18(18, "LIFX White 900 BR30 (Low Voltage)", TR_2700_6500), - PRODUCT_19(19, "LIFX White 900 BR30 (High Voltage)", TR_2700_6500), - PRODUCT_20(20, "LIFX Color 1000 BR30", TR_2500_9000, COLOR), - PRODUCT_22(22, "LIFX Color 1000", TR_2500_9000, COLOR), - PRODUCT_27(27, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_28(28, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_29(29, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_30(30, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_31(31, "LIFX Z", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_32(32, "LIFX Z", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_36(36, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_37(37, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_38(38, "LIFX Beam", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_39(39, "LIFX Downlight White to Warm", TR_1500_9000), - PRODUCT_40(40, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_43(43, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_44(44, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_45(45, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_46(46, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_49(49, "LIFX Mini Color", TR_2500_9000, COLOR), - PRODUCT_50(50, "LIFX Mini White to Warm", TR_1500_4000), - PRODUCT_51(51, "LIFX Mini White", TR_2700_2700), - PRODUCT_52(52, "LIFX GU10", TR_2500_9000, COLOR), - PRODUCT_53(53, "LIFX GU10", TR_2500_9000, COLOR), - PRODUCT_55(55, "LIFX Tile", TR_2500_9000, CHAIN, COLOR, MATRIX, TILE_EFFECT), - PRODUCT_57(57, "LIFX Candle", TR_1500_9000, COLOR, MATRIX), - PRODUCT_59(59, "LIFX Mini Color", TR_2500_9000, COLOR), - PRODUCT_60(60, "LIFX Mini White to Warm", TR_1500_4000), - PRODUCT_61(61, "LIFX Mini White", TR_2700_2700), - PRODUCT_62(62, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_63(63, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_64(64, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_65(65, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_66(66, "LIFX Mini White", TR_2700_2700), - PRODUCT_68(68, "LIFX Candle", TR_1500_9000, MATRIX), - PRODUCT_81(81, "LIFX Candle White to Warm", TR_2200_6500), - PRODUCT_82(82, "LIFX Filament Clear", TR_2100_2100), - PRODUCT_85(85, "LIFX Filament Amber", TR_2000_2000), - PRODUCT_87(87, "LIFX Mini White", TR_2700_2700), - PRODUCT_88(88, "LIFX Mini White", TR_2700_2700), - PRODUCT_90(90, "LIFX Clean", TR_2500_9000, HEV), - PRODUCT_91(91, "LIFX Color", TR_2500_9000, COLOR), - PRODUCT_92(92, "LIFX Color", TR_2500_9000, COLOR), - PRODUCT_94(94, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_96(96, "LIFX Candle White to Warm", TR_2200_6500), - PRODUCT_97(97, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_98(98, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_99(99, "LIFX Clean", TR_2500_9000, HEV), - PRODUCT_100(100, "LIFX Filament Clear", TR_2100_2100), - PRODUCT_101(101, "LIFX Filament Amber", TR_2000_2000), - PRODUCT_109(109, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_110(110, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_111(111, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED); - - /** - * Enumerates the product features. - */ - public enum Feature { - CHAIN, - COLOR, - HEV, - INFRARED, - MATRIX, - MULTIZONE, - TILE_EFFECT - } - - /** - * Enumerates the product vendors. - */ - public enum Vendor { - LIFX(1, "LIFX"); - - private final int id; - private final String name; - - Vendor(int id, String name) { - this.id = id; - this.name = name; - } - - public int getID() { - return id; - } - - public String getName() { - return name; - } - } - - /** - * Enumerates the color temperature ranges of lights. - */ - public enum TemperatureRange { - /** - * 1500-4000K - */ - TR_1500_4000(1500, 4000), - - /** - * 1500-9000K - */ - TR_1500_9000(1500, 9000), - - /** - * 2000-2000K - */ - TR_2000_2000(2000, 2000), - - /** - * 2100-2100K - */ - TR_2100_2100(2100, 2100), - - /** - * 2200-6500K - */ - TR_2200_6500(2200, 6500), - - /** - * 2500-9000K - */ - TR_2500_9000(2500, 9000), - - /** - * 2700-2700K - */ - TR_2700_2700(2700, 2700), - - /** - * 2700-6500K - */ - TR_2700_6500(2700, 6500); - - private final int minimum; - private final int maximum; - - TemperatureRange(int minimum, int maximum) { - this.minimum = minimum; - this.maximum = maximum; - } - - /** - * The minimum color temperature in degrees Kelvin. - * - * @return minimum color temperature (K) - */ - public int getMinimum() { - return minimum; - } - - /** - * The maxiumum color temperature in degrees Kelvin. - * - * @return maximum color temperature (K) - */ - public int getMaximum() { - return maximum; - } - - /** - * The color temperature range in degrees Kelvin. - * - * @return difference between maximum and minimum color temperature values - */ - public int getRange() { - return maximum - minimum; - } - } - - private final Vendor vendor; - private final long id; - private final String name; - private final TemperatureRange temperatureRange; - private final EnumSet features = EnumSet.noneOf(Feature.class); - - private Product(long id, String name, TemperatureRange temperatureRange) { - this(LIFX, id, name, temperatureRange); - } - - private Product(long id, String name, TemperatureRange temperatureRange, Feature... features) { - this(LIFX, id, name, temperatureRange, features); - } - - private Product(Vendor vendor, long id, String name, TemperatureRange temperatureRange) { - this(vendor, id, name, temperatureRange, new Feature[0]); - } - - private Product(Vendor vendor, long id, String name, TemperatureRange temperatureRange, Feature... features) { - this.vendor = vendor; - this.id = id; - this.name = name; - this.temperatureRange = temperatureRange; - this.features.addAll(Arrays.asList(features)); - } - - @Override - public String toString() { - return name; - } - - public Vendor getVendor() { - return vendor; - } - - public long getID() { - return id; - } - - public String getName() { - return name; - } - - public TemperatureRange getTemperatureRange() { - return temperatureRange; - } - - public ThingTypeUID getThingTypeUID() { - if (hasFeature(COLOR)) { - if (hasFeature(TILE_EFFECT)) { - return LifxBindingConstants.THING_TYPE_TILELIGHT; - } else if (hasFeature(INFRARED)) { - return LifxBindingConstants.THING_TYPE_COLORIRLIGHT; - } else if (hasFeature(MULTIZONE)) { - return LifxBindingConstants.THING_TYPE_COLORMZLIGHT; - } else { - return LifxBindingConstants.THING_TYPE_COLORLIGHT; - } - } else { - return LifxBindingConstants.THING_TYPE_WHITELIGHT; - } - } - - public boolean hasFeature(Feature feature) { - return features.contains(feature); - } - - /** - * Returns a product that has the given thing type UID. - * - * @param uid a thing type UID - * @return a product that has the given thing type UID - * @throws IllegalArgumentException when uid is not a valid LIFX thing type UID - */ - public static Product getLikelyProduct(ThingTypeUID uid) throws IllegalArgumentException { - for (Product product : Product.values()) { - if (product.getThingTypeUID().equals(uid)) { - return product; - } - } - - throw new IllegalArgumentException(uid + " is not a valid product thing type UID"); - } - - /** - * Returns the product that has the given product ID. - * - * @param id the product ID - * @return the product that has the given product ID - * @throws IllegalArgumentException when id is not a valid LIFX product ID - */ - public static Product getProductFromProductID(long id) throws IllegalArgumentException { - for (Product product : Product.values()) { - if (product.id == id) { - return product; - } - } - - throw new IllegalArgumentException(id + " is not a valid product ID"); - } -} diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxMessageUtil.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxMessageUtil.java index 7218a29f3dcc5..03c8135e2d7f3 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxMessageUtil.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxMessageUtil.java @@ -17,8 +17,8 @@ import java.util.UUID; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lifx.internal.LifxProduct.TemperatureRange; import org.openhab.binding.lifx.internal.fields.HSBK; -import org.openhab.binding.lifx.internal.protocol.Product.TemperatureRange; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.PercentType; @@ -26,7 +26,7 @@ /** * Utility class for sharing message utility methods between objects. * - * @author Wouter Born - Extracted methods from LifxLightHandler + * @author Wouter Born - Initial contribution */ @NonNullByDefault public final class LifxMessageUtil { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxNetworkUtil.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxNetworkUtil.java index 4d4774801447a..eca9a34a17efa 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxNetworkUtil.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxNetworkUtil.java @@ -34,7 +34,7 @@ * The {@link LifxNetworkUtil} provides network interface information to the LIFX binding objects. The information is * updated when it is older than {@link #UPDATE_INTERVAL_MILLIS}. * - * @author Wouter Born - Periodically update available interface information + * @author Wouter Born - Initial contribution */ @NonNullByDefault public final class LifxNetworkUtil { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxSelectorUtil.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxSelectorUtil.java index 10dec21b90a22..1630ec296acd5 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxSelectorUtil.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxSelectorUtil.java @@ -32,17 +32,17 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lifx.internal.LifxSelectorContext; +import org.openhab.binding.lifx.internal.dto.Packet; +import org.openhab.binding.lifx.internal.dto.PacketFactory; +import org.openhab.binding.lifx.internal.dto.PacketHandler; import org.openhab.binding.lifx.internal.fields.MACAddress; -import org.openhab.binding.lifx.internal.protocol.Packet; -import org.openhab.binding.lifx.internal.protocol.PacketFactory; -import org.openhab.binding.lifx.internal.protocol.PacketHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for sharing {@link Selector} logic between objects. * - * @author Wouter Born - Make selector logic reusable between discovery and handlers + * @author Wouter Born - Initial contribution */ @NonNullByDefault public class LifxSelectorUtil { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java index 397df9e07ea02..8001d1e74b5e7 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java @@ -31,7 +31,7 @@ * sent to LIFX lights. The LIFX LAN Protocol Specification states that lights can process up to 20 messages per second, * not more. * - * @author Karel Goderis - Initial Contribution + * @author Karel Goderis - Initial contribution * @author Wouter Born - Deadlock fix */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java new file mode 100644 index 0000000000000..d0547ed6709fb --- /dev/null +++ b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2021 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.lifx.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; +import static org.openhab.binding.lifx.internal.LifxProduct.TemperatureRange.*; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.lifx.internal.LifxProduct.Features; +import org.openhab.binding.lifx.internal.LifxProduct.Upgrade; + +/** + * Tests {@link LifxProduct}. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class LifxProductTest { + + @Test + public void productIDsAreUnique() { + Set productIDs = new HashSet<>(); + for (LifxProduct product : LifxProduct.values()) { + assertThat(productIDs, not(hasItem(product.getID()))); + productIDs.add(product.getID()); + } + } + + @Test + public void productNamesMatchProductIDs() { + for (LifxProduct product : LifxProduct.values()) { + assertThat(product.name(), is("PRODUCT_" + product.getID())); + } + } + + @Test + public void lightsHaveDefinedTemperatureRange() { + for (LifxProduct product : LifxProduct.values()) { + if (product.isLight()) { + String reason = String.format("The %s light does not define a temperature range", product.name()); + assertThat(reason, product.getFeatures().getTemperatureRange(), is(not(NONE))); + } + } + } + + @Test + public void upgradesSortedByMajorMinor() { + for (LifxProduct product : LifxProduct.values()) { + long major = 0; + long minor = 0; + for (Upgrade upgrade : product.getUpgrades()) { + String reason = String.format("Upgrades for %s are not sorted by major minor (%s.%s >= %s.%s)", + product.name(), major, minor, upgrade.major, upgrade.minor); + assertThat(reason, major < upgrade.major || (major == upgrade.major && minor < upgrade.minor), + is(true)); + major = upgrade.major; + minor = upgrade.minor; + } + } + } + + @Test + public void getFeaturesForProductWithoutUpgrades() { + LifxProduct product = LifxProduct.PRODUCT_1; + assertThat(product.getUpgrades(), hasSize(0)); + + Features features = product.getFeatures(); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + + features = product.getFeatures("1.23"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + } + + @Test + public void getFeaturesForProductWithUpgrades() { + LifxProduct product = LifxProduct.PRODUCT_32; + assertThat(product.getUpgrades(), hasSize(2)); + + Features features = product.getFeatures(); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(false)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.70"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(false)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.77"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.79"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.80"); + assertThat(features.getTemperatureRange(), is(TR_1500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.81"); + assertThat(features.getTemperatureRange(), is(TR_1500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + } +} diff --git a/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/fields/MACAddressTest.java b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/fields/MACAddressTest.java new file mode 100644 index 0000000000000..78b620b7af4f4 --- /dev/null +++ b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/fields/MACAddressTest.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2021 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.lifx.internal.fields; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.lifx.internal.fields.MACAddress.BROADCAST_ADDRESS; +import static org.openhab.core.util.HexUtils.bytesToHex; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.util.HexUtils; + +/** + * Tests {@link MACAddress}. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class MACAddressTest { + + @Test + public void broadcastAddress() { + assertThat(BROADCAST_ADDRESS.getAsLabel(), is("000000000000")); + assertThat(BROADCAST_ADDRESS.getHex(), is("00:00:00:00:00:00")); + assertThat(bytesToHex(BROADCAST_ADDRESS.getBytes().array()), is("000000000000")); + } + + @Test + public void defaultConstructor() { + MACAddress macAddress = new MACAddress(); + assertThat(macAddress.getAsLabel(), is("000000000000")); + assertThat(macAddress.getHex(), is("00:00:00:00:00:00")); + } + + @Test + public void constructFromByteBuffer() { + MACAddress macAddress = new MACAddress(ByteBuffer.wrap(HexUtils.hexToBytes("D073D5123456"))); + assertThat(macAddress.getAsLabel(), is("D073D5123456")); + assertThat(macAddress.getHex(), is("D0:73:D5:12:34:56")); + assertThat(bytesToHex(macAddress.getBytes().array()), is("D073D5123456")); + } + + @Test + public void constructFromString() { + MACAddress macAddress = new MACAddress("D073D5ABCDEF"); + assertThat(macAddress.getAsLabel(), is("D073D5ABCDEF")); + assertThat(macAddress.getHex(), is("D0:73:D5:AB:CD:EF")); + assertThat(bytesToHex(macAddress.getBytes().array()), is("D073D5ABCDEF")); + } + + @Test + public void broadcastAddressComparison() { + assertThat(BROADCAST_ADDRESS, is(BROADCAST_ADDRESS)); + assertThat(BROADCAST_ADDRESS.hashCode(), is(BROADCAST_ADDRESS.hashCode())); + + assertThat(BROADCAST_ADDRESS, is(new MACAddress())); + assertThat(BROADCAST_ADDRESS.hashCode(), is(new MACAddress().hashCode())); + + assertThat(BROADCAST_ADDRESS, is(not(new MACAddress("D073D5ABCDEF")))); + assertThat(BROADCAST_ADDRESS, is(not(new MACAddress("D073D5ABCDEF").hashCode()))); + } + + @Test + public void macAddressComparison() { + assertThat(new MACAddress("D073D5ABCDEF"), is(new MACAddress("D073D5ABCDEF"))); + assertThat(new MACAddress("D073D5ABCDEF").hashCode(), is(new MACAddress("D073D5ABCDEF").hashCode())); + + assertThat(new MACAddress("D073D5ABCDEF"), is(not(BROADCAST_ADDRESS))); + assertThat(new MACAddress("D073D5ABCDEF").hashCode(), is(not(BROADCAST_ADDRESS.hashCode()))); + + assertThat(new MACAddress("D073D5ABCDEF"), is(not(new MACAddress("D073D5123456")))); + assertThat(new MACAddress("D073D5ABCDEF").hashCode(), is(not(new MACAddress("D073D5123456").hashCode()))); + } +} diff --git a/bundles/org.openhab.binding.loxone/README.md b/bundles/org.openhab.binding.loxone/README.md index 37911c24bfa62..37ab59094e673 100644 --- a/bundles/org.openhab.binding.loxone/README.md +++ b/bundles/org.openhab.binding.loxone/README.md @@ -96,6 +96,9 @@ The acquired token will remain active for several weeks following the last succe In case a websocket connection to the Miniserver remains active for the whole duration of the token's life span, the binding will refresh the token one day before token expiration, without the need of providing the password. +In case of connecting to Generation 2 Miniservers, it is possible to establish a secure WebSocket connection over HTTPS protocol. Binding will automatically detect if HTTPS connection is available and will use it. In that case, commands sent to the Miniserver will not be additionally encrypted. When HTTPS is not available, binding will use unsecure HTTP connection and will encrypt each command. + +It is possible to override the communication protocol by setting `webSocketType` configuration parameter. Setting it to 1 will force to always establish HTTPS connection. Setting it to 2 will force to always establish HTTP connection. Default value of 0 means the binding will determine the right protocol in the runtime. A method to enable unrestricted security policy depends on the JRE version and vendor, some examples can be found [here](https://www.petefreitag.com/item/844.cfm) and [here](https://stackoverflow.com/questions/41580489/how-to-install-unlimited-strength-jurisdiction-policy-files). @@ -195,9 +198,10 @@ To define a parameter value in a .things file, please refer to it by parameter's ### Security -| ID | Name | Values | Default | Description | -|--------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------| -| `authMethod` | Authentication method | 0: Automatic
1: Hash-based
2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. | +| ID | Name | Values | Default | Description | +|-----------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------| +| `authMethod` | Authentication method | 0: Automatic
1: Hash-based
2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. | +| `webSocketType` | WebSocket protocol | 0: Automatic
1: Force HTTPS
2: Force HTTP | 0: Automatic | Communication protocol used for WebSocket connection. | ### Timeouts diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java index 1cebb8770567a..5d817bd2f4021 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java @@ -24,9 +24,13 @@ public class LxBindingConfiguration { */ public String host; /** - * Port of web service of the Miniserver + * Port of HTTP web service of the Miniserver */ public int port; + /** + * Port of HTTPS web service of the Miniserver + */ + public int httpsPort; /** * User name used to log into the Miniserver */ @@ -76,4 +80,8 @@ public class LxBindingConfiguration { * Authentication method (0-auto, 1-hash, 2-token) */ public int authMethod; + /** + * WebSocket connection type (0-auto, 1-HTTPS, 2-HTTP) + */ + public int webSocketType; } diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java index aa6b9d58dedc0..17517e2b3faa5 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java @@ -32,6 +32,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; @@ -72,7 +73,7 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandlerApi { private static final String SOCKET_URL = "/ws/rfc6455"; - private static final String CMD_CFG_API = "jdev/cfg/api"; + private static final String CMD_CFG_API = "jdev/cfg/apiKey"; private static final Gson GSON; @@ -139,6 +140,7 @@ public LxServerHandler(Thing thing, LxDynamicStateDescriptionProvider provider) @Override public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("[{}] Handle command: channelUID={}, command={}", debugId, channelUID, command); if (command instanceof RefreshType) { updateChannelState(channelUID); return; @@ -146,6 +148,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { LxControl control = channels.get(channelUID); if (control != null) { + logger.debug("[{}] Dispatching command to control UUID={}, name={}", debugId, control.getUuid(), + control.getName()); control.handleCommand(channelUID, command); } else { logger.error("[{}] Received command {} for unknown control.", debugId, command); @@ -182,7 +186,7 @@ public void initialize() { jettyThreadPool.setDaemon(true); socket = new LxWebSocket(debugId, this, bindingConfig, host); - wsClient = new WebSocketClient(); + wsClient = new WebSocketClient(new SslContextFactory.Client(true)); wsClient.setExecutor(jettyThreadPool); if (debugId > 1) { reconnectDelay.set(0); @@ -478,8 +482,16 @@ private void updateStateValue(LxStateUpdate update) { Map perStateUuid = states.get(update.getUuid()); if (perStateUuid != null) { perStateUuid.forEach((controlUuid, state) -> { + logger.debug("[{}] State update (UUID={}, value={}) dispatched to control UUID={}, state name={}", + debugId, update.getUuid(), update.getValue(), controlUuid, state.getName()); + state.setStateValue(update.getValue()); }); + if (perStateUuid.size() == 0) { + logger.debug("[{}] State update UUID={} has empty controls table", debugId, update.getUuid()); + } + } else { + logger.debug("[{}] State update UUID={} has no controls table", debugId, update.getUuid()); } } @@ -553,16 +565,36 @@ private boolean connect() { * Try to read CfgApi structure from the miniserver. It contains serial number and firmware version. If it can't * be read this is not a fatal issue, we will assume most recent version running. */ + boolean httpsCapable = false; String message = socket.httpGet(CMD_CFG_API); if (message != null) { LxResponse resp = socket.getResponse(message); if (resp != null) { - socket.setFwVersion(GSON.fromJson(resp.getValueAsString(), LxResponse.LxResponseCfgApi.class).version); + LxResponse.LxResponseCfgApi apiResp = GSON.fromJson(resp.getValueAsString(), + LxResponse.LxResponseCfgApi.class); + if (apiResp != null) { + socket.setFwVersion(apiResp.version); + httpsCapable = apiResp.httpsStatus != null && apiResp.httpsStatus == 1; + } } } else { logger.debug("[{}] Http get failed for API config request.", debugId); } + switch (bindingConfig.webSocketType) { + case 0: + // keep automatically determined option + break; + case 1: + logger.debug("[{}] Forcing HTTPS websocket connection.", debugId); + httpsCapable = true; + break; + case 2: + logger.debug("[{}] Forcing HTTP websocket connection.", debugId); + httpsCapable = false; + break; + } + try { wsClient.start(); @@ -570,7 +602,14 @@ private boolean connect() { // without this zero timeout, jetty will wait 30 seconds for stopping the client to eventually fail // with the timeout it is immediate and all threads end correctly jettyThreadPool.setStopTimeout(0); - URI target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL); + URI target; + if (httpsCapable) { + target = new URI("wss://" + host.getHostAddress() + ":" + bindingConfig.httpsPort + SOCKET_URL); + socket.setHttps(true); + } else { + target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL); + socket.setHttps(false); + } ClientUpgradeRequest request = new ClientUpgradeRequest(); request.setSubProtocols("remotecontrol"); diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java index 79867bd910adc..298e53f154579 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java @@ -78,6 +78,7 @@ public class LxWebSocket { private Session session; private String fwVersion; + private boolean httpsSession = false; private ScheduledFuture timeout; private LxWsBinaryHeader header; private LxWsSecurity security; @@ -455,9 +456,20 @@ void sendKeepAlive() { * @param fwVersion Miniserver firmware version */ void setFwVersion(String fwVersion) { + logger.debug("[{}] Firmware version: {}", debugId, fwVersion); this.fwVersion = fwVersion; } + /** + * Sets information if session is over HTTPS or HTTP protocol + * + * @param httpsSession true when HTTPS session + */ + void setHttps(boolean httpsSession) { + logger.debug("[{}] HTTPS session: {}", debugId, httpsSession); + this.httpsSession = httpsSession; + } + /** * Start a timer to wait for a Miniserver response to an action sent from the binding. * When timer expires, connection is removed and server error is reported. Further connection attempt can be made @@ -536,7 +548,7 @@ private boolean sendCmdNoResp(String command, boolean encrypt) { try { if (session != null) { String encrypted; - if (encrypt) { + if (encrypt && !httpsSession) { encrypted = security.encrypt(command); logger.debug("[{}] Sending encrypted string: {}", debugId, command); logger.debug("[{}] Encrypted: {}", debugId, encrypted); @@ -580,7 +592,9 @@ private void processResponse(String message) { } logger.debug("[{}] Response: {}", debugId, message.trim()); String control = resp.getCommand().trim(); - control = security.decryptControl(control); + if (!httpsSession) { + control = security.decryptControl(control); + } // for some reason the responses to some commands starting with jdev begin with dev, not jdev // this seems to be a bug in the Miniserver if (control.startsWith("dev/")) { diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java index 2533b90f8ce38..f16c04596fa3d 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java @@ -264,6 +264,8 @@ public final void handleCommand(ChannelUID channelId, Command command) throws IO Callbacks c = callbacks.get(channelId); if (c != null && c.commandCallback != null) { c.commandCallback.handleCommand(command); + } else { + logger.debug("Control UUID={} has no command handler", getUuid()); } } diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java index 45af9c7da117a..c9e4a37c6223d 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java @@ -39,6 +39,8 @@ public class LxResponse { public class LxResponseCfgApi { public String snr; public String version; + public String key; + public Integer httpsStatus; } /** diff --git a/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml index 52ef9ccf6c210..72330526c8f9f 100644 --- a/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml @@ -39,10 +39,15 @@ Host address or IP of the Loxone Miniserver
- - Web interface port of the Loxone Miniserver + + HTTP Web interface port of the Loxone Miniserver 80 + + + HTTPS Web interface port of the Loxone Miniserver + 443 + @@ -71,6 +76,18 @@ true + + + Protocol used to communicate over WebSocket to the Miniserver + 0 + + + + + + true + true + Time between binding initialization and first connection attempt (seconds, 0-120) diff --git a/bundles/org.openhab.binding.lutron/README.md b/bundles/org.openhab.binding.lutron/README.md index 149f6b8b5669d..a0dd8232158b6 100644 --- a/bundles/org.openhab.binding.lutron/README.md +++ b/bundles/org.openhab.binding.lutron/README.md @@ -794,7 +794,12 @@ end This binding integrates with the legacy Lutron RadioRA (Classic) lighting system. This binding depends on RS232 communication. -It has only been tested using the Chronos time module but the RS232 module should work as well. +It has only been tested using the Chronos System Bridge and Timeclock (RA-SBT-CHR) module, but Lutron's RA-RS232 or RB-RS232 module should work as well. + +Support has been added for bridged RadioRA systems. +A system is considered “bridged” when a Chronos System Bridge and Timeclock is used to integrate two RadioRA Systems in a single residence. +In a bridged system, the `system` parameter of each configured ra-dimmer, ra-switch, or ra-phantomButton thing should be set to indicate which RadioRA system it is a part of (i.e. 1 or 2). +In a non-bridged system, these parameters should be left at their default of 0. ## Supported Things @@ -808,17 +813,20 @@ This binding currently supports the following thing types: | ra-phantomButton | Thing | Phantom Button to control multiple controls (Scenes) | -## Thing Configurations - -| Thing | Config | Description | -|------------------|--------------|-----------------------------------------------------------------------| -| ra-rs232 | portName | The serial port to use to communicate with Chronos or RS232 module | -| | baud | (Optional) Baud Rate (defaults to 9600) | -| ra-dimmer | zoneNumber | Assigned Zone Number within the Lutron RadioRA system | -| | fadeOutSec | (Optional) Time in seconds dimmer should take when lowering the level | -| | fadeInSec | (Optional) Time in seconds dimmer should take when lowering the level | -| ra-switch | zoneNumber | Assigned Zone Number within the Lutron RadioRA system | -| ra-phantomButton | buttonNumber | Phantom Button Number within the Lutron RadioRA system | +## Thing Configuration Parameters + +| Thing | Parameter | Description | +|------------------|--------------|------------------------------------------------------------------------| +| ra-rs232 | portName | The serial port to use to communicate with Chronos or RS232 module | +| | baud | (Optional) Baud Rate (defaults to 9600) | +| ra-dimmer | zoneNumber | Assigned Zone Number within the Lutron RadioRA system | +| | system | (Optional) System number (1 or 2) in a bridged system. Default=0 (n/a) | +| | fadeOutSec | (Optional) Time in seconds dimmer should take when lowering the level | +| | fadeInSec | (Optional) Time in seconds dimmer should take when lowering the level | +| ra-switch | zoneNumber | Assigned Zone Number within the Lutron RadioRA system | +| | system | (Optional) System number (1 or 2) in a bridged system. Default=0 (n/a) | +| ra-phantomButton | buttonNumber | Phantom Button Number within the Lutron RadioRA system | +| | system | (Optional) System number (1 or 2) in a bridged system. Default=0 (n/a) | ## Channels diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/config/IPBridgeConfig.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/config/IPBridgeConfig.java index 4a1a3ce455724..5bde94ae0741f 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/config/IPBridgeConfig.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/config/IPBridgeConfig.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.lutron.internal.config; -import org.openhab.binding.lutron.internal.StringUtils; +import java.util.Objects; /** * Configuration settings for an {@link org.openhab.binding.lutron.internal.handler.IPBridgeHandler}. @@ -30,8 +30,8 @@ public class IPBridgeConfig { public int delay = 0; public boolean sameConnectionParameters(IPBridgeConfig config) { - return StringUtils.equals(ipAddress, config.ipAddress) && StringUtils.equals(user, config.user) - && StringUtils.equals(password, config.password) && (reconnect == config.reconnect) + return Objects.equals(ipAddress, config.ipAddress) && Objects.equals(user, config.user) + && Objects.equals(password, config.password) && (reconnect == config.reconnect) && (heartbeat == config.heartbeat) && (delay == config.delay); } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java index e564ee5225c37..61c6cf4e9bae7 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java @@ -25,7 +25,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.openhab.binding.lutron.internal.StringUtils; import org.openhab.binding.lutron.internal.config.IPBridgeConfig; import org.openhab.binding.lutron.internal.discovery.LutronDeviceDiscoveryService; import org.openhab.binding.lutron.internal.net.TelnetSession; @@ -156,7 +155,8 @@ private boolean validConfiguration(IPBridgeConfig config) { return false; } - if (StringUtils.isEmpty(config.ipAddress)) { + String ipAddress = config.ipAddress; + if (ipAddress == null || ipAddress.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge address not specified"); return false; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java index 31f1cd975276a..a66f0bf9e700d 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java @@ -76,7 +76,7 @@ public void handleMessage(String msg) { logger.trace("Received message: {}", msg); try { - JsonObject message = (JsonObject) new JsonParser().parse(msg); + JsonObject message = (JsonObject) JsonParser.parseString(msg); if (!message.has("CommuniqueType")) { logger.debug("No CommuniqueType found in message: {}", msg); diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232Connection.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232Connection.java index 2335ef0f99895..f917aa0127254 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232Connection.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232Connection.java @@ -15,8 +15,11 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; import java.util.TooManyListenersException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; @@ -34,16 +37,17 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class RS232Connection implements RadioRAConnection, SerialPortEventListener { private final Logger logger = LoggerFactory.getLogger(RS232Connection.class); protected SerialPortManager serialPortManager; - protected SerialPort serialPort; + protected @Nullable SerialPort serialPort; - protected BufferedReader inputReader; + protected @Nullable BufferedReader inputReader; - protected RadioRAFeedbackListener listener; + protected @Nullable RadioRAFeedbackListener listener; protected RS232MessageParser parser = new RS232MessageParser(); public RS232Connection(SerialPortManager serialPortManager) { @@ -59,7 +63,8 @@ public void open(String portName, int baud) throws RadioRAConnectionException { } try { - serialPort = portIdentifier.open("openhab", 5000); + SerialPort serialPort = portIdentifier.open("openhab", 5000); + this.serialPort = serialPort; serialPort.notifyOnDataAvailable(true); serialPort.setSerialPortParams(baud, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); serialPort.addEventListener(this); @@ -78,8 +83,19 @@ public void open(String portName, int baud) throws RadioRAConnectionException { @Override public void write(String command) { logger.debug("Writing to serial port: {}", command.toString()); + SerialPort serialPort = this.serialPort; + try { - serialPort.getOutputStream().write(command.getBytes()); + if (serialPort != null) { + OutputStream outputStream = serialPort.getOutputStream(); + if (outputStream != null) { + outputStream.write(command.getBytes()); + } else { + logger.debug("Cannot write to serial port. outputStream is null."); + } + } else { + logger.debug("Cannot write to serial port. serialPort is null."); + } } catch (IOException e) { logger.debug("An error occurred writing to serial port", e); } @@ -87,15 +103,19 @@ public void write(String command) { @Override public void disconnect() { - serialPort.close(); + SerialPort serialPort = this.serialPort; + if (serialPort != null) { + serialPort.close(); + } } @Override public void serialEvent(SerialPortEvent ev) { switch (ev.getEventType()) { case SerialPortEvent.DATA_AVAILABLE: + BufferedReader inputReader = this.inputReader; try { - if (!inputReader.ready()) { + if (inputReader == null || !inputReader.ready()) { logger.debug("Serial Data Available but input reader not ready"); return; } @@ -106,7 +126,12 @@ public void serialEvent(SerialPortEvent ev) { if (feedback != null) { logger.debug("Msg Parsed as {}", feedback.getClass().getName()); - listener.handleRadioRAFeedback(feedback); + RadioRAFeedbackListener listener = this.listener; + if (listener != null) { + listener.handleRadioRAFeedback(feedback); + } else { + logger.debug("Cannot handle feedback message. Listener is null."); + } } logger.debug("Finished handling feedback"); } catch (IOException e) { diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232MessageParser.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232MessageParser.java index 4ee5082f7da1e..6052f748d251b 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232MessageParser.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RS232MessageParser.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.lutron.internal.radiora; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.radiora.protocol.LEDMapFeedback; import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback; import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback; @@ -25,11 +27,12 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class RS232MessageParser { - private Logger logger = LoggerFactory.getLogger(RS232MessageParser.class); + private final Logger logger = LoggerFactory.getLogger(RS232MessageParser.class); - public RadioRAFeedback parse(String msg) { + public @Nullable RadioRAFeedback parse(String msg) { String prefix = parsePrefix(msg); switch (prefix) { diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnection.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnection.java index 1b4cb21835592..e03b6287d9f2b 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnection.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnection.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.lutron.internal.radiora; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Interface to the RadioRA Classic system * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public interface RadioRAConnection { public void open(String portName, int baud) throws RadioRAConnectionException; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnectionException.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnectionException.java index 198f3bf0d751e..df9f7106deb99 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnectionException.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAConnectionException.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.lutron.internal.radiora; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Thrown when an attempt to open a RadioRA Connection fails. * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class RadioRAConnectionException extends Exception { private static final long serialVersionUID = 1L; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAFeedbackListener.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAFeedbackListener.java index 9b884f45d987c..4e6050e6cb0d1 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAFeedbackListener.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/RadioRAFeedbackListener.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.lutron.internal.radiora; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback; /** @@ -20,6 +21,7 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public interface RadioRAFeedbackListener { void handleRadioRAFeedback(RadioRAFeedback feedback); diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/DimmerConfig.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/DimmerConfig.java index b6344794e6fb6..08a32a39db1a6 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/DimmerConfig.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/DimmerConfig.java @@ -21,31 +21,20 @@ * */ public class DimmerConfig { - private int zoneNumber; - private BigDecimal fadeOutSec; - private BigDecimal fadeInSec; + public int zoneNumber; + public int system = 0; + public BigDecimal fadeOutSec; + public BigDecimal fadeInSec; public int getZoneNumber() { return zoneNumber; } - public void setZoneNumber(int zoneNumber) { - this.zoneNumber = zoneNumber; - } - public BigDecimal getFadeOutSec() { return fadeOutSec; } - public void setFadeOutSec(BigDecimal fadeOutSec) { - this.fadeOutSec = fadeOutSec; - } - public BigDecimal getFadeInSec() { return fadeInSec; } - - public void setFadeInSec(BigDecimal fadeInSec) { - this.fadeInSec = fadeInSec; - } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/PhantomButtonConfig.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/PhantomButtonConfig.java index 8637c44274865..2c68be75f3a3f 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/PhantomButtonConfig.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/PhantomButtonConfig.java @@ -12,32 +12,21 @@ */ package org.openhab.binding.lutron.internal.radiora.config; -import java.math.BigDecimal; +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Configuration class for PhantomButton thing type. - * + * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class PhantomButtonConfig { - private int buttonNumber; - private BigDecimal fadeSec; + public int buttonNumber; + public int system = 0; public int getButtonNumber() { return buttonNumber; } - - public void setButtonNumber(int buttonNumber) { - this.buttonNumber = buttonNumber; - } - - public BigDecimal getFadeSec() { - return fadeSec; - } - - public void setFadeSec(BigDecimal fadeSec) { - this.fadeSec = fadeSec; - } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/RS232Config.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/RS232Config.java index 1b236846ddd12..07ab05787546c 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/RS232Config.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/RS232Config.java @@ -12,39 +12,30 @@ */ package org.openhab.binding.lutron.internal.radiora.config; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Configuration class for RS232 thing type. * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class RS232Config { - private String portName; - private int baud = 9600; - private int zoneMapQueryInterval = 60; + public String portName = ""; + public int baud = 9600; + public int zoneMapQueryInterval = 60; public String getPortName() { return portName; } - public void setPortName(String portName) { - this.portName = portName; - } - public int getBaud() { return baud; } - public void setBaud(int baud) { - this.baud = baud; - } - public int getZoneMapQueryInterval() { return zoneMapQueryInterval; } - - public void setZoneMapQueryInterval(int zoneMapQueryInterval) { - this.zoneMapQueryInterval = zoneMapQueryInterval; - } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/SwitchConfig.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/SwitchConfig.java index ec7a5bec492b2..064a000de5a70 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/SwitchConfig.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/config/SwitchConfig.java @@ -12,21 +12,21 @@ */ package org.openhab.binding.lutron.internal.radiora.config; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Configuration class for Switch thing type. - * + * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class SwitchConfig { - private int zoneNumber; + public int zoneNumber; + public int system = 0; public int getZoneNumber() { return zoneNumber; } - - public void setZoneNumber(int zoneNumber) { - this.zoneNumber = zoneNumber; - } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/DimmerHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/DimmerHandler.java index 997c871eb7b62..051ab6254a905 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/DimmerHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/DimmerHandler.java @@ -15,6 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.LutronBindingConstants; import org.openhab.binding.lutron.internal.radiora.config.DimmerConfig; import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback; @@ -34,6 +35,7 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class DimmerHandler extends LutronHandler { /** @@ -41,24 +43,33 @@ public class DimmerHandler extends LutronHandler { * to external dimmer changes since RadioRA protocol does not send dimmer * levels in their messages. */ + private @NonNullByDefault({}) DimmerConfig config; private AtomicInteger lastKnownIntensity = new AtomicInteger(100); - private AtomicBoolean switchEnabled = new AtomicBoolean(false); public DimmerHandler(Thing thing) { super(thing); } + @Override + public void initialize() { + config = getConfigAs(DimmerConfig.class); + super.initialize(); + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { - DimmerConfig config = getConfigAs(DimmerConfig.class); + RS232Handler bridgeHandler = getRS232Handler(); + if (bridgeHandler == null) { + return; + } if (LutronBindingConstants.CHANNEL_LIGHTLEVEL.equals(channelUID.getId())) { if (command instanceof PercentType) { int intensity = ((PercentType) command).intValue(); - SetDimmerLevelCommand cmd = new SetDimmerLevelCommand(config.getZoneNumber(), intensity); - getRS232Handler().sendCommand(cmd); + SetDimmerLevelCommand cmd = new SetDimmerLevelCommand(config.getZoneNumber(), intensity, config.system); + bridgeHandler.sendCommand(cmd); updateInternalState(intensity); } @@ -66,11 +77,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType onOffCmd = (OnOffType) command; - SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(config.getZoneNumber(), onOffCmd); - getRS232Handler().sendCommand(cmd); + SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(config.getZoneNumber(), onOffCmd, config.system); + bridgeHandler.sendCommand(cmd); updateInternalState(onOffCmd); - } } } @@ -85,7 +95,10 @@ public void handleFeedback(RadioRAFeedback feedback) { } private void handleZoneMapFeedback(ZoneMapFeedback feedback) { - char value = feedback.getZoneValue(getConfigAs(DimmerConfig.class).getZoneNumber()); + if (!systemsMatch(feedback.getSystem(), config.system)) { + return; + } + char value = feedback.getZoneValue(config.getZoneNumber()); if (value == '1') { turnDimmerOnToLastKnownIntensity(); } else if (value == '0') { @@ -94,7 +107,7 @@ private void handleZoneMapFeedback(ZoneMapFeedback feedback) { } private void handleLocalZoneChangeFeedback(LocalZoneChangeFeedback feedback) { - if (feedback.getZoneNumber() == getConfigAs(DimmerConfig.class).getZoneNumber()) { + if (systemsMatch(feedback.getSystem(), config.system) && feedback.getZoneNumber() == config.getZoneNumber()) { if (LocalZoneChangeFeedback.State.ON.equals(feedback.getState())) { turnDimmerOnToLastKnownIntensity(); } else if (LocalZoneChangeFeedback.State.OFF.equals(feedback.getState())) { diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/LutronHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/LutronHandler.java index b49366a5a7405..b0e9cd80ffb85 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/LutronHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/LutronHandler.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.lutron.internal.radiora.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -26,13 +28,14 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public abstract class LutronHandler extends BaseThingHandler { public LutronHandler(Thing thing) { super(thing); } - public RS232Handler getRS232Handler() { + public @Nullable RS232Handler getRS232Handler() { Bridge bridge = getBridge(); if (bridge == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Unable to get bridge"); @@ -47,6 +50,13 @@ public RS232Handler getRS232Handler() { } } + /** + * Returns true if system numbers match, meaning that either both are 2 or both are 1 or 0 (n/a). + */ + public static boolean systemsMatch(int a, int b) { + return ((a == 2 && b == 2) || ((a == 0 || a == 1) && (b == 0 || b == 1))); + } + public abstract void handleFeedback(RadioRAFeedback feedback); @Override diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/PhantomButtonHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/PhantomButtonHandler.java index 22199d8c5517b..6b17f9193c56f 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/PhantomButtonHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/PhantomButtonHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.lutron.internal.radiora.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.LutronBindingConstants; import org.openhab.binding.lutron.internal.radiora.config.PhantomButtonConfig; import org.openhab.binding.lutron.internal.radiora.protocol.ButtonPressCommand; @@ -28,20 +29,31 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class PhantomButtonHandler extends LutronHandler { + private @NonNullByDefault({}) PhantomButtonConfig config; + public PhantomButtonHandler(Thing thing) { super(thing); } + @Override + public void initialize() { + config = getConfigAs(PhantomButtonConfig.class); + super.initialize(); + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { + RS232Handler bridgeHandler = getRS232Handler(); if (channelUID.getId().equals(LutronBindingConstants.CHANNEL_SWITCH)) { if (command instanceof OnOffType) { - ButtonPressCommand cmd = new ButtonPressCommand( - getConfigAs(PhantomButtonConfig.class).getButtonNumber(), - ButtonPressCommand.ButtonState.valueOf(command.toString())); - getRS232Handler().sendCommand(cmd); + ButtonPressCommand cmd = new ButtonPressCommand(config.getButtonNumber(), + ButtonPressCommand.ButtonState.valueOf(command.toString()), config.system); + if (bridgeHandler != null) { + bridgeHandler.sendCommand(cmd); + } } } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/RS232Handler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/RS232Handler.java index 086b5a687bc91..66cd7b502082d 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/RS232Handler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/RS232Handler.java @@ -15,6 +15,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.radiora.RS232Connection; import org.openhab.binding.lutron.internal.radiora.RadioRAConnection; import org.openhab.binding.lutron.internal.radiora.RadioRAConnectionException; @@ -41,13 +43,14 @@ * * @author Jeff Lauterbach - Initial contribution */ +@NonNullByDefault public class RS232Handler extends BaseBridgeHandler implements RadioRAFeedbackListener { - private Logger logger = LoggerFactory.getLogger(RS232Handler.class); + private final Logger logger = LoggerFactory.getLogger(RS232Handler.class); private RadioRAConnection connection; - private ScheduledFuture zoneMapScheduledTask; + private @Nullable ScheduledFuture zoneMapScheduledTask; public RS232Handler(Bridge bridge, SerialPortManager serialPortManager) { super(bridge); @@ -58,6 +61,7 @@ public RS232Handler(Bridge bridge, SerialPortManager serialPortManager) { @Override public void dispose() { + ScheduledFuture zoneMapScheduledTask = this.zoneMapScheduledTask; if (zoneMapScheduledTask != null) { zoneMapScheduledTask.cancel(true); } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/SwitchHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/SwitchHandler.java index 1232b2ae30980..aa3e27eab0ec1 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/SwitchHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/handler/SwitchHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.lutron.internal.radiora.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.LutronBindingConstants; import org.openhab.binding.lutron.internal.radiora.config.SwitchConfig; import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback; @@ -31,22 +32,33 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class SwitchHandler extends LutronHandler { - private Logger logger = LoggerFactory.getLogger(SwitchHandler.class); + private final Logger logger = LoggerFactory.getLogger(SwitchHandler.class); + private @NonNullByDefault({}) SwitchConfig config; public SwitchHandler(Thing thing) { super(thing); } + @Override + public void initialize() { + config = getConfigAs(SwitchConfig.class); + super.initialize(); + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { + RS232Handler bridgeHandler = getRS232Handler(); if (LutronBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) { if (command instanceof OnOffType) { - SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(getConfigAs(SwitchConfig.class).getZoneNumber(), - (OnOffType) command); + SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(config.getZoneNumber(), (OnOffType) command, + config.system); - getRS232Handler().sendCommand(cmd); + if (bridgeHandler != null) { + bridgeHandler.sendCommand(cmd); + } } } } @@ -61,7 +73,10 @@ public void handleFeedback(RadioRAFeedback feedback) { } private void handleZoneMapFeedback(ZoneMapFeedback feedback) { - char value = feedback.getZoneValue(getConfigAs(SwitchConfig.class).getZoneNumber()); + if (!systemsMatch(feedback.getSystem(), config.system)) { + return; + } + char value = feedback.getZoneValue(config.getZoneNumber()); if (value == '1') { updateState(LutronBindingConstants.CHANNEL_SWITCH, OnOffType.ON); @@ -71,7 +86,7 @@ private void handleZoneMapFeedback(ZoneMapFeedback feedback) { } private void handleLocalZoneChangeFeedback(LocalZoneChangeFeedback feedback) { - if (feedback.getZoneNumber() == getConfigAs(SwitchConfig.class).getZoneNumber()) { + if (systemsMatch(feedback.getSystem(), config.system) && feedback.getZoneNumber() == config.getZoneNumber()) { if (LocalZoneChangeFeedback.State.CHG.equals(feedback.getState())) { logger.debug("Not Implemented Yet - CHG state received from Local Zone Change Feedback."); } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ButtonPressCommand.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ButtonPressCommand.java index 5d07fd63932c7..8f5a31fc69461 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ButtonPressCommand.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ButtonPressCommand.java @@ -15,6 +15,9 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Button Press (BP) Command. * Trigger a Phantom Button Press on the RadioRA Serial Device. @@ -22,6 +25,7 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class ButtonPressCommand extends RadioRACommand { public enum ButtonState { @@ -32,11 +36,13 @@ public enum ButtonState { private int buttonNumber; // 1 to 15, 16 ALL ON, 17 ALL OFF private ButtonState state; // ON/OFF/TOG - private Integer fadeSec; // 0 to 240 (optional) + private @Nullable Integer fadeSec; // 0 to 240 (optional) + private int system; // 1 or 2, or 0 for none - public ButtonPressCommand(int buttonNumber, ButtonState state) { + public ButtonPressCommand(int buttonNumber, ButtonState state, int system) { this.buttonNumber = buttonNumber; this.state = state; + this.system = system; } public void setFadeSeconds(int seconds) { @@ -58,6 +64,10 @@ public List getArgs() { args.add(String.valueOf(fadeSec)); } + if (system == 1 || system == 2) { + args.add("S" + String.valueOf(system)); + } + return args; } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LEDMapFeedback.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LEDMapFeedback.java index f25a01367b3cf..7c6ca738f9e91 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LEDMapFeedback.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LEDMapFeedback.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.lutron.internal.radiora.protocol; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Feedback (LMP) that gives the state of all phantom LEDs *

@@ -34,6 +36,7 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class LEDMapFeedback extends RadioRAFeedback { private String bitmap; // 15 bit String of (0,1). 1 is ON, 0 is OFF diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LocalZoneChangeFeedback.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LocalZoneChangeFeedback.java index 96fff4fc67db4..5cd9391f28a14 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LocalZoneChangeFeedback.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/LocalZoneChangeFeedback.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.lutron.internal.radiora.protocol; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Feedback for when a device was changed locally (not through Master Control) *

@@ -37,13 +41,22 @@ * LZC,04,ON * * + * In a bridged system, a system parameter S1 or S2 will be appended. + * + *

+ * LZC,04,ON,S2
+ * 
+ * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class LocalZoneChangeFeedback extends RadioRAFeedback { + private final Logger logger = LoggerFactory.getLogger(LocalZoneChangeFeedback.class); private int zoneNumber; // 1 to 32 private State state; // ON, OFF, CHG + private int system; // 1 or 2, or 0 for none public enum State { ON, @@ -56,6 +69,18 @@ public LocalZoneChangeFeedback(String msg) { zoneNumber = Integer.parseInt(params[1].trim()); state = State.valueOf(params[2].trim().toUpperCase()); + + system = 0; + if (params.length > 3) { + String sysParam = params[3].trim().toUpperCase(); + if ("S1".equals(sysParam)) { + system = 1; + } else if ("S2".equals(sysParam)) { + system = 2; + } else { + logger.debug("Invalid 3rd parameter {} in LZC message. Should be S1 or S2.", sysParam); + } + } } public State getState() { @@ -65,4 +90,8 @@ public State getState() { public int getZoneNumber() { return zoneNumber; } + + public int getSystem() { + return system; + } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRACommand.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRACommand.java index 180f48f223df1..ba103e665dd7b 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRACommand.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRACommand.java @@ -14,12 +14,15 @@ import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Abstract base class for commands. * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public abstract class RadioRACommand { protected static final String FIELD_SEPARATOR = ","; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRAFeedback.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRAFeedback.java index 366eddec09a94..5ee20a6d6a146 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRAFeedback.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/RadioRAFeedback.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.lutron.internal.radiora.protocol; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Base class for Feedback from RadioRA * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class RadioRAFeedback { public String[] parse(String msg, int numParams) { diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetDimmerLevelCommand.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetDimmerLevelCommand.java index ae17a3781bf44..16f4762e8e1f6 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetDimmerLevelCommand.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetDimmerLevelCommand.java @@ -15,6 +15,9 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Set Dimmer Level (SDL) * Set an individual Dimmer’s light level. @@ -22,15 +25,18 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class SetDimmerLevelCommand extends RadioRACommand { private int zoneNumber; // 1 to 32 private int dimmerLevel; // 0 to 100 - private Integer fadeSec; // 0 to 240 (optional) + private @Nullable Integer fadeSec; // 0 to 240 (optional) + private int system; // 1 or 2, or 0 for none - public SetDimmerLevelCommand(int zoneNumber, int dimmerLevel) { + public SetDimmerLevelCommand(int zoneNumber, int dimmerLevel, int system) { this.zoneNumber = zoneNumber; this.dimmerLevel = dimmerLevel; + this.system = system; } public void setFadeSeconds(int seconds) { @@ -52,6 +58,10 @@ public List getArgs() { args.add(String.valueOf(fadeSec)); } + if (system == 1 || system == 2) { + args.add("S" + String.valueOf(system)); + } + return args; } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetSwitchLevelCommand.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetSwitchLevelCommand.java index 06694a51e14ef..d762c9b680d72 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetSwitchLevelCommand.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/SetSwitchLevelCommand.java @@ -15,6 +15,8 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.OnOffType; /** @@ -24,15 +26,18 @@ * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class SetSwitchLevelCommand extends RadioRACommand { private int zoneNumber; // 1 to 32 private OnOffType state; // ON/OFF - private Integer delaySec; // 0 to 240 (optional) + private @Nullable Integer delaySec; // 0 to 240 (optional) + private int system; // 1 or 2, or 0 for none - public SetSwitchLevelCommand(int zoneNumber, OnOffType state) { + public SetSwitchLevelCommand(int zoneNumber, OnOffType state, int system) { this.zoneNumber = zoneNumber; this.state = state; + this.system = system; } public void setDelaySeconds(int seconds) { @@ -54,6 +59,10 @@ public List getArgs() { args.add(String.valueOf(delaySec)); } + if (system == 1 || system == 2) { + args.add("S" + String.valueOf(system)); + } + return args; } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapFeedback.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapFeedback.java index 68fae75c01029..6f046287f099c 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapFeedback.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapFeedback.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.lutron.internal.radiora.protocol; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Feedback that gives the state of all zones *

@@ -26,22 +30,39 @@ * Example: *

* Zones 2 and 9 are ON, all others are OFF, and Zones 31 and 32 are unassigned. + * In a bridged system, a system parameter S1 or S2 will be appended. * *

  * ZMP,010000001000000000000000000000XX
+ * ZMP,00100000010000000000000000000000,S2
  * 
* * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class ZoneMapFeedback extends RadioRAFeedback { + private final Logger logger = LoggerFactory.getLogger(ZoneMapFeedback.class); private String zoneStates; // 32 bit String of (0,1,X) + private int system; // 1 or 2, or 0 for none public ZoneMapFeedback(String msg) { String[] params = parse(msg, 1); zoneStates = params[1]; + + system = 0; + if (params.length > 2) { + String sysParam = params[2].trim().toUpperCase(); + if ("S1".equals(sysParam)) { + system = 1; + } else if ("S2".equals(sysParam)) { + system = 2; + } else { + logger.debug("Invalid 2nd parameter {} in ZMP message. Should be S1 or S2.", sysParam); + } + } } public String getZoneStates() { @@ -55,4 +76,8 @@ public char getZoneValue(int zone) { return zoneStates.charAt(zone - 1); } + + public int getSystem() { + return system; + } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapInquiryCommand.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapInquiryCommand.java index caf411b744229..11e6cba15b1ba 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapInquiryCommand.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/radiora/protocol/ZoneMapInquiryCommand.java @@ -15,12 +15,15 @@ import java.util.Collections; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Requests an updated ZoneMap. * * @author Jeff Lauterbach - Initial Contribution * */ +@NonNullByDefault public class ZoneMapInquiryCommand extends RadioRACommand { @Override diff --git a/bundles/org.openhab.binding.lutron/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lutron/src/main/resources/OH-INF/thing/thing-types.xml index d9e9459ac6e8c..b3b40832e227c 100644 --- a/bundles/org.openhab.binding.lutron/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.lutron/src/main/resources/OH-INF/thing/thing-types.xml @@ -1203,6 +1203,11 @@ Assigned Zone Number within the Lutron RadioRA system.
+ + + System number (bridged systems only). Set to 1 or 2. 0 = NA (default). + 0 + Time in seconds dimmer should take when lowering the level @@ -1231,6 +1236,11 @@ Assigned Zone Number within the Lutron RadioRA system. + + + System number (bridged systems only). Set to 1 or 2. 0 = NA (default). + 0 +
@@ -1250,6 +1260,11 @@ Phantom Button Number within the Lutron RadioRA system.
+ + + System number (bridged systems only). Set to 1 or 2. 0 = NA (default). + 0 +
diff --git a/bundles/org.openhab.binding.luxtronikheatpump/NOTICE b/bundles/org.openhab.binding.luxtronikheatpump/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.luxtronikheatpump/README.md b/bundles/org.openhab.binding.luxtronikheatpump/README.md new file mode 100644 index 0000000000000..c8aedc74522bb --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/README.md @@ -0,0 +1,334 @@ +# LuxtronikHeatpump Binding + +This binding gives the possibility to integrate any Heatpump that is based on the Luxtronik 2 contol unit of Alpha Innotec. This includes heatpumps of: + +* Alpha InnoTec +* Buderus (Logamatic HMC20, HMC20 Z) +* CTA All-In-One (Aeroplus) +* Elco +* Nibe (AP-AW10) +* Roth (ThermoAura®, ThermoTerra) +* (Siemens) Novelan (WPR NET) +* Wolf Heiztechnik (BWL/BWS) + +This binding was tested with: + +* Siemens Novelan LD 7 + +_If you have another heatpump the binding works with, let us know, so we can extend the list_ + +Note: The whole functionality is based on data that was reverse engineered, so use it at your own risk. + +## Supported Things + +This binding only supports one thing type "Luxtronik Heatpump" (heatpump). + +## Thing Configuration + +Each heatpump requires the following configuration parameters: + +| parameter | required | default | description | +|--------------|----------|---------|-------------| +| ipAddress | yes | | IP address of the heatpump | +| port | no | 8889 | Port number to connect to. This should be `8889` for most heatpumps. For heatpumps using a firmware version before V1.73 port `8888` needs to be used. | +| refresh | no | 300 | Interval (in seconds) to refresh the channel values. | +| showAllChannels | no | false | Show all channels (even those determined as not supported) | + +## Channels + +As the Luxtronik 2 control is able to handle multiple heat pumps with different features (like heating, hot water, cooling, solar, photovoltaics, swimming pool,...), the binding has a lot channels. +Depending on the heatpump it is used with, various channels might not hold any (useful) values. +If `showAllChannels` is not activated for the thing, this binding will automatically try to hide channels that are not available for your heat pump. As this is done using reverse engineered parameters it might not be correct in all cases. +If you miss a channel that should be available for your heat pump, you can enable `showAllChannels` for your thing, so all channels become available. Feel free to report such a case on the forum, so we can try to improve / fix that behavior. + +The following channels are holding read only values: + +| channel | type | advanced | description | +|----------|--------|----------|------------------------------| +| temperatureHeatingCircuitFlow | Number:Temperature | | Flow temperature heating circuit | +| temperatureHeatingCircuitReturn | Number:Temperature | | Return temperature heating circuit | +| temperatureHeatingCircuitReturnTarget | Number:Temperature | | Return setpoint heating circuit | +| temperatureBufferTankReturn | Number:Temperature | x | Return temperature in buffer tank | +| temperatureHotGas | Number:Temperature | x | Hot gas temperature | +| temperatureOutside | Number:Temperature | | Outside temperature | +| temperatureOutsideMean | Number:Temperature | | Average temperature outside over 24 h (heating limit function) | +| temperatureHotWater | Number:Temperature | | Hot water actual temperature | +| temperatureHotWaterTarget | Number:Temperature | | Hot water target temperature | +| temperatureHeatSourceInlet | Number:Temperature | x | Heat source inlet temperature | +| temperatureHeatSourceOutlet | Number:Temperature | x | Heat source outlet temperature | +| temperatureMixingCircuit1Flow | Number:Temperature | x | Mixing circuit 1 Flow temperature | +| temperatureMixingCircuit1FlowTarget | Number:Temperature | x | Mixing circuit 1 Flow target temperature | +| temperatureRoomStation | Number:Temperature | x | Room temperature room station 1 | +| temperatureMixingCircuit2Flow | Number:Temperature | x | Mixing circuit 2 Flow temperature | +| temperatureMixingCircuit2FlowTarget | Number:Temperature | x | Mixing circuit 2 Flow target temperature | +| temperatureSolarCollector | Number:Temperature | x | Solar collector sensor | +| temperatureSolarTank | Number:Temperature | x | Solar tank sensor | +| temperatureExternalEnergySource | Number:Temperature | x | Sensor external energy source | +| inputASD | Switch | x | Input "Defrost end, brine pressure, flow rate" | +| inputHotWaterThermostat | Switch | x | Input "Domestic hot water thermostat" | +| inputUtilityLock | Switch | x | Input "EVU lock" | +| inputHighPressureCoolingCircuit | Switch | x | Input "High pressure cooling circuit | +| inputMotorProtectionOK | Switch | x | Input "Motor protection OK" | +| inputLowPressure | Switch | x | Input "Low pressure" | +| inputPEX | Switch | x | Input "Monitoring contact for potentiostat" | +| inputSwimmingPoolThermostat | Switch | x | Input "Swimming pool thermostat" | +| outputDefrostValve | Switch | x | Output "Defrost valve" | +| outputBUP | Switch | x | Output "Domestic hot water pump/changeover valve" | +| outputHeatingCirculationPump | Switch | x | Output "Heating circulation pump" | +| outputMixingCircuit1Open | Switch | x | Output "Mixing circuit 1 Open" | +| outputMixingCircuit1Closed | Switch | x | Output "Mixing circuit 1 Closed" | +| outputVentilation | Switch | x | Output "Ventilation" | +| outputVBO | Switch | x | Output "Brine pump/fan" | +| outputCompressor1 | Switch | x | Output "Compressor 1" | +| outputCompressor2 | Switch | x | Output "Compressor 2" | +| outputCirculationPump | Switch | x | Output "Circulation pump" | +| outputZUP | Switch | x | Output "Auxiliary circulation pump" | +| outputControlSignalAdditionalHeating | Switch | x | Output "Control signal additional heating" | +| outputFaultSignalAdditionalHeating | Switch | x | Output "Control signal additional heating/fault signal" | +| outputAuxiliaryHeater3 | Switch | x | Output "Auxiliary heater 3" | +| outputMixingCircuitPump2 | Switch | x | Output "Pump mixing circuit 2" | +| outputSolarChargePump | Switch | x | Output "Solar charge pump" | +| outputSwimmingPoolPump | Switch | x | Output "Swimming pool pump" | +| outputMixingCircuit2Closed | Switch | x | Output "Mixing circuit 2 Closed" | +| outputMixingCircuit2Open | Switch | x | Output "Mixing circuit 2 Open" | +| runtimeTotalCompressor1 | Number:Time | | Operation hours compressor 1 | +| pulsesCompressor1 | Number:Dimensionless | | Pulses compressor 1 | +| runtimeTotalCompressor2 | Number:Time | x | Operation hours compressor 2 | +| pulsesCompressor2 | Number:Dimensionless | x | Pulses compressor 2 | +| runtimeTotalSecondHeatGenerator1 | Number:Time | x | Operation hours Second heat generator 1 | +| runtimeTotalSecondHeatGenerator2 | Number:Time | x | Operation hours Second heat generator 2 | +| runtimeTotalSecondHeatGenerator3 | Number:Time | x | Operation hours Second heat generator 3 | +| runtimeTotalHeatPump | Number:Time | | Operation hours heat pump | +| runtimeTotalHeating | Number:Time | | Operation hours heating | +| runtimeTotalHotWater | Number:Time | | Operation hours hot water | +| runtimeTotalCooling | Number:Time | | Operation hours cooling | +| runtimeCurrentHeatPump | Number:Time | x | Heat pump running since | +| runtimeCurrentSecondHeatGenerator1 | Number:Time | x | Second heat generator 1 running since | +| runtimeCurrentSecondHeatGenerator2 | Number:Time | x | Second heat generator 2 running since | +| mainsOnDelay | Number:Time | x | Mains on delay | +| switchingCycleLockOff | Number:Time | x | Switching cycle lock off | +| switchingCycleLockOn | Number:Time | x | Switching cycle lock on | +| compressorIdleTime | Number:Time | x | Compressor Idle time | +| heatingControllerMoreTime | Number:Time | x | Heating controller More time | +| heatingControllerLessTime | Number:Time | x | Heating controller Less time | +| runtimeCurrentThermalDisinfection | Number:Time | x | Thermal disinfection running since | +| timeHotWaterLock | Number:Time | x | Hot water lock | +| bivalenceStage | Number | x | Bivalence stage | +| operatingStatus | Number | | Operating status | +| errorTime0 | DateTime | | Timestamp error 0 in memory | +| errorTime1 | DateTime | x | Timestamp error 1 in memory | +| errorTime2 | DateTime | x | Timestamp error 2 in memory | +| errorTime3 | DateTime | x | Timestamp error 3 in memory | +| errorTime4 | DateTime | x | Timestamp error 4 in memory | +| errorCode0 | Number | | Error code Error 0 in memory | +| errorCode1 | Number | x | Error code Error 1 in memory | +| errorCode2 | Number | x | Error code Error 2 in memory | +| errorCode3 | Number | x | Error code Error 3 in memory | +| errorCode4 | Number | x | Error code Error 4 in memory | +| errorCountInMemory | Number | x | Number of errors in memory | +| shutdownReason0 | Number | | Reason shutdown 0 in memory | +| shutdownReason1 | Number | x | Reason shutdown 1 in memory | +| shutdownReason2 | Number | x | Reason shutdown 2 in memory | +| shutdownReason3 | Number | x | Reason shutdown 3 in memory | +| shutdownReason4 | Number | x | Reason shutdown 4 in memory | +| shutdownTime0 | DateTime | | Timestamp shutdown 0 in memory | +| shutdownTime1 | DateTime | x | Timestamp shutdown 1 in memory | +| shutdownTime2 | DateTime | x | Timestamp shutdown 2 in memory | +| shutdownTime3 | DateTime | x | Timestamp shutdown 3 in memory | +| shutdownTime4 | DateTime | x | Timestamp shutdown 4 in memory | +| comfortBoardInstalled | Switch | x | Comfort board installed | +| menuStateFull | String | | Status (complete) | +| menuStateLine1 | Number | | Status line 1 | +| menuStateLine2 | Number | x | Status line 2 | +| menuStateLine3 | Number | x | Status line 3 | +| menuStateTime | Number:Time | x | Status Time | +| bakeoutProgramStage | Number | x | Stage bakeout program | +| bakeoutProgramTemperature | Number:Temperature | x | Temperature bakeout program | +| bakeoutProgramTime | Number:Time | x | Runtime bakeout program | +| iconHotWater | Switch | x | DHW active/inactive icon | +| iconHeater | Number | x | Heater icon | +| iconMixingCircuit1 | Number | x | Mixing circuit 1 icon | +| iconMixingCircuit2 | Number | x | Mixing circuit 2 icon | +| shortProgramSetting | Number | x | Short program setting | +| statusSlave1 | Number | x | Status Slave 1 | +| statusSlave2 | Number | x | Status Slave 2 | +| statusSlave3 | Number | x | Status Slave 3 | +| statusSlave4 | Number | x | Status Slave 4 | +| statusSlave5 | Number | x | Status Slave 5 | +| currentTimestamp | DateTime | x | Current time of the heat pump | +| iconMixingCircuit3 | Number | x | Mixing circuit 3 icon | +| temperatureMixingCircuit3FlowTarget | Number:Temperature | x | Mixing circuit 3 Flow set temperature | +| temperatureMixingCircuit3Flow | Number:Temperature | x | Mixing circuit 3 Flow temperature | +| outputMixingCircuit3Close | Switch | x | Output "Mixing circuit 3 Closed" | +| outputMixingCircuit3Open | Switch | x | Output "Mixing circuit 3 Up" | +| outputMixingCircuitPump3 | Switch | x | Pump mixing circuit 3 | +| timeUntilDefrost | Number:Time | x | Time until defrost | +| temperatureRoomStation2 | Number:Temperature | x | Room temperature room station 2 | +| temperatureRoomStation3 | Number:Temperature | x | Room temperature room station 3 | +| iconTimeSwitchSwimmingPool | Number | x | Time switch swimming pool icon | +| runtimeTotalSwimmingPool | Number:Time | x | Operation hours swimming pool | +| coolingRelease | Switch | x | Release cooling | +| inputAnalog | Number:ElectricPotential | x | Analog input signal | +| iconCirculationPump | Number | x | Circulation pumps icon | +| heatMeterHeating | Number:Energy | | Heat meter heating | +| heatMeterHotWater | Number:Energy | | Heat meter domestic water | +| heatMeterSwimmingPool | Number:Energy | | Heat meter swimming pool | +| heatMeterTotalSinceReset | Number:Energy | | Total heat meter | +| heatMeterFlowRate | Number:VolumetricFlowRate | | Heat meter flow rate | +| outputAnalog1 | Number:ElectricPotential | x | Analog output 1 | +| outputAnalog2 | Number:ElectricPotential | x | Analog output 2 | +| timeLockSecondHotGasCompressor | Number:Time | x | Lock second compressor hot gas | +| temperatureSupplyAir | Number:Temperature | x | Supply air temperature | +| temperatureExhaustAir | Number:Temperature | x | Exhaust air temperature | +| runtimeTotalSolar | Number:Time | x | Operating hours solar | +| outputAnalog3 | Number:ElectricPotential | x | Analog output 3 | +| outputAnalog4 | Number:ElectricPotential | x | Analog output 4 | +| outputSupplyAirFan | Number:ElectricPotential | x | Supply air fan (defrost function) | +| outputExhaustFan | Number:ElectricPotential | x | Exhaust fan | +| outputVSK | Switch | x | Output VSK | +| outputFRH | Switch | x | Output FRH | +| inputAnalog2 | Number:ElectricPotential | x | Analog input 2 | +| inputAnalog3 | Number:ElectricPotential | x | Analog input 3 | +| inputSAX | Switch | x | Input SAX | +| inputSPL | Switch | x | Input SPL | +| ventilationBoardInstalled | Switch | x | Ventilation board installed | +| flowRateHeatSource | Number:VolumetricFlowRate | x | Flow rate heat source | +| linBusInstalled | Switch | x | LIN BUS installed | +| temperatureSuctionEvaporator | Number:Temperature | x | Temperature suction evaporator | +| temperatureSuctionCompressor | Number:Temperature | x | Temperature suction compressor | +| temperatureCompressorHeating | Number:Temperature | x | Temperature compressor heating | +| temperatureOverheating | Number:Temperature | x | Overheating | +| temperatureOverheatingTarget | Number:Temperature | x | Overheating target | +| highPressure | Number:Pressure | x | High pressure | +| lowPressure | Number:Pressure | x | Low pressure | +| outputCompressorHeating | Switch | x | Output compressor heating | +| controlSignalCirculatingPump | Number:Energy | x | Control signal circulating pump | +| fanSpeed | Number | x | Fan speed | +| temperatureSafetyLimitFloorHeating | Switch | x | Safety temperature limiter floor heating | +| powerTargetValue | Number:Energy | x | Power target value | +| powerActualValue | Number:Energy | x | Power actual value | +| temperatureFlowTarget | Number:Temperature | x | Temperature flow set point | +| operatingStatusSECBoard | Number | x | SEC Board operating status | +| fourWayValve | Number | x | Four-way valve | +| compressorSpeed | Number | x | Compressor speed | +| temperatureCompressorEVI | Number:Temperature | x | Compressor temperature EVI (Enhanced Vapour Injection) | +| temperatureIntakeEVI | Number:Temperature | x | Intake temperature EVI | +| temperatureOverheatingEVI | Number:Temperature | x | Overheating EVI | +| temperatureOverheatingTargetEVI | Number:Temperature | x | Overheating EVI target | +| temperatureCondensation | Number:Temperature | x | Condensation temperature | +| temperatureLiquidEEV | Number:Temperature | x | Liquid temperature EEV (electronic expansion valve) | +| temperatureHypothermiaEEV | Number:Temperature | x | Hypothermia EEV | +| pressureEVI | Number:Pressure | x | Pressure EVI | +| voltageInverter | Number:ElectricPotential | x | Voltage inverter | +| temperatureHotGas2 | Number:Temperature | x | Hot gas temperature sensor 2 | +| temperatureHeatSourceInlet2 | Number:Temperature | x | Temperature sensor heat source inlet 2 | +| temperatureIntakeEvaporator2 | Number:Temperature | x | Intake temperature evaporator 2 | +| temperatureIntakeCompressor2 | Number:Temperature | x | Intake temperature compressor 2 | +| temperatureCompressor2Heating | Number:Temperature | x | Temperature compressor 2 heating | +| temperatureOverheating2 | Number:Temperature | x | Overheating 2 | +| temperatureOverheatingTarget2 | Number:Temperature | x | Overheating target 2 | +| highPressure2 | Number:Pressure | x | High pressure 2 | +| lowPressure2 | Number:Pressure | x | Low pressure 2 | +| inputSwitchHighPressure2 | Switch | x | Input pressure switch high pressure 2 | +| outputDefrostValve2 | Switch | x | Output defrost valve 2 | +| outputVBO2 | Switch | x | Output brine pump/fan 2 | +| outputCompressor1_2 | Switch | x | Compressor output 1 / 2 | +| outputCompressorHeating2 | Switch | x | Compressor output heating 2 | +| secondShutdownReason0 | Number | x | Reason shutdown 0 in memory | +| secondShutdownReason1 | Number | x | Reason shutdown 1 in memory | +| secondShutdownReason2 | Number | x | Reason shutdown 2 in memory | +| secondShutdownReason3 | Number | x | Reason shutdown 3 in memory | +| secondShutdownReason4 | Number | x | Reason shutdown 4 in memory | +| secondShutdownTime0 | DateTime | x | Timestamp shutdown 0 in memory | +| secondShutdownTime1 | DateTime | x | Timestamp shutdown 1 in memory | +| secondShutdownTime2 | DateTime | x | Timestamp shutdown 2 in memory | +| secondShutdownTime3 | DateTime | x | Timestamp shutdown 3 in memory | +| secondShutdownTime4 | DateTime | x | Timestamp shutdown 4 in memory | +| temperatureRoom | Number:Temperature | x | Room temperature actual value | +| temperatureRoomTarget | Number:Temperature | x | Room temperature set point | +| temperatureHotWaterTop | Number:Temperature | x | Temperature domestic water top | +| frequencyCompressor | Number:Frequency | x | Compressor frequency | + +The following channels are also writable: +| channel | type | advanced | description | +|----------|--------|----------|------------------------------| +| temperatureHeatingParallelShift | Number:Temperature | | Heating temperature (parallel shift) | +| temperatureTargetHotWater_2 | Number:Temperature | | Hot water temperature | +| heatingMode | Number | | Heating mode | +| hotWaterMode | Number | | Hot water operating mode | +| thermalDisinfectionMonday | Switch | x | Thermal disinfection (Monday) | +| thermalDisinfectionTuesday | Switch | x | Thermal disinfection (Tuesday) | +| thermalDisinfectionWednesday | Switch | x | Thermal disinfection (Wednesday) | +| thermalDisinfectionThursday | Switch | x | Thermal disinfection (Thursday) | +| thermalDisinfectionFriday | Switch | x | Thermal disinfection (Friday) | +| thermalDisinfectionSaturday | Switch | x | Thermal disinfection (Saturday) | +| thermalDisinfectionSunday | Switch | x | Thermal disinfection (Sunday) | +| thermalDisinfectionPermanent | Switch | x | Thermal disinfection (Permanent) | +| comfortCoolingMode | Number | | Comfort cooling mode | +| temperatureComfortCoolingATRelease | Number:Temperature | | Comfort cooling AT release | +| temperatureComfortCoolingATReleaseTarget | Number:Temperature | | Comfort cooling AT release target | +| comfortCoolingATExcess | Number:Time | | AT Excess | +| comfortCoolingATUndercut | Number:Time | | AT undercut | + + +## Example + +Below you can find some example textual configuration for a heatpump with some basic functionallity. This can be extended/adjusted according to your needs and depending on the availability of channels (see list above). + +_heatpump.things:_ + +``` +Thing luxtronikheatpump:heatpump:heatpump "Heatpump" [ + ipAddress="192.168.178.12", + port="8889", + refresh="300" +] +``` + +_heatpump.items:_ + +``` +Group gHeatpump "Heatpump" + +Number:Temperature HeatPump_Temp_Outside "Temperature outside [%.1f °C]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:temperatureOutside" } +Number:Temperature HeatPump_Temp_Outside_Avg "Avg. temperature outside [%.1f °C]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:temperatureOutsideMean" } + +Number:Time HeatPump_Hours_Heatpump "Operating hours [%d h]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:runtimeTotalHeatPump" } +Number:Time HeatPump_Hours_Heating "Operating hours heating [%d h]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:runtimeTotalHeating" } +Number:Time HeatPump_Hours_Warmwater "Operating hours hot water [%d h]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:runtimeTotalHotWater" } + +String HeatPump_State_Ext "State [%s]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:menuStateFull" } + +Number HeatPump_heating_operation_mode "Heating operation mode [%s]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:heatingMode" } +Number HeatPump_heating_temperature "Heating temperature [%.1f]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:temperatureHeatingParallelShift" } +Number HeatPump_warmwater_operation_mode "Hot water operation mode [%s]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:hotWaterMode" } +Number HeatPump_warmwater_temperature "Hot water temperature [%.1f]" (gHeatpump) { channel="luxtronikheatpump:heatpump:heatpump:temperatureHotWaterTarget" } +``` + +_heatpump.sitemap:_ + +``` +sitemap heatpump label="Heatpump" { + Frame label="Heatpump" { + Text item=HeatPump_State_Ext + Text item=HeatPump_Temperature_1 + Text item=HeatPump_Outside_Avg + Text item=HeatPump_Hours_Heatpump + Text item=HeatPump_Hours_Heating + Text item=HeatPump_Hours_Warmwater + Switch item=HeatPump_heating_operation_mode mappings=[0="Auto", 1="Auxiliary heater", 2="Party", 3="Holiday", 4="Off"] + Setpoint item=HeatPump_heating_temperature minValue=-10 maxValue=10 step=0.5 + Switch item=HeatPump_warmwater_operation_mode mappings=[0="Auto", 1="Auxiliary heater", 2="Party", 3="Holiday", 4="Off"] + Setpoint item=HeatPump_warmwater_temperature minValue=10 maxValue=65 step=1 + } +} +``` + +## Development Notes + +This binding was initially based on the [Novelan/Luxtronik Heat Pump Binding](https://v2.openhab.org/addons/bindings/novelanheatpump1/) for openHAB 1. + +Luxtronik control units have an internal webserver which serves a Java applet. This applet can be used to configure some parts of the heat pump. The applet itselves uses a socket connection to fetch and send data to the heatpump. +This socket is also used by this binding. To get some more information on how this socket works you can check out other Luxtronik tools like [Luxtronik2 for NodeJS](https://github.com/coolchip/luxtronik2). + +A detailed parameter descriptions for the Java Webinterface can be found in the [Loxwiki](https://www.loxwiki.eu/display/LOX/Java+Webinterface) diff --git a/bundles/org.openhab.binding.luxtronikheatpump/pom.xml b/bundles/org.openhab.binding.luxtronikheatpump/pom.xml new file mode 100644 index 0000000000000..f48eeaaa99f8a --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.luxtronikheatpump + + openHAB Add-ons :: Bundles :: Luxtronik Heatpump Binding + + diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/feature/feature.xml b/bundles/org.openhab.binding.luxtronikheatpump/src/main/feature/feature.xml new file mode 100644 index 0000000000000..fcd8ee58cf365 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.luxtronikheatpump/${project.version} + + diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/ChannelUpdaterJob.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/ChannelUpdaterJob.java new file mode 100644 index 0000000000000..7bca5ae90aad5 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/ChannelUpdaterJob.java @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import java.io.IOException; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.ZoneId; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpChannel; +import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpType; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.DateTimeItem; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.SwitchItem; +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.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.scheduler.SchedulerRunnable; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ChannelUpdaterJob} updates all channel values + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class ChannelUpdaterJob implements SchedulerRunnable, Runnable { + + private final Thing thing; + private final LuxtronikHeatpumpConfiguration config; + private final LuxtronikTranslationProvider translationProvider; + private final Logger logger = LoggerFactory.getLogger(ChannelUpdaterJob.class); + private final LuxtronikHeatpumpHandler handler; + + public ChannelUpdaterJob(LuxtronikHeatpumpHandler handler, LuxtronikTranslationProvider translationProvider) { + this.translationProvider = translationProvider; + this.handler = handler; + this.thing = handler.getThing(); + this.config = this.thing.getConfiguration().as(LuxtronikHeatpumpConfiguration.class); + } + + public Thing getThing() { + return thing; + } + + @Override + public void run() { + // connect to heatpump and check if values can be fetched + final HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port); + + try { + connector.read(); + } catch (IOException e) { + logger.warn("Could not connect to heatpump (uuid={}, ip={}, port={}): {}", thing.getUID(), config.ipAddress, + config.port, e.getMessage()); + + handler.setStatusConnectionError(); + return; + } + + handler.setStatusOnline(); + + // read all available values + Integer[] heatpumpValues = connector.getValues(); + + // read all parameters + Integer[] heatpumpParams = connector.getParams(); + Integer[] heatpumpVisibilities = connector.getVisibilities(); + + for (HeatpumpChannel channel : HeatpumpChannel.values()) { + try { + Integer rawValue = getChannelValue(channel, heatpumpValues, heatpumpParams, heatpumpVisibilities); + + if (rawValue == null) { + continue; + } + + State value = convertValueToState(rawValue, channel.getItemClass(), channel.getUnit()); + + if (value != null) { + handleEventType(value, channel); + } + } catch (Exception e) { + // an exception should actually not occur, but is catched nevertheless in case it does + // this ensures the remaining channels are updated even if one fails for some reason + logger.warn("An error occurred while updating the channel {}: {}", channel.getCommand(), + e.getMessage()); + } + } + + setExtendedState(heatpumpValues, heatpumpParams, heatpumpVisibilities); + + updateProperties(heatpumpValues); + } + + private @Nullable State convertValueToState(Integer rawValue, Class itemClass, + @Nullable Unit unit) { + if (itemClass == SwitchItem.class) { + return (rawValue == 0) ? OnOffType.OFF : OnOffType.ON; + } + + if (itemClass == DateTimeItem.class && rawValue > 0) { + try { + Instant instant = Instant.ofEpochSecond(rawValue.longValue()); + return new DateTimeType(instant.atZone(ZoneId.of("UTC"))); + } catch (DateTimeException e) { + logger.warn("Invalid timestamp '{}' received from heatpump: {}", rawValue, e.getMessage()); + } + } + + if (itemClass == NumberItem.class) { + if (unit == null) { + return new DecimalType(rawValue); + } + if (unit == SIUnits.CELSIUS || unit == Units.KELVIN || unit == Units.KILOWATT_HOUR || unit == Units.PERCENT + || unit == Units.HOUR) { + return new QuantityType<>((double) rawValue / 10, unit); + } else if (unit == Units.HERTZ || unit == Units.SECOND) { + return new QuantityType<>((double) rawValue, unit); + } else if (unit == Units.LITRE_PER_MINUTE) { + return new QuantityType<>((double) rawValue / 60, unit); + } else if (unit == Units.BAR || unit == Units.VOLT) { + return new QuantityType<>((double) rawValue / 100, unit); + } + + logger.debug("Unhandled unit {} configured for a channel.", unit); + return new DecimalType(rawValue); + } + + return null; + } + + private @Nullable Integer getChannelValue(HeatpumpChannel channel, Integer[] heatpumpValues, + Integer[] heatpumpParams, Integer[] heatpumpVisibilities) { + Integer channelId = channel.getChannelId(); + + if (channelId == null) { + return null; // no channel id to read defined (for channels handeled separatly) + } + + if (!channel.isVisible(heatpumpVisibilities) && config.showAllChannels) { + logger.debug("Channel {} is not available. Skipped updating it", channel.getCommand()); + return null; + } + + Integer rawValue = null; + + if (channel.isWritable()) { + rawValue = heatpumpParams[channelId]; + } else { + if (heatpumpValues.length <= channelId) { + return null; // channel not available + } + rawValue = heatpumpValues[channelId]; + } + + return rawValue; + } + + private String getSoftwareVersion(Integer[] heatpumpValues) { + StringBuffer softwareVersion = new StringBuffer(""); + + for (int i = 81; i <= 90; i++) { + if (heatpumpValues[i] > 0) { + softwareVersion.append(Character.toString(heatpumpValues[i])); + } + } + + return softwareVersion.toString(); + } + + private String transformIpAddress(int ip) { + return String.format("%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF); + } + + private void handleEventType(State state, HeatpumpChannel heatpumpCommandType) { + handler.updateState(heatpumpCommandType.getCommand(), state); + } + + private void setExtendedState(Integer[] heatpumpValues, Integer[] heatpumpParams, Integer[] heatpumpVisibilities) { + Integer row1 = getChannelValue(HeatpumpChannel.CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE1, heatpumpValues, + heatpumpParams, heatpumpVisibilities); + Integer error = getChannelValue(HeatpumpChannel.CHANNEL_HEATPUMP_ERROR_NR0, heatpumpValues, heatpumpParams, + heatpumpVisibilities); + Integer row2 = getChannelValue(HeatpumpChannel.CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE2, heatpumpValues, + heatpumpParams, heatpumpVisibilities); + Integer row3 = getChannelValue(HeatpumpChannel.CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE3, heatpumpValues, + heatpumpParams, heatpumpVisibilities); + Integer time = getChannelValue(HeatpumpChannel.CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEIT, heatpumpValues, + heatpumpParams, heatpumpVisibilities); + String state = ""; + + if (row1 != null && row1 == 3) { + // 3 means error state + state = getStateTranslation("errorCodeX", error); + } else { + state = getStateTranslation("menuStateLine1", row1); + } + + var longState = String.format("%s - %s %s - %s", state, getStateTranslation("menuStateLine2", row2), + formatHours(time), getStateTranslation("menuStateLine3", row3)); + + handleEventType((State) new StringType(longState), HeatpumpChannel.CHANNEL_HEATPUMP_STATUS); + } + + private void updateProperties(Integer[] heatpumpValues) { + String heatpumpType = HeatpumpType.fromCode(heatpumpValues[78]).getName(); + + setProperty("heatpumpType", heatpumpType); + + // Not sure when Typ 2 should be used + // String heatpumpType2 = HeatpumpType.fromCode(heatpumpValues[230]).getName(); + // setProperty("heatpumpType2", heatpumpType2); + + setProperty("softwareVersion", getSoftwareVersion(heatpumpValues)); + setProperty("ipAddress", transformIpAddress(heatpumpValues[91])); + setProperty("subnetMask", transformIpAddress(heatpumpValues[92])); + setProperty("broadcastAddress", transformIpAddress(heatpumpValues[93])); + setProperty("gateway", transformIpAddress(heatpumpValues[94])); + } + + private String getStateTranslation(String name, @Nullable Integer option) { + if (option == null) { + return ""; + } + + String translation = translationProvider + .getText("channel-type.luxtronikheatpump." + name + ".state.option." + option); + return translation == null ? "" : translation; + } + + private void setProperty(String name, String value) { + handler.updateProperty(name, value); + } + + private String formatHours(@Nullable Integer value) { + String returnValue = ""; + + if (value == null) { + return returnValue; + } + + int intVal = value; + + returnValue += String.format("%02d:", intVal / 3600); + intVal %= 3600; + returnValue += String.format("%02d:", intVal / 60); + intVal %= 60; + returnValue += String.format("%02d", intVal); + return returnValue; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/HeatpumpConnector.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/HeatpumpConnector.java new file mode 100644 index 0000000000000..a3ed82e643361 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/HeatpumpConnector.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HeatpumpConnector} reads / writes internal states of a Heat pump with Luxtronik control. + * + * Based on HeatpumpConnector class of novelanheatpump binding + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class HeatpumpConnector { + + private static final int SOCKET_PARAM_WRITE_PARAMS = 3002; + private static final int SOCKET_PARAM_READ_PARAMS = 3003; + private static final int SOCKET_PARAM_READ_VALUES = 3004; + private static final int SOCKET_PARAM_READ_VISIBILITIES = 3005; + + private final Logger logger = LoggerFactory.getLogger(HeatpumpConnector.class); + + private String serverIp; + private int serverPort; + private Integer[] heatpumpValues = new Integer[0]; + private Integer[] heatpumpParams = new Integer[0]; + private Integer[] heatpumpVisibilities = new Integer[0]; + + public HeatpumpConnector(String serverIp, int serverPort) { + this.serverIp = serverIp; + this.serverPort = serverPort; + } + + /** + * reads all values from the heatpump via network + * + * @throws UnknownHostException indicate that the IP address of a host could not be determined. + * @throws IOException indicate that no data can be read from the heat pump + */ + public void read() throws UnknownHostException, IOException { + try (Socket sock = new Socket(serverIp, serverPort)) { + InputStream in = sock.getInputStream(); + OutputStream out = sock.getOutputStream(); + DataInputStream datain = new DataInputStream(in); + DataOutputStream dataout = new DataOutputStream(out); + + heatpumpValues = readInt(datain, dataout, SOCKET_PARAM_READ_VALUES); + + // workaround for thermal energies + // the thermal energies can be unreasonably high in some cases, probably due to a sign bug in the firmware + // trying to correct this issue here + for (int i = 151; i <= 154; i++) { + if (heatpumpValues[i] >= 214748364) { + heatpumpValues[i] -= 214748364; + } + } + + heatpumpParams = readInt(datain, dataout, SOCKET_PARAM_READ_PARAMS); + heatpumpVisibilities = readInt(datain, dataout, SOCKET_PARAM_READ_VISIBILITIES); + + datain.close(); + dataout.close(); + } + } + + /** + * read the parameters of the heat pump + */ + public Integer[] getParams() { + return heatpumpParams; + } + + /** + * set a parameter of the heat pump + * + * @param param + * @param value + * @throws IOException indicate that no data can be sent to the heat pump + */ + public boolean setParam(int param, int value) throws IOException { + try (Socket sock = new Socket(serverIp, serverPort)) { + InputStream in = sock.getInputStream(); + OutputStream out = sock.getOutputStream(); + DataInputStream datain = new DataInputStream(in); + DataOutputStream dataout = new DataOutputStream(out); + + while (datain.available() > 0) { + datain.readByte(); + } + + // write command, param and value to heatpump socket + dataout.writeInt(SOCKET_PARAM_WRITE_PARAMS); + dataout.writeInt(param); + dataout.writeInt(value); + dataout.flush(); + + // first integer on socket output should represent the command + int cmd = datain.readInt(); + datain.readInt(); + + datain.close(); + dataout.close(); + + if (cmd != SOCKET_PARAM_WRITE_PARAMS) { + logger.debug("Couldn't write parameter {} with value {} to heat pump.", param, value); + return false; + } else { + logger.debug("Parameter {} with value {} successfully written to heat pump.", param, value); + return true; + } + } + } + + /** + * Returns the internal states of the heat pump + * + * @return a array with all internal data of the heat pump + */ + public Integer[] getValues() { + return heatpumpValues; + } + + /** + * Returns the internal visibilities of the heat pump + * + * @return a array with all internal visibilities of the heat pump + */ + public Integer[] getVisibilities() { + return heatpumpVisibilities; + } + + /** + * Reads all available values for the given parameter from socket + * + * @param datain data input stream of socket connection + * @param dataout data output stream of socket connection + * @param parameter int command to read from socket + * @return an array with all values returned from heat pump socket + * @throws IOException indicate that no data can be read from the heat pump + */ + private Integer[] readInt(DataInputStream datain, DataOutputStream dataout, int parameter) throws IOException { + Integer[] result = null; + while (datain.available() > 0) { + datain.readByte(); + } + + // to receive values we first need to write the command followed by four 0 byte values to the socket + dataout.writeInt(parameter); + dataout.writeInt(0); + dataout.flush(); + + // the first integer received from socket should match the written command + if (datain.readInt() != parameter) { + return new Integer[0]; + } + + if (parameter == SOCKET_PARAM_READ_VALUES) { + // when reading values the next integer represents some kind of status + datain.readInt(); + } + + // the next integer value should define the number of values that are following + // Currently the list or parameters is the longest list and contains around 1050 values + // To avoid possible (memory) problems in case the returned number would be unexpected high we limit it to 2000 + int arraylength = Integer.min(datain.readInt(), 2000); + + logger.debug("Found {} values for {}", arraylength, parameter); + + result = new Integer[arraylength]; + + // Note: the visibility params are returned as single byte values + // probably as the are used as boolean values (0/1) only + if (parameter == SOCKET_PARAM_READ_VISIBILITIES) { + byte[] data = datain.readNBytes(arraylength); + for (int i = 0; i < data.length; i++) { + result[i] = (int) data[i]; + } + return result; + } + + for (int i = 0; i < arraylength; i++) { + result[i] = datain.readInt(); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpBindingConstants.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpBindingConstants.java new file mode 100644 index 0000000000000..d9e9286d03bd1 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpBindingConstants.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link LuxtronikHeatpumpBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class LuxtronikHeatpumpBindingConstants { + + public static final String BINDING_ID = "luxtronikheatpump"; + + public static final ThingTypeUID THING_TYPE_HEATPUMP = new ThingTypeUID(BINDING_ID, "heatpump"); +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpConfiguration.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpConfiguration.java new file mode 100644 index 0000000000000..8d00ca0b8fbdc --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LuxtronikHeatpumpConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class LuxtronikHeatpumpConfiguration { + + public String ipAddress = ""; + public int port = 8889; + public int refresh = 60000; + public boolean showAllChannels = false; + + public boolean isValid() { + return !ipAddress.isEmpty() && port > 0 && refresh > 0; + } + + @Override + public String toString() { + return new StringBuilder().append("[IP=").append(ipAddress).append(",port=").append(port).append(",refresh=") + .append(refresh).append(",showAllChannels=").append(showAllChannels).append("]").toString(); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandler.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandler.java new file mode 100644 index 0000000000000..281d5920c3167 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandler.java @@ -0,0 +1,335 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpChannel; +import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpCoolingOperationMode; +import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpOperationMode; +import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidChannelException; +import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidOperationModeException; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Channel; +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.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +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 LuxtronikHeatpumpHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class LuxtronikHeatpumpHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(LuxtronikHeatpumpHandler.class); + private final Set> scheduledFutures = new HashSet<>(); + private static final int RETRY_INTERVAL_SEC = 60; + private boolean tiggerChannelUpdate = false; + private final LuxtronikTranslationProvider translationProvider; + private LuxtronikHeatpumpConfiguration config; + + public LuxtronikHeatpumpHandler(Thing thing, LuxtronikTranslationProvider translationProvider) { + super(thing); + this.translationProvider = translationProvider; + config = new LuxtronikHeatpumpConfiguration(); + } + + @Override + public void updateState(String channelID, State state) { + super.updateState(channelID, state); + } + + @Override + public void updateProperty(String name, String value) { + super.updateProperty(name, value); + } + + public void setStatusConnectionError() { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "couldn't establish network connection [host '" + config.ipAddress + "']"); + } + + public void setStatusOnline() { + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channelId = channelUID.getIdWithoutGroup(); + logger.debug("Handle command '{}' for channel {}", command, channelId); + if (command == RefreshType.REFRESH) { + // ignore resresh command as channels will be updated automatically + return; + } + + HeatpumpChannel channel; + + try { + channel = HeatpumpChannel.fromString(channelId); + } catch (InvalidChannelException e) { + logger.debug("Channel '{}' could not be found for thing {}", channelId, thing.getUID()); + return; + } + + if (!channel.isWritable()) { + logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command); + return; + } + + if (command instanceof QuantityType) { + QuantityType value = (QuantityType) command; + + Unit unit = channel.getUnit(); + if (unit != null) { + value = value.toUnit(unit); + } + + command = new DecimalType(value.floatValue()); + } + + if (command instanceof OnOffType) { + command = ((OnOffType) command) == OnOffType.ON ? new DecimalType(1) : DecimalType.ZERO; + } + + if (!(command instanceof DecimalType)) { + logger.warn("Heatpump operation for item {} must be from type: {}. Received {}", channel.getCommand(), + DecimalType.class.getSimpleName(), command.getClass()); + return; + } + + Integer param = channel.getChannelId(); + Integer value = null; + + switch (channel) { + case CHANNEL_EINST_BWTDI_AKT_MO: + case CHANNEL_EINST_BWTDI_AKT_DI: + case CHANNEL_EINST_BWTDI_AKT_MI: + case CHANNEL_EINST_BWTDI_AKT_DO: + case CHANNEL_EINST_BWTDI_AKT_FR: + case CHANNEL_EINST_BWTDI_AKT_SA: + case CHANNEL_EINST_BWTDI_AKT_SO: + case CHANNEL_EINST_BWTDI_AKT_AL: + value = ((DecimalType) command).intValue(); + break; + case CHANNEL_BA_HZ_AKT: + case CHANNEL_BA_BW_AKT: + value = ((DecimalType) command).intValue(); + try { + // validate the value is valid + HeatpumpOperationMode.fromValue(value); + } catch (InvalidOperationModeException e) { + logger.warn("Heatpump {} mode recevieved invalid value {}: {}", channel.getCommand(), value, + e.getMessage()); + return; + } + break; + case CHANNEL_EINST_WK_AKT: + case CHANNEL_EINST_BWS_AKT: + case CHANNEL_EINST_KUCFTL_AKT: + case CHANNEL_SOLLWERT_KUCFTL_AKT: + float temperature = ((DecimalType) command).floatValue(); + value = (int) (temperature * 10); + break; + case CHANNEL_EINST_BWSTYP_AKT: + value = ((DecimalType) command).intValue(); + try { + // validate the value is valid + HeatpumpCoolingOperationMode.fromValue(value); + } catch (InvalidOperationModeException e) { + logger.warn("Heatpump {} mode recevieved invalid value {}: {}", channel.getCommand(), value, + e.getMessage()); + return; + } + break; + case CHANNEL_EINST_KUHL_ZEIT_EIN_AKT: + case CHANNEL_EINST_KUHL_ZEIT_AUS_AKT: + float hours = ((DecimalType) command).floatValue(); + value = (int) (hours * 10); + break; + + default: + logger.debug("Received unknown channel {}", channelId); + break; + } + + if (param != null && value != null) { + if (sendParamToHeatpump(param, value)) { + logger.debug("Heat pump mode {} set to {}.", channel.getCommand(), value); + } else { + logger.warn("Failed setting heat pump mode {} to {}", channel.getCommand(), value); + } + } else { + logger.warn("No valid value given for Heatpump operation {}", channel.getCommand()); + } + } + + @Override + public void initialize() { + config = getConfigAs(LuxtronikHeatpumpConfiguration.class); + + if (!config.isValid()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "At least one mandatory configuration field is empty"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + synchronized (scheduledFutures) { + ScheduledFuture future = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, + RETRY_INTERVAL_SEC, TimeUnit.SECONDS); + scheduledFutures.add(future); + } + } + + private void internalInitialize() { + // connect to heatpump and check if values can be fetched + HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port); + + try { + connector.read(); + } catch (IOException e) { + setStatusConnectionError(); + return; + } + + // stop trying to establish a connection for initializing the thing once it was established + stopJobs(); + + // When thing is initialized the first time or and update was triggered, set the available channels + if (thing.getProperties().isEmpty() || tiggerChannelUpdate) { + updateChannels(connector); + } + + setStatusOnline(); + restartJobs(); + } + + @Override + protected void updateConfiguration(Configuration configuration) { + tiggerChannelUpdate = true; + super.updateConfiguration(configuration); + } + + @Override + public void dispose() { + stopJobs(); + } + + private void updateChannels(HeatpumpConnector connector) { + Integer[] visibilityValues = connector.getVisibilities(); + Integer[] heatpumpValues = connector.getValues(); + Integer[] heatpumpParams = connector.getParams(); + + logger.debug("Updating available channels for thing {}", thing.getUID()); + + final ThingHandlerCallback callback = getCallback(); + if (callback == null) { + logger.debug("ThingHandlerCallback is null. Skipping migration of last_update channel."); + return; + } + + ThingBuilder thingBuilder = editThing(); + List channelList = new ArrayList<>(); + + // clear channel list + thingBuilder.withoutChannels(thing.getChannels()); + + // create list with available channels + for (HeatpumpChannel channel : HeatpumpChannel.values()) { + Integer channelId = channel.getChannelId(); + int length = channel.isWritable() ? heatpumpParams.length : heatpumpValues.length; + ChannelUID channelUID = new ChannelUID(thing.getUID(), channel.getCommand()); + ChannelTypeUID channelTypeUID = new ChannelTypeUID(LuxtronikHeatpumpBindingConstants.BINDING_ID, + channel.getCommand()); + if ((channelId != null && length <= channelId) + || (config.showAllChannels == Boolean.FALSE && !channel.isVisible(visibilityValues))) { + logger.debug("Hiding channel {}", channel.getCommand()); + } else { + channelList.add(callback.createChannelBuilder(channelUID, channelTypeUID).build()); + } + } + + thingBuilder.withChannels(channelList); + + updateThing(thingBuilder.build()); + } + + private void restartJobs() { + stopJobs(); + + synchronized (scheduledFutures) { + if (getThing().getStatus() == ThingStatus.ONLINE) { + // Repeat channel update job every configured seconds + Runnable channelUpdaterJob = new ChannelUpdaterJob(this, translationProvider); + ScheduledFuture future = scheduler.scheduleWithFixedDelay(channelUpdaterJob, 0, config.refresh, + TimeUnit.SECONDS); + scheduledFutures.add(future); + } + } + } + + private void stopJobs() { + synchronized (scheduledFutures) { + for (ScheduledFuture future : scheduledFutures) { + if (!future.isDone()) { + future.cancel(true); + } + } + scheduledFutures.clear(); + } + } + + /** + * Set a parameter on the Luxtronik heatpump. + * + * @param param + * @param value + */ + private boolean sendParamToHeatpump(int param, int value) { + HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port); + + try { + return connector.setParam(param, value); + } catch (IOException e) { + setStatusConnectionError(); + } + + return false; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandlerFactory.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandlerFactory.java new file mode 100644 index 0000000000000..1cbf8118a68db --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikHeatpumpHandlerFactory.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import static org.openhab.binding.luxtronikheatpump.internal.LuxtronikHeatpumpBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link LuxtronikHeatpumpHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.luxtronikheatpump", service = ThingHandlerFactory.class) +public class LuxtronikHeatpumpHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HEATPUMP); + private final LuxtronikTranslationProvider translationProvider; + + @Activate + public LuxtronikHeatpumpHandlerFactory(final @Reference LocaleProvider localeProvider, + final @Reference TranslationProvider i18nProvider, ComponentContext componentContext) { + super.activate(componentContext); + this.translationProvider = new LuxtronikTranslationProvider(getBundleContext().getBundle(), i18nProvider, + localeProvider); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_HEATPUMP.equals(thingTypeUID)) { + return new LuxtronikHeatpumpHandler(thing, translationProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikTranslationProvider.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikTranslationProvider.java new file mode 100644 index 0000000000000..a62d7d1607139 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/LuxtronikTranslationProvider.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; + +/** + * {@link LuxtronikTranslationProvider} provides i18n message lookup + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class LuxtronikTranslationProvider { + + private final Bundle bundle; + private final TranslationProvider i18nProvider; + private final LocaleProvider localeProvider; + + public LuxtronikTranslationProvider(Bundle bundle, TranslationProvider i18nProvider, + LocaleProvider localeProvider) { + this.bundle = bundle; + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + public @Nullable String getText(String key, @Nullable Object... arguments) { + try { + Locale locale = localeProvider.getLocale(); + return i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments); + } catch (IllegalArgumentException e) { + return "Can't to load message for key " + key; + } + } + + public @Nullable String getDefaultText(String key) { + return i18nProvider.getText(bundle, key, key, Locale.ENGLISH); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpChannel.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpChannel.java new file mode 100644 index 0000000000000..440499b49d633 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpChannel.java @@ -0,0 +1,1385 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.enums; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidChannelException; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.DateTimeItem; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; + +/** + * Represents all valid channels which could be processed by this binding + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public enum HeatpumpChannel { + + // The constant names are currently based on the variable names used in the heat pumps internal JAVA applet + // for possible values see https://www.loxwiki.eu/display/LOX/Java+Webinterface + // or https://github.com/Bouni/Home-Assistant-Luxtronik/blob/master/data.txt + + /** + * Flow temperature heating circuit + * (original: Vorlauftemperatur Heizkreis) + */ + CHANNEL_TEMPERATUR_TVL(10, "temperatureHeatingCircuitFlow", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_VORLAUF), + + /** + * Return temperature heating circuit + * (original: Rücklauftemperatur Heizkreis) + */ + CHANNEL_TEMPERATUR_TRL(11, "temperatureHeatingCircuitReturn", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RUCKLAUF), + + /** + * Return set point heating circuit + * (original: Rücklauf-Soll Heizkreis) + */ + CHANNEL_SOLLWERT_TRL_HZ(12, "temperatureHeatingCircuitReturnTarget", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RL_SOLL), + + /** + * Return temperature in buffer tank + * (original: Rücklauftemperatur im Trennspeicher) + */ + CHANNEL_TEMPERATUR_TRL_EXT(13, "temperatureBufferTankReturn", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RUECKLEXT), + + /** + * Hot gas temperature + * (original: Heißgastemperatur) + */ + CHANNEL_TEMPERATUR_THG(14, "temperatureHotGas", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_HEISSGAS), + + /** + * Outside temperature + * (original: Außentemperatur) + */ + CHANNEL_TEMPERATUR_TA(15, "temperatureOutside", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_AUSSENT), + + /** + * Average temperature outside over 24 h (heating limit function) + * (original: Durchschnittstemperatur Außen über 24 h (Funktion Heizgrenze)) + */ + CHANNEL_MITTELTEMPERATUR(16, "temperatureOutsideMean", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_AUSSENT), + + /** + * Hot water actual temperature + * (original: Warmwasser Ist-Temperatur) + */ + CHANNEL_TEMPERATUR_TBW(17, "temperatureHotWater", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_BW_IST), + + /** + * Hot water target temperature + * (original: Warmwasser Soll-Temperatur) + */ + // Not needed as it duplicates writable param (2) + // CHANNEL_EINST_BWS_AKT(18, "temperatureHotWaterTarget", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Heat source inlet temperature + * (original: Wärmequellen-Eintrittstemperatur) + */ + CHANNEL_TEMPERATUR_TWE(19, "temperatureHeatSourceInlet", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_WQ_EIN), + + /** + * Heat source outlet temperature + * (original: Wärmequellen-Austrittstemperatur) + */ + CHANNEL_TEMPERATUR_TWA(20, "temperatureHeatSourceOutlet", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_WQ_EIN), + + /** + * Mixing circuit 1 Flow temperature + * (original: Mischkreis 1 Vorlauftemperatur) + */ + CHANNEL_TEMPERATUR_TFB1(21, "temperatureMixingCircuit1Flow", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK1_VORLAUF), + + /** + * Mixing circuit 1 Flow target temperature + * (original: Mischkreis 1 Vorlauf-Soll-Temperatur) + */ + CHANNEL_SOLLWERT_TVL_MK1(22, "temperatureMixingCircuit1FlowTarget", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK1VL_SOLL), + + /** + * Room temperature room station 1 + * (original: Raumtemperatur Raumstation 1) + */ + CHANNEL_TEMPERATUR_RFV(23, "temperatureRoomStation", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RAUMSTATION), + + /** + * Mixing circuit 2 Flow temperature + * (original: Mischkreis 2 Vorlauftemperatur) + */ + CHANNEL_TEMPERATUR_TFB2(24, "temperatureMixingCircuit2Flow", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK2_VORLAUF), + + /** + * Mixing circuit 2 Flow target temperature + * (original: Mischkreis 2 Vorlauf-Soll-Temperatur) + */ + CHANNEL_SOLLWERT_TVL_MK2(25, "temperatureMixingCircuit2FlowTarget", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK2VL_SOLL), + + /** + * Solar collector sensor + * (original: Fühler Solarkollektor) + */ + CHANNEL_TEMPERATUR_TSK(26, "temperatureSolarCollector", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_SOLARKOLL), + + /** + * Solar tank sensor + * (original: Fühler Solarspeicher) + */ + CHANNEL_TEMPERATUR_TSS(27, "temperatureSolarTank", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_SOLARSP), + + /** + * Sensor external energy source + * (original: Fühler externe Energiequelle) + */ + CHANNEL_TEMPERATUR_TEE(28, "temperatureExternalEnergySource", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_EXT_ENERG), + + /** + * Input "Defrost end, brine pressure, flow rate" + * (original: Eingang "Abtauende, Soledruck, Durchfluss") + */ + CHANNEL_ASDIN(29, "inputASD", SwitchItem.class, null, false, HeatpumpVisibility.IN_ASD), + + /** + * Input "Domestic hot water thermostat" + * (original: Eingang "Brauchwarmwasserthermostat") + */ + CHANNEL_BWTIN(30, "inputHotWaterThermostat", SwitchItem.class, null, false, HeatpumpVisibility.IN_BWT), + + /** + * Input "EVU lock" + * (original: Eingang "EVU Sperre") + */ + CHANNEL_EVUIN(31, "inputUtilityLock", SwitchItem.class, null, false, HeatpumpVisibility.IN_EVU), + + /** + * Input "High pressure cooling circuit" + * (original: Eingang "Hochdruck Kältekreis") + */ + CHANNEL_HDIN(32, "inputHighPressureCoolingCircuit", SwitchItem.class, null, false, HeatpumpVisibility.IN_HD), + + /** + * Input "Motor protection OK" + * (original: Eingang "Motorschutz OK") + */ + CHANNEL_MOTIN(33, "inputMotorProtectionOK", SwitchItem.class, null, false, HeatpumpVisibility.IN_MOT), + + /** + * Input "Low pressure" + * (original: Eingang "Niederdruck") + */ + CHANNEL_NDIN(34, "inputLowPressure", SwitchItem.class, null, false, HeatpumpVisibility.IN_ND), + + /** + * Input "Monitoring contact for potentiostat" + * (original: Eingang "Überwachungskontakt für Potentiostat") + */ + CHANNEL_PEXIN(35, "inputPEX", SwitchItem.class, null, false, HeatpumpVisibility.IN_PEX), + + /** + * Input "Swimming pool thermostat" + * (original: Eingang "Schwimmbadthermostat") + */ + CHANNEL_SWTIN(36, "inputSwimmingPoolThermostat", SwitchItem.class, null, false, HeatpumpVisibility.IN_SWT), + + /** + * Output "Defrost valve" + * (original: Ausgang "Abtauventil") + */ + CHANNEL_AVOUT(37, "outputDefrostValve", SwitchItem.class, null, false, HeatpumpVisibility.OUT_ABTAUVENTIL), + + /** + * Output "Domestic hot water pump/changeover valve" + * (original: Ausgang "Brauchwasserpumpe/Umstellventil") + */ + CHANNEL_BUPOUT(38, "outputBUP", SwitchItem.class, null, false, HeatpumpVisibility.OUT_BUP), + + /** + * Output "Heating circulation pump" + * (original: Ausgang "Heizungsumwälzpumpe") + */ + CHANNEL_HUPOUT(39, "outputHeatingCirculationPump", SwitchItem.class, null, false, HeatpumpVisibility.OUT_HUP), + + /** + * Output "Mixing circuit 1 Open" + * (original: Ausgang "Mischkreis 1 Auf") + */ + CHANNEL_MA1OUT(40, "outputMixingCircuit1Open", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER1AUF), + + /** + * Output "Mixing circuit 1 Closed" + * (original: Ausgang "Mischkreis 1 Zu") + */ + CHANNEL_MZ1OUT(41, "outputMixingCircuit1Closed", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER1ZU), + + /** + * Output "Ventilation" + * (original: Ausgang "Ventilation (Lüftung)") + */ + CHANNEL_VENOUT(42, "outputVentilation", SwitchItem.class, null, false, HeatpumpVisibility.OUT_VENTILATION), + + /** + * Output "Brine pump/fan" + * (original: Ausgang "Solepumpe/Ventilator") + */ + CHANNEL_VBOOUT(43, "outputVBO", SwitchItem.class, null, false, null), + + /** + * Output "Compressor 1" + * (original: Ausgang "Verdichter 1") + */ + CHANNEL_VD1OUT(44, "outputCompressor1", SwitchItem.class, null, false, HeatpumpVisibility.OUT_VERDICHTER1), + + /** + * Output "Compressor 2" + * (original: Ausgang "Verdichter 2") + */ + CHANNEL_VD2OUT(45, "outputCompressor2", SwitchItem.class, null, false, HeatpumpVisibility.OUT_VERDICHTER2), + + /** + * Output "Circulation pump" + * (original: Ausgang "Zirkulationspumpe") + */ + CHANNEL_ZIPOUT(46, "outputCirculationPump", SwitchItem.class, null, false, HeatpumpVisibility.OUT_ZIP), + + /** + * Output "Auxiliary circulation pump" + * (original: Ausgang "Zusatzumwälzpumpe") + */ + CHANNEL_ZUPOUT(47, "outputZUP", SwitchItem.class, null, false, HeatpumpVisibility.OUT_ZUP), + + /** + * Output "Control signal additional heating" + * (original: Ausgang "Steuersignal Zusatzheizung v. Heizung") + */ + CHANNEL_ZW1OUT(48, "outputControlSignalAdditionalHeating", SwitchItem.class, null, false, + HeatpumpVisibility.OUT_ZWE1), + + /** + * Output "Control signal additional heating/fault signal" + * (original: Ausgang "Steuersignal Zusatzheizung/Störsignal") + */ + CHANNEL_ZW2SSTOUT(49, "outputFaultSignalAdditionalHeating", SwitchItem.class, null, false, + HeatpumpVisibility.OUT_ZWE2_SST), + + /** + * Output "Auxiliary heater 3" + * (original: Ausgang "Zusatzheizung 3") + */ + CHANNEL_ZW3SSTOUT(50, "outputAuxiliaryHeater3", SwitchItem.class, null, false, HeatpumpVisibility.OUT_ZWE3), + + /** + * Output "Pump mixing circuit 2" + * (original: Ausgang "Pumpe Mischkreis 2") + */ + CHANNEL_FP2OUT(51, "outputMixingCircuitPump2", SwitchItem.class, null, false, HeatpumpVisibility.OUT_FUP2), + + /** + * Output "Solar charge pump" + * (original: Ausgang "Solarladepumpe") + */ + CHANNEL_SLPOUT(52, "outputSolarChargePump", SwitchItem.class, null, false, HeatpumpVisibility.OUT_SLP), + + /** + * Output "Swimming pool pump" + * (original: Ausgang "Schwimmbadpumpe") + */ + CHANNEL_OUTPUT_SUP(53, "outputSwimmingPoolPump", SwitchItem.class, null, false, HeatpumpVisibility.OUT_SUP), + + /** + * Output "Mixing circuit 2 Closed" + * (original: Ausgang "Mischkreis 2 Zu") + */ + CHANNEL_MZ2OUT(54, "outputMixingCircuit2Closed", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER2ZU), + + /** + * Output "Mixing circuit 2 Open" + * (original: Ausgang "Mischkreis 2 Auf") + */ + CHANNEL_MA2OUT(55, "outputMixingCircuit2Open", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER2AUF), + + /** + * Operating hours compressor 1 + * (original: Betriebsstunden Verdichter 1) + */ + CHANNEL_ZAEHLER_BETRZEITVD1(56, "runtimeTotalCompressor1", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDVD1), + + /** + * Pulses compressor 1 + * (original: Impulse Verdichter 1) + */ + CHANNEL_ZAEHLER_BETRZEITIMPVD1(57, "pulsesCompressor1", NumberItem.class, null, false, + HeatpumpVisibility.BST_IMPVD1), + + /** + * Operating hours compressor 2 + * (original: Betriebsstunden Verdichter 2) + */ + CHANNEL_ZAEHLER_BETRZEITVD2(58, "runtimeTotalCompressor2", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDVD2), + + /** + * Pulses compressor 2 + * (original: Impulse Verdichter 2) + */ + CHANNEL_ZAEHLER_BETRZEITIMPVD2(59, "pulsesCompressor2", NumberItem.class, null, false, + HeatpumpVisibility.BST_IMPVD2), + + /** + * Operating hours Second heat generator 1 + * (original: Betriebsstunden Zweiter Wärmeerzeuger 1) + */ + CHANNEL_ZAEHLER_BETRZEITZWE1(60, "runtimeTotalSecondHeatGenerator1", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDZWE1), + + /** + * Operating hours Second heat generator 2 + * (original: Betriebsstunden Zweiter Wärmeerzeuger 2) + */ + CHANNEL_ZAEHLER_BETRZEITZWE2(61, "runtimeTotalSecondHeatGenerator2", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDZWE2), + + /** + * Operating hours Second heat generator 3 + * (original: Betriebsstunden Zweiter Wärmeerzeuger 3) + */ + CHANNEL_ZAEHLER_BETRZEITZWE3(62, "runtimeTotalSecondHeatGenerator3", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDZWE3), + + /** + * Operating hours heat pump + * (original: Betriebsstunden Wärmepumpe) + */ + CHANNEL_ZAEHLER_BETRZEITWP(63, "runtimeTotalHeatPump", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDWP), + + /** + * Operating hours heating + * (original: Betriebsstunden Heizung) + */ + CHANNEL_ZAEHLER_BETRZEITHZ(64, "runtimeTotalHeating", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDHZ), + + /** + * Operating hours hot water + * (original: Betriebsstunden Warmwasser) + */ + CHANNEL_ZAEHLER_BETRZEITBW(65, "runtimeTotalHotWater", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDBW), + + /** + * Operating hours cooling + * (original: Betriebsstunden Kühlung) + */ + CHANNEL_ZAEHLER_BETRZEITKUE(66, "runtimeTotalCooling", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDKUE), + + /** + * Heat pump running since + * (original: Wärmepumpe läuft seit) + */ + CHANNEL_TIME_WPEIN_AKT(67, "runtimeCurrentHeatPump", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_WP_SEIT), + + /** + * Second heat generator 1 running since + * (original: Zweiter Wärmeerzeuger 1 läuft seit) + */ + CHANNEL_TIME_ZWE1_AKT(68, "runtimeCurrentSecondHeatGenerator1", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_ZWE1_SEIT), + + /** + * Second heat generator 2 running since + * (original: Zweiter Wärmeerzeuger 2 läuft seit) + */ + CHANNEL_TIME_ZWE2_AKT(69, "runtimeCurrentSecondHeatGenerator2", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_ZWE2_SEIT), + + /** + * Mains on delay + * (original: Netzeinschaltverzögerung) + */ + CHANNEL_TIMER_EINSCHVERZ(70, "mainsOnDelay", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_NETZEINV), + + /** + * Switching cycle lock off + * (original: Schaltspielsperre Aus) + */ + CHANNEL_TIME_SSPAUS_AKT(71, "switchingCycleLockOff", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_SSP_ZEIT1), + + /** + * Switching cycle lock on + * (original: Schaltspielsperre Ein) + */ + CHANNEL_TIME_SSPEIN_AKT(72, "switchingCycleLockOn", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_SSP_ZEIT1), + + /** + * Compressor Idle time + * (original: Verdichter-Standzeit) + */ + CHANNEL_TIME_VDSTD_AKT(73, "compressorIdleTime", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_VD_STAND), + + /** + * Heating controller More time + * (original: Heizungsregler Mehr-Zeit) + */ + CHANNEL_TIME_HRM_AKT(74, "heatingControllerMoreTime", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_HRM_ZEIT), + + /** + * Heating controller Less time + * (original: Heizungsregler Weniger-Zeit) + */ + CHANNEL_TIME_HRW_AKT(75, "heatingControllerLessTime", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_HRW_ZEIT), + + /** + * Thermal disinfection running since + * (original: Thermische Desinfektion läuft seit) + */ + CHANNEL_TIME_LGS_AKT(76, "runtimeCurrentThermalDisinfection", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_TDI_SEIT), + + /** + * Hot water lock + * (original: Sperre Warmwasser) + */ + CHANNEL_TIME_SBW_AKT(77, "timeHotWaterLock", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_SPERRE_BW), + + // Channel 78 (Code_WP_akt_2) represents the heatpump type, will be handeled as property + + /** + * Bivalence stage + * (original: Bivalenzstufe) + */ + CHANNEL_BIV_STUFE_AKT(79, "bivalenceStage", NumberItem.class, null, false, null), + + /** + * Operating status + * (original: Betriebszustand) + */ + CHANNEL_WP_BZ_AKT(80, "operatingStatus", NumberItem.class, null, false, null), + + // channel 81 - 90 represents the firmware version, will be handeled as property + // channel 91 represents the IP address, will be handeled as property + // channel 92 represents the Subnet mask, will be handeled as property + // channel 93 represents the Broadcast address, will be handeled as property + // channel 94 represents the Gateway, will be handeled as property + + /** + * Timestamp error X in memory + * (original: Zeitstempel Fehler X im Speicher) + */ + CHANNEL_HEATPUMP_ERROR_TIME0(95, "errorTime0", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_ERROR_TIME1(96, "errorTime1", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_ERROR_TIME2(97, "errorTime2", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_ERROR_TIME3(98, "errorTime3", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_ERROR_TIME4(99, "errorTime4", DateTimeItem.class, Units.SECOND, false, null), + + /** + * Error code Error X in memory + * (original: Fehlercode Fehler X im Speicher) + */ + CHANNEL_HEATPUMP_ERROR_NR0(100, "errorCode0", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_ERROR_NR1(101, "errorCode1", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_ERROR_NR2(102, "errorCode2", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_ERROR_NR3(103, "errorCode3", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_ERROR_NR4(104, "errorCode4", NumberItem.class, null, false, null), + + /** + * Number of errors in memory + * (original: Anzahl der Fehler im Speicher) + */ + CHANNEL_ANZAHLFEHLERINSPEICHER(105, "errorCountInMemory", NumberItem.class, null, false, null), + + /** + * Reason shutdown X in memory + * (original: Grund Abschaltung X im Speicher) + */ + CHANNEL_HEATPUMP_SWITCHOFF_FILE_NR0(106, "shutdownReason0", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_NR1(107, "shutdownReason1", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_NR2(108, "shutdownReason2", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_NR3(109, "shutdownReason3", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_NR4(110, "shutdownReason4", NumberItem.class, null, false, null), + + /** + * Timestamp shutdown X in memory + * (original: Zeitstempel Abschaltung X im Speicher) + */ + CHANNEL_HEATPUMP_SWITCHOFF_FILE_TIME0(111, "shutdownTime0", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_TIME1(112, "shutdownTime1", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_TIME2(113, "shutdownTime2", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_TIME3(114, "shutdownTime3", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_HEATPUMP_SWITCHOFF_FILE_TIME4(115, "shutdownTime4", DateTimeItem.class, Units.SECOND, false, null), + + /** + * Comfort board installed + * (original: Comfort Platine installiert) + */ + CHANNEL_HEATPUMP_COMFORT_EXISTS(116, "comfortBoardInstalled", SwitchItem.class, null, false, null), + + /** + * Status + * (original: Status) + */ + CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE1(117, "menuStateLine1", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE2(118, "menuStateLine2", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEILE3(119, "menuStateLine3", NumberItem.class, null, false, null), + CHANNEL_HEATPUMP_HAUPTMENUSTATUS_ZEIT(120, "menuStateTime", NumberItem.class, Units.SECOND, false, null), + + /** + * Stage bakeout program + * (original: Stufe Ausheizprogramm) + */ + CHANNEL_HAUPTMENUAHP_STUFE(121, "bakeoutProgramStage", NumberItem.class, null, false, + HeatpumpVisibility.SERVICE_AUSHEIZ), + + /** + * Temperature bakeout program + * (original: Temperatur Ausheizprogramm) + */ + CHANNEL_HAUPTMENUAHP_TEMP(122, "bakeoutProgramTemperature", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.SERVICE_AUSHEIZ), + + /** + * Runtime bakeout program + * (original: Laufzeit Ausheizprogramm) + */ + CHANNEL_HAUPTMENUAHP_ZEIT(123, "bakeoutProgramTime", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.SERVICE_AUSHEIZ), + + /** + * DHW active/inactive icon + * (original: Brauchwasser aktiv/inaktiv Symbol) + */ + CHANNEL_SH_BWW(124, "iconHotWater", SwitchItem.class, null, false, HeatpumpVisibility.BRAUWASSER), + + /** + * Heater icon + * (original: Heizung Symbol) + */ + CHANNEL_SH_HZ(125, "iconHeater", NumberItem.class, null, false, HeatpumpVisibility.HEIZUNG), + + /** + * Mixing circuit 1 icon + * (original: Mischkreis 1 Symbol) + */ + CHANNEL_SH_MK1(126, "iconMixingCircuit1", NumberItem.class, null, false, HeatpumpVisibility.MK1), + + /** + * Mixing circuit 2 icon + * (original: Mischkreis 2 Symbol) + */ + CHANNEL_SH_MK2(127, "iconMixingCircuit2", NumberItem.class, null, false, HeatpumpVisibility.MK2), + + /** + * Short program setting + * (original: Einstellung Kurzprogramm) + */ + CHANNEL_EINST_KURZPROGRAMM(128, "shortProgramSetting", NumberItem.class, null, false, null), + + /** + * Status Slave X + * (original: Status Slave X) + */ + CHANNEL_STATUSSLAVE1(129, "statusSlave1", NumberItem.class, null, false, null), + CHANNEL_STATUSSLAVE2(130, "statusSlave2", NumberItem.class, null, false, null), + CHANNEL_STATUSSLAVE3(131, "statusSlave3", NumberItem.class, null, false, null), + CHANNEL_STATUSSLAVE4(132, "statusSlave4", NumberItem.class, null, false, null), + CHANNEL_STATUSSLAVE5(133, "statusSlave5", NumberItem.class, null, false, null), + + /** + * Current time of the heat pump + * (original: Aktuelle Zeit der Wärmepumpe) + */ + CHANNEL_AKTUELLETIMESTAMP(134, "currentTimestamp", DateTimeItem.class, Units.SECOND, false, + HeatpumpVisibility.SERVICE_DATUMUHRZEIT), + + /** + * Mixing circuit 3 icon + * (original: Mischkreis 3 Symbol) + */ + CHANNEL_SH_MK3(135, "iconMixingCircuit3", NumberItem.class, null, false, HeatpumpVisibility.MK3), + + /** + * Mixing circuit 3 Flow set temperature + * (original: Mischkreis 3 Vorlauf-Soll-Temperatur) + */ + CHANNEL_SOLLWERT_TVL_MK3(136, "temperatureMixingCircuit3FlowTarget", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK3VL_SOLL), + + /** + * Mixing circuit 3 Flow temperature + * (original: Mischkreis 3 Vorlauftemperatur) + */ + CHANNEL_TEMPERATUR_TFB3(137, "temperatureMixingCircuit3Flow", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_MK3_VORLAUF), + + /** + * Output "Mixing circuit 3 close" + * (original: Ausgang "Mischkreis 3 Zu") + */ + CHANNEL_MZ3OUT(138, "outputMixingCircuit3Close", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER3ZU), + + /** + * Output "Mixing circuit 3 open" + * (original: Ausgang "Mischkreis 3 Auf") + */ + CHANNEL_MA3OUT(139, "outputMixingCircuit3Open", SwitchItem.class, null, false, HeatpumpVisibility.OUT_MISCHER3AUF), + + /** + * Pump mixing circuit 3 + * (original: Pumpe Mischkreis 3) + */ + CHANNEL_FP3OUT(140, "outputMixingCircuitPump3", SwitchItem.class, null, false, HeatpumpVisibility.OUT_FUP3), + + /** + * Time until defrost + * (original: Zeit bis Abtauen) + */ + CHANNEL_TIME_ABTIN(141, "timeUntilDefrost", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_ABTAUIN), + + /** + * Room temperature room station 2 + * (original: Raumtemperatur Raumstation 2) + */ + CHANNEL_TEMPERATUR_RFV2(142, "temperatureRoomStation2", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RAUMSTATION2), + + /** + * Room temperature room station 3 + * (original: Raumtemperatur Raumstation 3) + */ + CHANNEL_TEMPERATUR_RFV3(143, "temperatureRoomStation3", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMP_RAUMSTATION3), + + /** + * Time switch swimming pool icon + * (original: Schaltuhr Schwimmbad Symbol) + */ + CHANNEL_SH_SW(144, "iconTimeSwitchSwimmingPool", NumberItem.class, null, false, HeatpumpVisibility.SCHWIMMBAD), + + /** + * Swimming pool operating hours + * (original: Betriebsstunden Schwimmbad) + */ + CHANNEL_ZAEHLER_BETRZEITSW(145, "runtimeTotalSwimmingPool", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.BST_BSTDSW), + + /** + * Release cooling + * (original: Freigabe Kühlung) + */ + CHANNEL_FREIGABKUEHL(146, "coolingRelease", SwitchItem.class, null, false, HeatpumpVisibility.KUHLUNG), + + /** + * Analog input signal + * (original: Analoges Eingangssignal) + */ + CHANNEL_ANALOGIN(147, "inputAnalog", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.IN_ANALOGIN), + + // CHANNEL_SONDERZEICHEN(148, "SonderZeichen", NumberItem.class, null, false, null), + + /** + * Circulation pumps icon + * (original: Zirkulationspumpen Symbol) + */ + CHANNEL_SH_ZIP(149, "iconCirculationPump", NumberItem.class, null, false, null), + + // CHANNEL_WEBSRVPROGRAMMWERTEBEOBARTEN(150, "WebsrvProgrammWerteBeobarten", NumberItem.class, null, false, null), + + /** + * Heat meter heating + * (original: Wärmemengenzähler Heizung) + */ + CHANNEL_WMZ_HEIZUNG(151, "heatMeterHeating", NumberItem.class, Units.KILOWATT_HOUR, false, + HeatpumpVisibility.HEIZUNG), + + /** + * Heat meter domestic water + * (original: Wärmemengenzähler Brauchwasser) + */ + CHANNEL_WMZ_BRAUCHWASSER(152, "heatMeterHotWater", NumberItem.class, Units.KILOWATT_HOUR, false, + HeatpumpVisibility.BRAUWASSER), + + /** + * Heat meter swimming pool + * (original: Wärmemengenzähler Schwimmbad) + */ + CHANNEL_WMZ_SCHWIMMBAD(153, "heatMeterSwimmingPool", NumberItem.class, Units.KILOWATT_HOUR, false, + HeatpumpVisibility.SCHWIMMBAD), + + /** + * Total heat meter (since reset) + * (original: Wärmemengenzähler seit Reset) + */ + CHANNEL_WMZ_SEIT(154, "heatMeterTotalSinceReset", NumberItem.class, Units.KILOWATT_HOUR, false, null), + + /** + * Heat meter flow rate + * (original: Wärmemengenzähler Durchfluss) + */ + CHANNEL_WMZ_DURCHFLUSS(155, "heatMeterFlowRate", NumberItem.class, Units.LITRE_PER_MINUTE, false, null), + + /** + * Analog output 1 + * (original: Analog Ausgang 1) + */ + CHANNEL_ANALOGOUT1(156, "outputAnalog1", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_ANALOG_1), + + /** + * Analog output 2 + * (original: Analog Ausgang 2) + */ + CHANNEL_ANALOGOUT2(157, "outputAnalog2", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_ANALOG_2), + + /** + * Lock second compressor hot gas + * (original: Sperre zweiter Verdichter Heissgas) + */ + CHANNEL_TIME_HEISSGAS(158, "timeLockSecondHotGasCompressor", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.ABLAUFZ_HG_SPERRE), + + /** + * Supply air temperature + * (original: Zulufttemperatur) + */ + CHANNEL_TEMP_LUEFTUNG_ZULUFT(159, "temperatureSupplyAir", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMPERATUR_LUEFTUNG_ZULUFT), + + /** + * Exhaust air temperature + * (original: Ablufttemperatur) + */ + CHANNEL_TEMP_LUEFTUNG_ABLUFT(160, "temperatureExhaustAir", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.TEMPERATUR_LUEFTUNG_ABLUFT), + + /** + * Operating hours solar + * (original: Betriebstundenzähler Solar) + */ + CHANNEL_ZAEHLER_BETRZEITSOLAR(161, "runtimeTotalSolar", NumberItem.class, Units.SECOND, false, + HeatpumpVisibility.SOLAR), + + /** + * Analog output 3 + * (original: Analog Ausgang 3) + */ + CHANNEL_ANALOGOUT3(162, "outputAnalog3", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_ANALOG_3), + + /** + * Analog output 4 + * (original: Analog Ausgang 4) + */ + CHANNEL_ANALOGOUT4(163, "outputAnalog4", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_ANALOG_4), + + /** + * Supply air fan (defrost function) + * (original: Zuluft Ventilator (Abtaufunktion)) + */ + CHANNEL_OUT_VZU(164, "outputSupplyAirFan", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_VZU), + + /** + * Exhaust fan + * (original: Abluft Ventilator) + */ + CHANNEL_OUT_VAB(165, "outputExhaustFan", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.OUT_VAB), + + /** + * Output VSK + * (original: Ausgang VSK) + */ + CHANNEL_OUT_VSK(166, "outputVSK", SwitchItem.class, null, false, HeatpumpVisibility.OUT_VSK), + + /** + * Output FRH + * (original: Ausgang FRH) + */ + CHANNEL_OUT_FRH(167, "outputFRH", SwitchItem.class, null, false, HeatpumpVisibility.OUT_FRH), + + /** + * Analog input 2 + * (original: Analog Eingang 2) + */ + CHANNEL_ANALOGIN2(168, "inputAnalog2", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.IN_ANALOG_2), + + /** + * Analog input 3 + * (original: Analog Eingang 3) + */ + CHANNEL_ANALOGIN3(169, "inputAnalog3", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.IN_ANALOG_3), + + /** + * Input SAX + * (original: Eingang SAX) + */ + CHANNEL_SAXIN(170, "inputSAX", SwitchItem.class, null, false, HeatpumpVisibility.IN_SAX), + + /** + * Input SPL + * (original: Eingang SPL) + */ + CHANNEL_SPLIN(171, "inputSPL", SwitchItem.class, null, false, HeatpumpVisibility.IN_SPL), + + /** + * Ventilation board installed + * (original: Lüftungsplatine verbaut) + */ + CHANNEL_COMPACT_EXISTS(172, "ventilationBoardInstalled", SwitchItem.class, null, false, null), + + /** + * Flow rate heat source + * (original: Durchfluss Wärmequelle) + */ + CHANNEL_DURCHFLUSS_WQ(173, "flowRateHeatSource", NumberItem.class, Units.LITRE_PER_MINUTE, false, null), + + /** + * LIN BUS installed + * (original: LIN BUS verbaut) + */ + CHANNEL_LIN_EXISTS(174, "linBusInstalled", SwitchItem.class, null, false, null), + + /** + * Temperature suction evaporator + * (original: Temperatur Ansaug Verdampfer) + */ + CHANNEL_LIN_ANSAUG_VERDAMPFER(175, "temperatureSuctionEvaporator", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.LIN_ANSAUG_VERDAMPFER), + + /** + * Temperature suction compressor + * (original: Temperatur Ansaug Verdichter) + */ + CHANNEL_LIN_ANSAUG_VERDICHTER(176, "temperatureSuctionCompressor", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.LIN_ANSAUG_VERDICHTER), + + /** + * Temperature compressor heating + * (original: Temperatur Verdichter Heizung) + */ + CHANNEL_LIN_VDH(177, "temperatureCompressorHeating", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.LIN_VDH), + + /** + * Overheating + * (original: Überhitzung) + */ + CHANNEL_LIN_UH(178, "temperatureOverheating", NumberItem.class, Units.KELVIN, false, HeatpumpVisibility.LIN_UH), + + /** + * Overheating target + * (original: Überhitzung Soll) + */ + CHANNEL_LIN_UH_SOLL(179, "temperatureOverheatingTarget", NumberItem.class, Units.KELVIN, false, + HeatpumpVisibility.LIN_UH), + + /** + * High pressure + * (original: Hochdruck) + */ + CHANNEL_LIN_HD(180, "highPressure", NumberItem.class, Units.BAR, false, HeatpumpVisibility.LIN_DRUCK), + + /** + * Low pressure + * (original: Niederdruck) + */ + CHANNEL_LIN_ND(181, "lowPressure", NumberItem.class, Units.BAR, false, HeatpumpVisibility.LIN_DRUCK), + + /** + * Output compressor heating + * (original: Ausgang Verdichterheizung) + */ + CHANNEL_LIN_VDH_OUT(182, "outputCompressorHeating", SwitchItem.class, null, false, null), + + /** + * Control signal circulating pump + * (original: Steuersignal Umwälzpumpe) + */ + CHANNEL_HZIO_PWM(183, "controlSignalCirculatingPump", NumberItem.class, Units.PERCENT, false, null), + + /** + * Fan speed + * (original: Ventilator Drehzahl) + */ + CHANNEL_HZIO_VEN(184, "fanSpeed", NumberItem.class, null, false, null), + + /** + * EVU 2 + * (original: EVU 2) + */ + // CHANNEL_HZIO_EVU2(185, "HZIO_EVU2", NumberItem.class, null, false, null), + + /** + * Safety tempearture limiter floor heating + * (original: Sicherheits-Tempeartur-Begrenzer Fussbodenheizung) + */ + CHANNEL_HZIO_STB(186, "temperatureSafetyLimitFloorHeating", SwitchItem.class, null, false, null), + + /** + * Power target value + * (original: Leistung Sollwert) + */ + CHANNEL_SEC_QH_SOLL(187, "powerTargetValue", NumberItem.class, Units.KILOWATT_HOUR, false, null), + + /** + * Power actual value + * (original: Leistung Istwert) + */ + CHANNEL_SEC_QH_IST(188, "powerActualValue", NumberItem.class, Units.KILOWATT_HOUR, false, null), + + /** + * Temperature flow set point + * (original: Temperatur Vorlauf Soll) + */ + CHANNEL_SEC_TVL_SOLL(189, "temperatureFlowTarget", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Software version SEC Board + * (original: Software Stand SEC Board) + */ + // CHANNEL_SEC_SOFTWARE(190, "SEC_Software", NumberItem.class, null, false, null), + + /** + * SEC Board operating status + * (original: Betriebszustand SEC Board) + */ + CHANNEL_SEC_BZ(191, "operatingStatusSECBoard", NumberItem.class, null, false, HeatpumpVisibility.SEC), + + /** + * Four-way valve + * (original: Vierwegeventil) + */ + CHANNEL_SEC_VWV(192, "fourWayValve", NumberItem.class, null, false, HeatpumpVisibility.SEC), + + /** + * Compressor speed + * (original: Verdichterdrehzahl) + */ + CHANNEL_SEC_VD(193, "compressorSpeed", NumberItem.class, null, false, HeatpumpVisibility.SEC), + + /** + * Compressor temperature EVI (Enhanced Vapour Injection) + * (original: Verdichtertemperatur EVI) + */ + CHANNEL_SEC_VERDEVI(194, "temperatureCompressorEVI", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.SEC), + + /** + * Intake temperature EVI + * (original: Ansaugtemperatur EVI) + */ + CHANNEL_SEC_ANSEVI(195, "temperatureIntakeEVI", NumberItem.class, SIUnits.CELSIUS, false, HeatpumpVisibility.SEC), + + /** + * Overheating EVI + * (original: Überhitzung EVI) + */ + CHANNEL_SEC_UEH_EVI(196, "temperatureOverheatingEVI", NumberItem.class, Units.KELVIN, false, + HeatpumpVisibility.SEC), + + /** + * Overheating EVI target + * (original: Überhitzung EVI Sollwert) + */ + CHANNEL_SEC_UEH_EVI_S(197, "temperatureOverheatingTargetEVI", NumberItem.class, Units.KELVIN, false, + HeatpumpVisibility.SEC), + + /** + * Condensation temperature + * (original: Kondensationstemperatur) + */ + CHANNEL_SEC_KONDTEMP(198, "temperatureCondensation", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.SEC), + + /** + * Liquid temperature EEV (electronic expansion valve) + * (original: Flüssigtemperatur EEV (elektronisches Expansionsventil)) + */ + CHANNEL_SEC_FLUSSIGEX(199, "temperatureLiquidEEV", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.SEC), + + /** + * Hypothermia EEV + * (original: Unterkühlung EEV) + */ + CHANNEL_SEC_UK_EEV(200, "temperatureHypothermiaEEV", NumberItem.class, SIUnits.CELSIUS, false, + HeatpumpVisibility.SEC), + + /** + * Pressure EVI + * (original: Druck EVI) + */ + CHANNEL_SEC_EVI_DRUCK(201, "pressureEVI", NumberItem.class, Units.BAR, false, HeatpumpVisibility.SEC), + + /** + * Voltage inverter + * (original: Spannung Inverter) + */ + CHANNEL_SEC_U_INV(202, "voltageInverter", NumberItem.class, Units.VOLT, false, HeatpumpVisibility.SEC), + + /** + * Hot gas temperature sensor 2 + * (original: Temperarturfühler Heissgas 2) + */ + CHANNEL_TEMPERATUR_THG_2(203, "temperatureHotGas2", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Temperature sensor heat source inlet 2 + * (original: Temperaturfühler Wärmequelleneintritt 2) + */ + CHANNEL_TEMPERATUR_TWE_2(204, "temperatureHeatSourceInlet2", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Intake temperature evaporator 2 + * (original: Ansaugtemperatur Verdampfer 2) + */ + CHANNEL_LIN_ANSAUG_VERDAMPFER_2(205, "temperatureIntakeEvaporator2", NumberItem.class, SIUnits.CELSIUS, false, + null), + + /** + * Intake temperature compressor 2 + * (original: Ansaugtemperatur Verdichter 2) + */ + CHANNEL_LIN_ANSAUG_VERDICHTER_2(206, "temperatureIntakeCompressor2", NumberItem.class, SIUnits.CELSIUS, false, + null), + + /** + * Temperature compressor 2 heating + * (original: Temperatur Verdichter 2 Heizung) + */ + CHANNEL_LIN_VDH_2(207, "temperatureCompressor2Heating", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Overheating 2 + * (original: Überhitzung 2) + */ + CHANNEL_LIN_UH_2(208, "temperatureOverheating2", NumberItem.class, Units.KELVIN, false, HeatpumpVisibility.LIN_UH), + + /** + * Overheating target 2 + * (original: Überhitzung Soll 2) + */ + CHANNEL_LIN_UH_SOLL_2(209, "temperatureOverheatingTarget2", NumberItem.class, Units.KELVIN, false, + HeatpumpVisibility.LIN_UH), + + /** + * High pressure 2 + * (original: Hochdruck 2) + */ + CHANNEL_LIN_HD_2(210, "highPressure2", NumberItem.class, Units.BAR, false, HeatpumpVisibility.LIN_DRUCK), + + /** + * Low pressure 2 + * (original: Niederdruck 2) + */ + CHANNEL_LIN_ND_2(211, "lowPressure2", NumberItem.class, Units.BAR, false, HeatpumpVisibility.LIN_DRUCK), + + /** + * Input pressure switch high pressure 2 + * (original: Eingang Druckschalter Hochdruck 2) + */ + CHANNEL_HDIN_2(212, "inputSwitchHighPressure2", SwitchItem.class, null, false, HeatpumpVisibility.IN_HD), + + /** + * Output defrost valve 2 + * (original: Ausgang Abtauventil 2) + */ + CHANNEL_AVOUT_2(213, "outputDefrostValve2", SwitchItem.class, null, false, HeatpumpVisibility.OUT_ABTAUVENTIL), + + /** + * Output brine pump/fan 2 + * (original: Ausgang Solepumpe/Ventilator 2) + */ + CHANNEL_VBOOUT_2(214, "outputVBO2", SwitchItem.class, null, false, null), + + /** + * Compressor output 1 / 2 + * (original: Ausgang Verdichter 1 / 2) + */ + CHANNEL_VD1OUT_2(215, "outputCompressor1_2", SwitchItem.class, null, false, null), + + /** + * Compressor output heating 2 + * (original: Ausgang Verdichter Heizung 2) + */ + CHANNEL_LIN_VDH_OUT_2(216, "outputCompressorHeating2", SwitchItem.class, null, false, null), + + /** + * Reason shutdown X in memory 2 + * (original: Grund Abschaltung X im Speicher 2) + */ + CHANNEL_SWITCHOFF2_FILE_NR0(217, "secondShutdownReason0", NumberItem.class, null, false, null), + CHANNEL_SWITCHOFF2_FILE_NR1(218, "secondShutdownReason1", NumberItem.class, null, false, null), + CHANNEL_SWITCHOFF2_FILE_NR2(219, "secondShutdownReason2", NumberItem.class, null, false, null), + CHANNEL_SWITCHOFF2_FILE_NR3(220, "secondShutdownReason3", NumberItem.class, null, false, null), + CHANNEL_SWITCHOFF2_FILE_NR4(221, "secondShutdownReason4", NumberItem.class, null, false, null), + + /** + * Timestamp shutdown X in memory 2 + * (original: Zeitstempel Abschaltung X im Speicher 2) + */ + CHANNEL_SWITCHOFF2_FILE_TIME0(222, "secondShutdownTime0", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_SWITCHOFF2_FILE_TIME1(223, "secondShutdownTime1", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_SWITCHOFF2_FILE_TIME2(224, "secondShutdownTime2", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_SWITCHOFF2_FILE_TIME3(225, "secondShutdownTime3", DateTimeItem.class, Units.SECOND, false, null), + CHANNEL_SWITCHOFF2_FILE_TIME4(226, "secondShutdownTime4", DateTimeItem.class, Units.SECOND, false, null), + + /** + * Room temperature actual value + * (original: Raumtemperatur Istwert) + */ + CHANNEL_RBE_RT_IST(227, "temperatureRoom", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Room temperature set point + * (original: Raumtemperatur Sollwert) + */ + CHANNEL_RBE_RT_SOLL(228, "temperatureRoomTarget", NumberItem.class, SIUnits.CELSIUS, false, null), + + /** + * Temperature domestic water top + * (original: Temperatur Brauchwasser Oben) + */ + CHANNEL_TEMPERATUR_BW_OBEN(229, "temperatureHotWaterTop", NumberItem.class, SIUnits.CELSIUS, false, null), + + // DE: Channel 230 (Code_WP_akt_2) represent the heatpump type 2 + + /** + * Compressor frequency + * (original: Verdichterfrequenz) + */ + CHANNEL_CODE_FREQ_VD(231, "frequencyCompressor", NumberItem.class, Units.HERTZ, false, null), + + // Changeable Parameters + // https://www.loxwiki.eu/display/LOX/Java+Webinterface?preview=/13306044/13307658/3003.txt + + /** + * Heating temperature (parallel shift) + * (original: Heizung Temperatur (Parallelverschiebung)) + */ + CHANNEL_EINST_WK_AKT(1, "temperatureHeatingParallelShift", NumberItem.class, SIUnits.CELSIUS, true, + HeatpumpVisibility.HEIZUNG), + + /** + * Hot water temperature + * (original: Warmwasser Soll Temperatur) + */ + CHANNEL_EINST_BWS_AKT(2, "temperatureHotWaterTarget", NumberItem.class, SIUnits.CELSIUS, true, + HeatpumpVisibility.BRAUWASSER), + + /** + * Heating mode + * (original: Heizung Betriebsart) + */ + CHANNEL_BA_HZ_AKT(3, "heatingMode", NumberItem.class, null, true, HeatpumpVisibility.HEIZUNG), + + /** + * Hot water operating mode + * (original: Warmwasser Betriebsart) + */ + CHANNEL_BA_BW_AKT(4, "hotWaterMode", NumberItem.class, null, true, HeatpumpVisibility.BRAUWASSER), + + /** + * Thermal disinfection (Monday) + * (original: Thermische Desinfektion (Montag)) + */ + CHANNEL_EINST_BWTDI_AKT_MO(20, "thermalDisinfectionMonday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Tuesday) + * (original: Thermische Desinfektion (Dienstag)) + */ + CHANNEL_EINST_BWTDI_AKT_DI(21, "thermalDisinfectionTuesday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Wednesday) + * (original: Thermische Desinfektion (Mittwoch)) + */ + CHANNEL_EINST_BWTDI_AKT_MI(22, "thermalDisinfectionWednesday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Thursday) + * (original: Thermische Desinfektion (Donnerstag)) + */ + CHANNEL_EINST_BWTDI_AKT_DO(23, "thermalDisinfectionThursday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Friday) + * (original: Thermische Desinfektion (Freitag)) + */ + CHANNEL_EINST_BWTDI_AKT_FR(24, "thermalDisinfectionFriday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Saturday) + * (original: Thermische Desinfektion (Samstag)) + */ + CHANNEL_EINST_BWTDI_AKT_SA(25, "thermalDisinfectionSaturday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Sunday) + * (original: Thermische Desinfektion (Sonntag)) + */ + CHANNEL_EINST_BWTDI_AKT_SO(26, "thermalDisinfectionSunday", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + /** + * Thermal disinfection (Permanent) + * (original: Thermische Desinfektion (Dauerbetrieb)) + */ + CHANNEL_EINST_BWTDI_AKT_AL(27, "thermalDisinfectionPermanent", SwitchItem.class, null, true, + HeatpumpVisibility.THERMDESINFEKT), + + /** + * Comfort cooling mode + * (original: Comfort Kühlung Betriebsart) + */ + CHANNEL_EINST_BWSTYP_AKT(108, "comfortCoolingMode", NumberItem.class, null, true, HeatpumpVisibility.KUHLUNG), + + /** + * Comfort cooling AT release + * (original: Comfort Kühlung AT-Freigabe) + */ + CHANNEL_EINST_KUCFTL_AKT(110, "temperatureComfortCoolingATRelease", NumberItem.class, SIUnits.CELSIUS, true, + HeatpumpVisibility.KUHLUNG), + + /** + * Comfort cooling AT release target + * (original: Comfort Kühlung AT-Freigabe Sollwert) + */ + CHANNEL_SOLLWERT_KUCFTL_AKT(132, "temperatureComfortCoolingATReleaseTarget", NumberItem.class, SIUnits.CELSIUS, + true, HeatpumpVisibility.KUHLUNG), + + /** + * AT Excess + * (original: AT-Überschreitung) + */ + CHANNEL_EINST_KUHL_ZEIT_EIN_AKT(850, "comfortCoolingATExcess", NumberItem.class, Units.HOUR, true, + HeatpumpVisibility.KUHLUNG), + + /** + * AT undercut + * (original: AT-Unterschreitung) + */ + CHANNEL_EINST_KUHL_ZEIT_AUS_AKT(851, "comfortCoolingATUndercut", NumberItem.class, Units.HOUR, true, + HeatpumpVisibility.KUHLUNG), + + /** + * Channel holding complete (localized) status message + */ + CHANNEL_HEATPUMP_STATUS(null, "menuStateFull", StringItem.class, null, false, null); + + private @Nullable Integer channelId; + private String command; + private Class itemClass; + private @Nullable Unit unit; + private boolean isParameter; + private @Nullable HeatpumpVisibility requiredVisibility; + + private HeatpumpChannel(@Nullable Integer channelId, String command, Class itemClass, + @Nullable Unit unit, boolean isParameter, @Nullable HeatpumpVisibility requiredVisibility) { + this.channelId = channelId; + this.command = command; + this.itemClass = itemClass; + this.unit = unit; + this.isParameter = isParameter; + this.requiredVisibility = requiredVisibility; + } + + public @Nullable Integer getChannelId() { + return channelId; + } + + public String getCommand() { + return command; + } + + public Class getItemClass() { + return itemClass; + } + + public @Nullable Unit getUnit() { + return unit; + } + + public boolean isWritable() { + return isParameter == Boolean.TRUE; + } + + protected @Nullable HeatpumpVisibility getVisibility() { + return requiredVisibility; + } + + public boolean isVisible(Integer[] visibilityValues) { + HeatpumpVisibility visiblity = getVisibility(); + + if (visiblity == null) { + return true; + } + + int code = visiblity.getCode(); + + if (visibilityValues.length < code || visibilityValues[code] == 1) { + return true; + } + + return false; + } + + public static HeatpumpChannel fromString(String heatpumpCommand) throws InvalidChannelException { + for (HeatpumpChannel c : HeatpumpChannel.values()) { + + if (c.getCommand().equals(heatpumpCommand)) { + return c; + } + } + + throw new InvalidChannelException("cannot find LuxtronikHeatpump channel for '" + heatpumpCommand + "'"); + } + + @Override + public String toString() { + return getCommand(); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpCoolingOperationMode.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpCoolingOperationMode.java new file mode 100644 index 0000000000000..2ce17cade64e7 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpCoolingOperationMode.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidOperationModeException; + +/** + * Represents all heat pump cooling operation modes + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public enum HeatpumpCoolingOperationMode { + AUTOMATIC(1), + OFF(0); + + private int value; + + private HeatpumpCoolingOperationMode(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static HeatpumpCoolingOperationMode fromValue(int value) throws InvalidOperationModeException { + for (HeatpumpCoolingOperationMode mode : HeatpumpCoolingOperationMode.values()) { + if (mode.value == value) { + return mode; + } + } + + throw new InvalidOperationModeException("Invalid heat pump cooling operation mode: '" + value + "'"); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpOperationMode.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpOperationMode.java new file mode 100644 index 0000000000000..2f90149c30da3 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpOperationMode.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidOperationModeException; + +/** + * Represents all heat pump operation modes + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public enum HeatpumpOperationMode { + AUTOMATIC(0), + OFF(4), + PARTY(2), + HOLIDAY(3), + AUXILIARY_HEATER(1); + + private int value; + + private HeatpumpOperationMode(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static HeatpumpOperationMode fromValue(int value) throws InvalidOperationModeException { + for (HeatpumpOperationMode mode : HeatpumpOperationMode.values()) { + if (mode.value == value) { + return mode; + } + } + + throw new InvalidOperationModeException("Invalid heat pump operation mode: '" + value + "'"); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpType.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpType.java new file mode 100644 index 0000000000000..31295f80b165b --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpType.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents all heat pump types + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public enum HeatpumpType { + TYPE_ERC(0, "ERC"), + TYPE_SW1(1, "SW1"), + TYPE_SW2(2, "SW2"), + TYPE_WW1(3, "WW1"), + TYPE_WW2(4, "WW2"), + TYPE_L1I(5, "L1I"), + TYPE_L2I(6, "L2I"), + TYPE_L1A(7, "L1A"), + TYPE_L2A(8, "L2A"), + TYPE_KSW(9, "KSW"), + TYPE_KLW(10, "KLW"), + TYPE_SWC(11, "SWC"), + TYPE_LWC(12, "LWC"), + TYPE_L2G(13, "L2G"), + TYPE_WZS(14, "WZS"), + TYPE_L1I407(15, "L1I407"), + TYPE_L2I407(16, "L2I407"), + TYPE_L1A407(17, "L1A407"), + TYPE_L2A407(18, "L2A407"), + TYPE_L2G407(19, "L2G407"), + TYPE_LWC407(20, "LWC407"), + TYPE_L1AREV(21, "L1AREV"), + TYPE_L2AREV(22, "L2AREV"), + TYPE_WWC1(23, "WWC1"), + TYPE_WWC2(24, "WWC2"), + TYPE_L2G404(25, "L2G404"), + TYPE_WZW(26, "WZW"), + TYPE_L1S(27, "L1S"), + TYPE_L1H(28, "L1H"), + TYPE_L2H(29, "L2H"), + TYPE_WZWD(30, "WZWD"), + TYPE_ERC2(31, "ERC"), + TYPE_WWB_20(40, "WWB_20"), + TYPE_LD5(41, "LD5"), + TYPE_LD7(42, "LD7"), + TYPE_SW_37_45(43, "SW 37_45"), + TYPE_SW_58_69(44, "SW 58_69"), + TYPE_SW_29_56(45, "SW 29_56"), + TYPE_LD5_230V(46, "LD5 (230V)"), + TYPE_LD7_230V(47, "LD7 (230 V)"), + TYPE_LD9(48, "LD9"), + TYPE_LD5_REV(49, "LD5 REV"), + TYPE_LD7_REV(50, "LD7 REV"), + TYPE_LD5_REV_230V(51, "LD5 REV 230V"), + TYPE_LD7_REV_230V(52, "LD7 REV 230V"), + TYPE_LD9_REV_230V(53, "LD9 REV 230V"), + TYPE_SW_291(54, "SW 291"), + TYPE_LW_SEC(55, "LW SEC"), + TYPE_HMD_2(56, "HMD 2"), + TYPE_MSW_4(57, "MSW 4"), + TYPE_MSW_6(58, "MSW 6"), + TYPE_MSW_8(59, "MSW 8"), + TYPE_MSW_10(60, "MSW 10"), + TYPE_MSW_12(61, "MSW 12"), + TYPE_MSW_14(62, "MSW 14"), + TYPE_MSW_17(63, "MSW 17"), + TYPE_MSW_19(64, "MSW 19"), + TYPE_MSW_23(65, "MSW 23"), + TYPE_MSW_26(66, "MSW 26"), + TYPE_MSW_30(67, "MSW 30"), + TYPE_MSW_4S(68, "MSW 4S"), + TYPE_MSW_6S(69, "MSW 6S"), + TYPE_MSW_8S(70, "MSW 8S"), + TYPE_MSW_10S(71, "MSW 10S"), + TYPE_MSW_13S(72, "MSW 13S"), + TYPE_MSW_16S(73, "MSW 16S"), + TYPE_MSW2_6S(74, "MSW2-6S"), + TYPE_MSW4_16(75, "MSW4-16"), + TYPE_LD2AG(76, "LD2AG"), + TYPE_LWD90V(77, "LWD90V"), + TYPE_MSW3_12(78, "MSW3-12"), + TYPE_MSW3_12S(79, "MSW3-12S"), + TYPE_MSW2_9S(80, "MSW2-9S"), + TYPE_LW12(82, "LW 12"), + TYPE_UNKNOWN(-1, "Unknown"); + + private final String name; + private final Integer code; + private static final Logger logger = LoggerFactory.getLogger(HeatpumpType.class); + + private HeatpumpType(Integer code, String name) { + this.code = code; + this.name = name; + } + + public static final HeatpumpType fromCode(Integer code) { + for (HeatpumpType error : HeatpumpType.values()) { + if (error.code.equals(code)) { + return error; + } + } + + logger.warn("Unknown heatpump type code {}", code); + + return TYPE_UNKNOWN; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return code + ": " + name; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpVisibility.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpVisibility.java new file mode 100644 index 0000000000000..00d5d521abb34 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/enums/HeatpumpVisibility.java @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Represents all heatpump visibily settings + * + * The names of the enum values are those used in the code if the internal Java applet of the heat pump + * The meaning of most of the values is currently unclear, but are included here for completeness only + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public enum HeatpumpVisibility { + /** + * Defines if the device has heating capabilities + */ + HEIZUNG(0, "Heizung"), + + /** + * Defines if the device has hot water capabilities + */ + BRAUWASSER(1, "Brauwasser"), + + /** + * Defines if the device swimming pool capabilities + */ + SCHWIMMBAD(2, "Schwimmbad"), + + /** + * Defines if the device has cooling capabilities + */ + KUHLUNG(3, "Kuhlung"), + + /** + * Defines if the device has ventilation capabilities + */ + LUEFTUNG(4, "Lueftung"), + + MK1(5, "MK1"), + MK2(6, "MK2"), + + /** + * Defines if thermal disinfiction is available + */ + THERMDESINFEKT(7, "ThermDesinfekt"), + ZIRKULATION(8, "Zirkulation"), + KUHLTEMP_SOLLTEMPMK1(9, "KuhlTemp_SolltempMK1"), + KUHLTEMP_SOLLTEMPMK2(10, "KuhlTemp_SolltempMK2"), + KUHLTEMP_ATDIFFMK1(11, "KuhlTemp_ATDiffMK1"), + KUHLTEMP_ATDIFFMK2(12, "KuhlTemp_ATDiffMK2"), + SERVICE_INFORMATION(13, "Service_Information"), + SERVICE_EINSTELLUNG(14, "Service_Einstellung"), + SERVICE_SPRACHE(15, "Service_Sprache"), + SERVICE_DATUMUHRZEIT(16, "Service_DatumUhrzeit"), + SERVICE_AUSHEIZ(17, "Service_Ausheiz"), + SERVICE_ANLAGENKONFIGURATION(18, "Service_Anlagenkonfiguration"), + SERVICE_IBNASSISTANT(19, "Service_IBNAssistant"), + SERVICE_PARAMETERIBNZURUCK(20, "Service_ParameterIBNZuruck"), + TEMP_VORLAUF(21, "Temp_Vorlauf"), + TEMP_RUCKLAUF(22, "Temp_Rucklauf"), + TEMP_RL_SOLL(23, "Temp_RL_Soll"), + TEMP_RUECKLEXT(24, "Temp_Ruecklext"), + TEMP_HEISSGAS(25, "Temp_Heissgas"), + TEMP_AUSSENT(26, "Temp_Aussent"), + TEMP_BW_IST(27, "Temp_BW_Ist"), + TEMP_BW_SOLL(28, "Temp_BW_Soll"), + TEMP_WQ_EIN(29, "Temp_WQ_Ein"), + TEMP_KALTEKREIS(30, "Temp_Kaltekreis"), + TEMP_MK1_VORLAUF(31, "Temp_MK1_Vorlauf"), + TEMP_MK1VL_SOLL(32, "Temp_MK1VL_Soll"), + TEMP_RAUMSTATION(33, "Temp_Raumstation"), + TEMP_MK2_VORLAUF(34, "Temp_MK2_Vorlauf"), + TEMP_MK2VL_SOLL(35, "Temp_MK2VL_Soll"), + TEMP_SOLARKOLL(36, "Temp_Solarkoll"), + TEMP_SOLARSP(37, "Temp_Solarsp"), + TEMP_EXT_ENERG(38, "Temp_Ext_Energ"), + IN_ASD(39, "IN_ASD"), + IN_BWT(40, "IN_BWT"), + IN_EVU(41, "IN_EVU"), + IN_HD(42, "IN_HD"), + IN_MOT(43, "IN_MOT"), + IN_ND(44, "IN_ND"), + IN_PEX(45, "IN_PEX"), + IN_SWT(46, "IN_SWT"), + OUT_ABTAUVENTIL(47, "OUT_Abtauventil"), + OUT_BUP(48, "OUT_BUP"), + OUT_FUP1(49, "OUT_FUP1"), + OUT_HUP(50, "OUT_HUP"), + OUT_MISCHER1AUF(51, "OUT_Mischer1Auf"), + OUT_MISCHER1ZU(52, "OUT_Mischer1Zu"), + OUT_VENTILATION(53, "OUT_Ventilation"), + OUT_VENTIL_BOSUP(54, "OUT_Ventil_BOSUP"), + OUT_VERDICHTER1(55, "OUT_Verdichter1"), + OUT_VERDICHTER2(56, "OUT_Verdichter2"), + OUT_ZIP(57, "OUT_ZIP"), + OUT_ZUP(58, "OUT_ZUP"), + OUT_ZWE1(59, "OUT_ZWE1"), + OUT_ZWE2_SST(60, "OUT_ZWE2_SST"), + OUT_ZWE3(61, "OUT_ZWE3"), + OUT_FUP2(62, "OUT_FUP2"), + OUT_SLP(63, "OUT_SLP"), + OUT_SUP(64, "OUT_SUP"), + OUT_MISCHER2AUF(65, "OUT_Mischer2Auf"), + OUT_MISCHER2ZU(66, "OUT_Mischer2Zu"), + ABLAUFZ_WP_SEIT(67, "AblaufZ_WP_Seit"), + ABLAUFZ_ZWE1_SEIT(68, "AblaufZ_ZWE1_seit"), + ABLAUFZ_ZWE2_SEIT(69, "AblaufZ_ZWE2_seit"), + ABLAUFZ_ZWE3_SEIT(70, "AblaufZ_ZWE3_seit"), + ABLAUFZ_NETZEINV(71, "AblaufZ_Netzeinv"), + ABLAUFZ_SSP_ZEIT1(72, "AblaufZ_SSP_Zeit1"), + ABLAUFZ_VD_STAND(73, "AblaufZ_VD_Stand"), + ABLAUFZ_HRM_ZEIT(74, "AblaufZ_HRM_Zeit"), + ABLAUFZ_HRW_ZEIT(75, "AblaufZ_HRW_Zeit"), + ABLAUFZ_TDI_SEIT(76, "AblaufZ_TDI_seit"), + ABLAUFZ_SPERRE_BW(77, "AblaufZ_Sperre_BW"), + BST_BSTDVD1(78, "Bst_BStdVD1"), + BST_IMPVD1(79, "Bst_ImpVD1"), + BST_DEZVD1(80, "Bst_dEZVD1"), + BST_BSTDVD2(81, "Bst_BStdVD2"), + BST_IMPVD2(82, "Bst_ImpVD2"), + BST_DEZVD2(83, "Bst_dEZVD2"), + BST_BSTDZWE1(84, "Bst_BStdZWE1"), + BST_BSTDZWE2(85, "Bst_BStdZWE2"), + BST_BSTDZWE3(86, "Bst_BStdZWE3"), + BST_BSTDWP(87, "Bst_BStdWP"), + TEXT_KURZPROGRAMME(88, "Text_Kurzprogramme"), + TEXT_ZWANGSHEIZUNG(89, "Text_Zwangsheizung"), + TEXT_ZWANGSBRAUCHWASSER(90, "Text_Zwangsbrauchwasser"), + TEXT_ABTAUEN(91, "Text_Abtauen"), + EINSTTEMP_RUCKLBEGR(92, "EinstTemp_RucklBegr"), + EINSTTEMP_HYSTERESEHR(93, "EinstTemp_HystereseHR"), + EINSTTEMP_TRERHMAX(94, "EinstTemp_TRErhmax"), + EINSTTEMP_FREIG2VD(95, "EinstTemp_Freig2VD"), + EINSTTEMP_FREIGZWE(96, "EinstTemp_FreigZWE"), + EINSTTEMP_TLUFTABT(97, "EinstTemp_Tluftabt"), + EINSTTEMP_TDISOLLTEMP(98, "EinstTemp_TDISolltemp"), + EINSTTEMP_HYSTERESEBW(99, "EinstTemp_HystereseBW"), + EINSTTEMP_VORL2VDBW(100, "EinstTemp_Vorl2VDBW"), + EINSTTEMP_TAUSSENMAX(101, "EinstTemp_TAussenmax"), + EINSTTEMP_TAUSSENMIN(102, "EinstTemp_TAussenmin"), + EINSTTEMP_TWQMIN(103, "EinstTemp_TWQmin"), + EINSTTEMP_THGMAX(104, "EinstTemp_THGmax"), + EINSTTEMP_TLABTENDE(105, "EinstTemp_TLABTEnde"), + EINSTTEMP_ABSENKBIS(106, "EinstTemp_Absenkbis"), + EINSTTEMP_VORLAUFMAX(107, "EinstTemp_Vorlaufmax"), + EINSTTEMP_TDIFFEIN(108, "EinstTemp_TDiffEin"), + EINSTTEMP_TDIFFAUS(109, "EinstTemp_TDiffAus"), + EINSTTEMP_TDIFFMAX(110, "EinstTemp_TDiffmax"), + EINSTTEMP_TEEHEIZUNG(111, "EinstTemp_TEEHeizung"), + EINSTTEMP_TEEBRAUCHW(112, "EinstTemp_TEEBrauchw"), + EINSTTEMP_VORL2VDSW(113, "EinstTemp_Vorl2VDSW"), + EINSTTEMP_VLMAXMK1(114, "EinstTemp_VLMaxMk1"), + EINSTTEMP_VLMAXMK2(115, "EinstTemp_VLMaxMk2"), + PRIORI_BRAUCHWASSER(116, "Priori_Brauchwasser"), + PRIORI_HEIZUNG(117, "Priori_Heizung"), + PRIORI_SCHWIMMBAD(118, "Priori_Schwimmbad"), + SYSEIN_EVUSPERRE(119, "SysEin_EVUSperre"), + SYSEIN_RAUMSTATION(120, "SysEin_Raumstation"), + SYSEIN_EINBINDUNG(121, "SysEin_Einbindung"), + SYSEIN_MISCHKREIS1(122, "SysEin_Mischkreis1"), + SYSEIN_MISCHKREIS2(123, "SysEin_Mischkreis2"), + SYSEIN_ZWE1ART(124, "SysEin_ZWE1Art"), + SYSEIN_ZWE1FKT(125, "SysEin_ZWE1Fkt"), + SYSEIN_ZWE2ART(126, "SysEin_ZWE2Art"), + SYSEIN_ZWE2FKT(127, "SysEin_ZWE2Fkt"), + SYSEIN_ZWE3ART(128, "SysEin_ZWE3Art"), + SYSEIN_ZWE3FKT(129, "SysEin_ZWE3Fkt"), + SYSEIN_STOERUNG(130, "SysEin_Stoerung"), + SYSEIN_BRAUCHWASSER1(131, "SysEin_Brauchwasser1"), + SYSEIN_BRAUCHWASSER2(132, "SysEin_Brauchwasser2"), + SYSEIN_BRAUCHWASSER3(133, "SysEin_Brauchwasser3"), + SYSEIN_BRAUCHWASSER4(134, "SysEin_Brauchwasser4"), + SYSEIN_BRAUCHWASSER5(135, "SysEin_Brauchwasser5"), + SYSEIN_BWWPMAX(136, "SysEin_BWWPmax"), + SYSEIN_ABTZYKMAX(137, "SysEin_Abtzykmax"), + SYSEIN_LUFTABT(138, "SysEin_Luftabt"), + SYSEIN_LUFTABTMAX(139, "SysEin_LuftAbtmax"), + SYSEIN_ABTAUEN1(140, "SysEin_Abtauen1"), + SYSEIN_ABTAUEN2(141, "SysEin_Abtauen2"), + SYSEIN_PUMPENOPTIM(142, "SysEin_Pumpenoptim"), + SYSEIN_ZUSATZPUMPE(143, "SysEin_Zusatzpumpe"), + SYSEIN_ZUGANG(144, "SysEin_Zugang"), + SYSEIN_SOLEDRDURCHF(145, "SysEin_SoledrDurchf"), + SYSEIN_UBERWACHUNGVD(146, "SysEin_UberwachungVD"), + SYSEIN_REGELUNGHK(147, "SysEin_RegelungHK"), + SYSEIN_REGELUNGMK1(148, "SysEin_RegelungMK1"), + SYSEIN_REGELUNGMK2(149, "SysEin_RegelungMK2"), + SYSEIN_KUHLUNG(150, "SysEin_Kuhlung"), + SYSEIN_AUSHEIZEN(151, "SysEin_Ausheizen"), + SYSEIN_ELEKTRANODE(152, "SysEin_ElektrAnode"), + SYSEIN_SWBBER(153, "SysEin_SWBBer"), + SYSEIN_SWBMIN(154, "SysEin_SWBMin"), + SYSEIN_HEIZUNG(155, "SysEin_Heizung"), + SYSEIN_PERIODEMK1(156, "SysEin_PeriodeMk1"), + SYSEIN_LAUFZEITMK1(157, "SysEin_LaufzeitMk1"), + SYSEIN_PERIODEMK2(158, "SysEin_PeriodeMk2"), + SYSEIN_LAUFZEITMK2(159, "SysEin_LaufzeitMk2"), + SYSEIN_HEIZGRENZE(160, "SysEin_Heizgrenze"), + ENLT_HUP(161, "Enlt_HUP"), + ENLT_ZUP(162, "Enlt_ZUP"), + ENLT_BUP(163, "Enlt_BUP"), + ENLT_VENTILATOR_BOSUP(164, "Enlt_Ventilator_BOSUP"), + ENLT_MA1(165, "Enlt_MA1"), + ENLT_MZ1(166, "Enlt_MZ1"), + ENLT_ZIP(167, "Enlt_ZIP"), + ENLT_MA2(168, "Enlt_MA2"), + ENLT_MZ2(169, "Enlt_MZ2"), + ENLT_SUP(170, "Enlt_SUP"), + ENLT_SLP(171, "Enlt_SLP"), + ENLT_FP2(172, "Enlt_FP2"), + ENLT_LAUFZEIT(173, "Enlt_Laufzeit"), + ANLGKONF_HEIZUNG(174, "Anlgkonf_Heizung"), + ANLGKONF_BRAUCHWARMWASSER(175, "Anlgkonf_Brauchwarmwasser"), + ANLGKONF_SCHWIMMBAD(176, "Anlgkonf_Schwimmbad"), + HEIZUNG_BETRIEBSART(177, "Heizung_Betriebsart"), + HEIZUNG_TEMPERATURPLUSMINUS(178, "Heizung_TemperaturPlusMinus"), + HEIZUNG_HEIZKURVEN(179, "Heizung_Heizkurven"), + HEIZUNG_ZEITSCHLALTPROGRAMM(180, "Heizung_Zeitschlaltprogramm"), + HEIZUNG_HEIZGRENZE(181, "Heizung_Heizgrenze"), + MITTELTEMPERATUR(182, "Mitteltemperatur"), + DATAENLOGGER(183, "Dataenlogger"), + SPRACHEN_DEUTSCH(184, "Sprachen_DEUTSCH"), + SPRACHEN_ENGLISH(185, "Sprachen_ENGLISH"), + SPRACHEN_FRANCAIS(186, "Sprachen_FRANCAIS"), + SPRACHEN_NORWAY(187, "Sprachen_NORWAY"), + SPRACHEN_TCHECH(188, "Sprachen_TCHECH"), + SPRACHEN_ITALIANO(189, "Sprachen_ITALIANO"), + SPRACHEN_NEDERLANDS(190, "Sprachen_NEDERLANDS"), + SPRACHEN_SVENSKA(191, "Sprachen_SVENSKA"), + SPRACHEN_POLSKI(192, "Sprachen_POLSKI"), + SPRACHEN_MAGYARUL(193, "Sprachen_MAGYARUL"), + ERRORUSBSPEICHERN(194, "ErrorUSBspeichern"), + BST_BSTDHZ(195, "Bst_BStdHz"), + BST_BSTDBW(196, "Bst_BStdBW"), + BST_BSTDKUE(197, "Bst_BStdKue"), + SERVICE_SYSTEMSTEUERUNG(198, "Service_Systemsteuerung"), + SERVICE_SYSTEMSTEUERUNG_CONTRAST(199, "Service_Systemsteuerung_Contrast"), + SERVICE_SYSTEMSTEUERUNG_WEBSERVER(200, "Service_Systemsteuerung_Webserver"), + SERVICE_SYSTEMSTEUERUNG_IPADRESSE(201, "Service_Systemsteuerung_IPAdresse"), + SERVICE_SYSTEMSTEUERUNG_FERNWARTUNG(202, "Service_Systemsteuerung_Fernwartung"), + PARALLELESCHALTUNG(203, "Paralleleschaltung"), + SYSEIN_PARALLELESCHALTUNG(204, "SysEin_Paralleleschaltung"), + SPRACHEN_DANSK(205, "Sprachen_DANSK"), + SPRACHEN_PORTUGES(206, "Sprachen_PORTUGES"), + HEIZKURVE_HEIZUNG(207, "Heizkurve_Heizung"), + SYSEIN_MISCHKREIS3(208, "SysEin_Mischkreis3"), + MK3(209, "MK3"), + TEMP_MK3_VORLAUF(210, "Temp_MK3_Vorlauf"), + TEMP_MK3VL_SOLL(211, "Temp_MK3VL_Soll"), + OUT_MISCHER3AUF(212, "OUT_Mischer3Auf"), + OUT_MISCHER3ZU(213, "OUT_Mischer3Zu"), + SYSEIN_REGELUNGMK3(214, "SysEin_RegelungMK3"), + SYSEIN_PERIODEMK3(215, "SysEin_PeriodeMk3"), + SYSEIN_LAUFZEITMK3(216, "SysEin_LaufzeitMk3"), + SYSEIN_KUHL_ZEIT_EIN(217, "SysEin_Kuhl_Zeit_Ein"), + SYSEIN_KUHL_ZEIT_AUS(218, "SysEin_Kuhl_Zeit_Aus"), + ABLAUFZ_ABTAUIN(219, "AblaufZ_AbtauIn"), + WAERMEMENGE_WS(220, "Waermemenge_WS"), + WAERMEMENGE_WQ(221, "Waermemenge_WQ"), + ENLT_MA3(222, "Enlt_MA3"), + ENLT_MZ3(223, "Enlt_MZ3"), + ENLT_FP3(224, "Enlt_FP3"), + OUT_FUP3(225, "OUT_FUP3"), + TEMP_RAUMSTATION2(226, "Temp_Raumstation2"), + TEMP_RAUMSTATION3(227, "Temp_Raumstation3"), + BST_BSTDSW(228, "Bst_BStdSW"), + SPRACHEN_LITAUISCH(229, "Sprachen_LITAUISCH"), + SPRACHEN_ESTNICH(230, "Sprachen_ESTNICH"), + SYSEIN_FERNWARTUNG(231, "SysEin_Fernwartung"), + SPRACHEN_SLOVENISCH(232, "Sprachen_SLOVENISCH"), + EINSTTEMP_TA_EG(233, "EinstTemp_TA_EG"), + EINST_TVLMAX_EG(234, "Einst_TVLmax_EG"), + SYSEIN_POPTNACHLAUF(235, "SysEin_PoptNachlauf"), + RFV_K_KUEHLIN(236, "RFV_K_Kuehlin"), + SYSEIN_EFFIZIENZPUMPENOM(237, "SysEin_EffizienzpumpeNom"), + SYSEIN_EFFIZIENZPUMPEMIN(238, "SysEin_EffizienzpumpeMin"), + SYSEIN_EFFIZIENZPUMPE(239, "SysEin_Effizienzpumpe"), + SYSEIN_WAERMEMENGE(240, "SysEin_Waermemenge"), + SERVICE_WMZ_EFFIZIENZ(241, "Service_WMZ_Effizienz"), + SYSEIN_WM_VERSORGUNG_KORREKTUR(242, "SysEin_Wm_Versorgung_Korrektur"), + SYSEIN_WM_AUSWERTUNG_KORREKTUR(243, "SysEin_Wm_Auswertung_Korrektur"), + IN_ANALOGIN(244, "IN_AnalogIn"), + EINS_SN_EINGABE(245, "Eins_SN_Eingabe"), + OUT_ANALOG_1(246, "OUT_Analog_1"), + OUT_ANALOG_2(247, "OUT_Analog_2"), + SOLAR(248, "Solar"), + SYSEIN_SOLAR(249, "SysEin_Solar"), + EINSTTEMP_TDIFFKOLLMAX(250, "EinstTemp_TDiffKollmax"), + ABLAUFZ_HG_SPERRE(251, "AblaufZ_HG_Sperre"), + SYSEIN_AKT_KUEHLUNG(252, "SysEin_Akt_Kuehlung"), + SYSEIN_VORLAUF_VBO(253, "SysEin_Vorlauf_VBO"), + EINST_KRHYST(254, "Einst_KRHyst"), + EINST_AKT_KUEHL_SPEICHER_MIN(255, "Einst_Akt_Kuehl_Speicher_min"), + EINST_AKT_KUEHL_FREIG_WQE(256, "Einst_Akt_Kuehl_Freig_WQE"), + SYSEIN_ABTZYKMIN(257, "SysEin_AbtZykMin"), + SYSEIN_VD2_ZEIT_MIN(258, "SysEin_VD2_Zeit_Min"), + EINSTTEMP_HYSTERESE_HR_VERKUERZT(259, "EinstTemp_Hysterese_HR_verkuerzt"), + EINST_LUF_FEUCHTESCHUTZ_AKT(260, "Einst_Luf_Feuchteschutz_akt"), + EINST_LUF_REDUZIERT_AKT(261, "Einst_Luf_Reduziert_akt"), + EINST_LUF_NENNLUEFTUNG_AKT(262, "Einst_Luf_Nennlueftung_akt"), + EINST_LUF_INTENSIVLUEFTUNG_AKT(263, "Einst_Luf_Intensivlueftung_akt"), + TEMPERATUR_LUEFTUNG_ZULUFT(264, "Temperatur_Lueftung_Zuluft"), + TEMPERATUR_LUEFTUNG_ABLUFT(265, "Temperatur_Lueftung_Abluft"), + OUT_ANALOG_3(266, "OUT_Analog_3"), + OUT_ANALOG_4(267, "OUT_Analog_4"), + IN_ANALOG_2(268, "IN_Analog_2"), + IN_ANALOG_3(269, "IN_Analog_3"), + IN_SAX(270, "IN_SAX"), + OUT_VZU(271, "OUT_VZU"), + OUT_VAB(272, "OUT_VAB"), + OUT_VSK(273, "OUT_VSK"), + OUT_FRH(274, "OUT_FRH"), + KUHLTEMP_SOLLTEMPMK3(275, "KuhlTemp_SolltempMK3"), + KUHLTEMP_ATDIFFMK3(276, "KuhlTemp_ATDiffMK3"), + IN_SPL(277, "IN_SPL"), + SYSEIN_LUEFTUNGSSTUFEN(278, "SysEin_Lueftungsstufen"), + SYSEIN_MELDUNG_TDI(279, "SysEin_Meldung_TDI"), + SYSEIN_TYP_WZW(280, "SysEin_Typ_WZW"), + BACNET(281, "BACnet"), + SPRACHEN_SLOWAKISCH(282, "Sprachen_SLOWAKISCH"), + SPRACHEN_LETTISCH(283, "Sprachen_LETTISCH"), + SPRACHEN_FINNISCH(284, "Sprachen_FINNISCH"), + KALIBRIERUNG_LWD(285, "Kalibrierung_LWD"), + IN_DURCHFLUSS(286, "IN_Durchfluss"), + LIN_ANSAUG_VERDICHTER(287, "LIN_ANSAUG_VERDICHTER"), + LIN_VDH(288, "LIN_VDH"), + LIN_UH(289, "LIN_UH"), + LIN_DRUCK(290, "LIN_Druck"), + EINST_SOLLWERT_TRL_KUEHLEN(291, "Einst_Sollwert_TRL_Kuehlen"), + ENTL_EXVENTIL(292, "Entl_ExVentil"), + EINST_MEDIUM_WAERMEQUELLE(293, "Einst_Medium_Waermequelle"), + EINST_MULTISPEICHER(294, "Einst_Multispeicher"), + EINST_MINIMALE_RUECKLAUFSOLLTEMPERATUR(295, "Einst_Minimale_Ruecklaufsolltemperatur"), + EINST_PKUEHLTIME(296, "Einst_PKuehlTime"), + SPRACHEN_TUERKISCH(297, "Sprachen_TUERKISCH"), + RBE(298, "RBE"), + EINST_LUF_STUFEN_FAKTOR(299, "Einst_Luf_Stufen_Faktor"), + FREIGABE_ZEIT_ZWE(300, "Freigabe_Zeit_ZWE"), + EINST_MIN_VL_KUEHL(301, "Einst_min_VL_Kuehl"), + ZWE1(302, "ZWE1"), + ZWE2(303, "ZWE2"), + ZWE3(304, "ZWE3"), + SEC(305, "SEC"), + HZIO(306, "HZIO"), + WPIO(307, "WPIO"), + LIN_ANSAUG_VERDAMPFER(308, "LIN_ANSAUG_VERDAMPFER"), + LIN_MULTI1(309, "LIN_MULTI1"), + LIN_MULTI2(310, "LIN_MULTI2"), + EINST_LEISTUNG_ZWE(311, "Einst_Leistung_ZWE"), + SPRACHEN_ESPANOL(312, "Sprachen_ESPANOL"), + TEMP_BW_OBEN(313, "Temp_BW_oben"), + MAXIO(314, "MAXIO"), + OUT_ABTAUWUNSCH(315, "OUT_Abtauwunsch"), + SMARTGRID(316, "SmartGrid"), + DREHZAHLGEREGELT(317, "Drehzahlgeregelt"), + P155_INVERTER(318, "P155_Inverter"), + LEISTUNGSFREIGABE(319, "Leistungsfreigabe"), + EINST_VORL_AKT_KUEHL(320, "Einst_Vorl_akt_Kuehl"), + EINST_ABTAUEN_IM_WARMWASSER(321, "Einst_Abtauen_im_Warmwasser"), + WAERMEMENGE_ZWE(32, "Waermemenge_ZWE"); + + private final String name; + private final Integer code; + + private HeatpumpVisibility(Integer code, String name) { + this.code = code; + this.name = name; + } + + public String getName() { + return name; + } + + public Integer getCode() { + return code; + } + + @Override + public String toString() { + return code + ": " + name; + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidChannelException.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidChannelException.java new file mode 100644 index 0000000000000..b8b484f1f3f5c --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidChannelException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link InvalidChannelException} is thrown if a channel can't be found + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class InvalidChannelException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidChannelException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidOperationModeException.java b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidOperationModeException.java new file mode 100644 index 0000000000000..54688a31b0321 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/java/org/openhab/binding/luxtronikheatpump/internal/exceptions/InvalidOperationModeException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.luxtronikheatpump.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link InvalidOperationModeException} is thrown for invalid operation modes + * + * @author Stefan Giehl - Initial contribution + */ +@NonNullByDefault +public class InvalidOperationModeException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidOperationModeException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..ca5ce9ed07aa2 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Luxtronik Heatpump Binding + This is the binding for a Heatpump with Luxtronik control. + + diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump.properties b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump.properties new file mode 100644 index 0000000000000..dcab406312b6d --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump.properties @@ -0,0 +1,131 @@ +channel-type.luxtronikheatpump.menuStateLine1.state.option.0 = Heat pump runs +channel-type.luxtronikheatpump.menuStateLine1.state.option.1 = Heat pump stopped +channel-type.luxtronikheatpump.menuStateLine1.state.option.2 = Heat pump coming +channel-type.luxtronikheatpump.menuStateLine1.state.option.3 = Error code memory location 0 +channel-type.luxtronikheatpump.menuStateLine1.state.option.4 = Defrost +channel-type.luxtronikheatpump.menuStateLine1.state.option.5 = Wait for LIN connection +channel-type.luxtronikheatpump.menuStateLine1.state.option.6 = Compressor heats up +channel-type.luxtronikheatpump.menuStateLine1.state.option.7 = Pump flow +channel-type.luxtronikheatpump.menuStateLine2.state.option.0 = since +channel-type.luxtronikheatpump.menuStateLine2.state.option.1 = in +channel-type.luxtronikheatpump.menuStateLine3.state.option.0 = Heating mode +channel-type.luxtronikheatpump.menuStateLine3.state.option.1 = No requirement +channel-type.luxtronikheatpump.menuStateLine3.state.option.2 = Mains switch-on delay +channel-type.luxtronikheatpump.menuStateLine3.state.option.3 = Switching cycle lock +channel-type.luxtronikheatpump.menuStateLine3.state.option.4 = Blocking time +channel-type.luxtronikheatpump.menuStateLine3.state.option.5 = Service water +channel-type.luxtronikheatpump.menuStateLine3.state.option.6 = Info bakeout program +channel-type.luxtronikheatpump.menuStateLine3.state.option.7 = Defrost +channel-type.luxtronikheatpump.menuStateLine3.state.option.8 = Pump flow +channel-type.luxtronikheatpump.menuStateLine3.state.option.9 = Thermal disinfection +channel-type.luxtronikheatpump.menuStateLine3.state.option.10 = Cooling mode +channel-type.luxtronikheatpump.menuStateLine3.state.option.12 = Swimming pool / Photovoltaics +channel-type.luxtronikheatpump.menuStateLine3.state.option.13 = Heating ext. energy source +channel-type.luxtronikheatpump.menuStateLine3.state.option.14 = Service water ext. energy source +channel-type.luxtronikheatpump.menuStateLine3.state.option.16 = Flow monitoring +channel-type.luxtronikheatpump.menuStateLine3.state.option.17 = Second heat generator 1 operation + +channel-type.luxtronikheatpump.operationMode.state.option.0 = Auto +channel-type.luxtronikheatpump.operationMode.state.option.1 = Auxiliary heater +channel-type.luxtronikheatpump.operationMode.state.option.2 = Party +channel-type.luxtronikheatpump.operationMode.state.option.3 = Holiday +channel-type.luxtronikheatpump.operationMode.state.option.4 = Off + +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.1 = Heat pump malfunction +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.2 = Facility malfunction +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.3 = Second heat generator operating mode +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.4 = Utility lock +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.5 = Running dew (LW units only) +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.6 = Temperature application limit maximum +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.7 = Temperature application limit minimum +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.8 = Lower operating limit +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.9 = No requirement + +channel-type.luxtronikheatpump.errorCodeX.state.option.701 = Error low pressure - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.702 = Low pressure stop - RESET automatic +channel-type.luxtronikheatpump.errorCodeX.state.option.703 = Antifreeze - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.704 = Error hot gas - reset in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.705 = Motor protection VEN - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.706 = Motor protection BCP - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.707 = Coding of heat pump - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.708 = Return sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.709 = Flow sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.710 = Hot gas sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.711 = External temp. sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.712 = Domestic hot water sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.713 = HS-on sensor - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.714 = Hot gas SW - Reset in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.715 = High-pressure switch-off - RESET automatic +channel-type.luxtronikheatpump.errorCodeX.state.option.716 = High-pressure fault - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.717 = Flow HS - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.718 = Max. outside temp. - reset automatic +channel-type.luxtronikheatpump.errorCodeX.state.option.719 = Min. outside temp. - reset automatic +channel-type.luxtronikheatpump.errorCodeX.state.option.720 = HS temperature - reset automatic in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.721 = Low-pressure switch-off - reset automatic +channel-type.luxtronikheatpump.errorCodeX.state.option.722 = Tempdiff HW - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.723 = Tempdiff SW - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.724 = Tempdiff defrosting - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.725 = System error DHW - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.726 = Sensor mixing circ 1 - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.727 = Brine pressure - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.728 = Sensor HS Off - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.729 = Rotating field error - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.730 = Screed heating error - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.731 = Timeout TDI +channel-type.luxtronikheatpump.errorCodeX.state.option.732 = Cooling fault - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.733 = Anode fault - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.734 = Anode fault - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.735 = Error Ext. En - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.736 = Error solar collector - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.737 = Error solar tank - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.738 = Error mixing circle 2 - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.739 = Error mixing circle 3 - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.750 = Return sensor external - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.751 = Phase monitoring fault +channel-type.luxtronikheatpump.errorCodeX.state.option.752 = Flow error +channel-type.luxtronikheatpump.errorCodeX.state.option.755 = Lost connection to slave - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.756 = Lost connection to master - Please call fitter +channel-type.luxtronikheatpump.errorCodeX.state.option.757 = Low-pressure fault in W/W-appliance +channel-type.luxtronikheatpump.errorCodeX.state.option.758 = Defrosting malfunction +channel-type.luxtronikheatpump.errorCodeX.state.option.759 = TDI Message +channel-type.luxtronikheatpump.errorCodeX.state.option.760 = Defrosting fault +channel-type.luxtronikheatpump.errorCodeX.state.option.761 = LIN timeout +channel-type.luxtronikheatpump.errorCodeX.state.option.762 = Sensor evaporator intake +channel-type.luxtronikheatpump.errorCodeX.state.option.763 = Sensor compressor intake +channel-type.luxtronikheatpump.errorCodeX.state.option.764 = Sensor compressor heater +channel-type.luxtronikheatpump.errorCodeX.state.option.765 = Overheating +channel-type.luxtronikheatpump.errorCodeX.state.option.766 = Compressors functional range +channel-type.luxtronikheatpump.errorCodeX.state.option.767 = STB E-Rod +channel-type.luxtronikheatpump.errorCodeX.state.option.768 = Flow monitoring +channel-type.luxtronikheatpump.errorCodeX.state.option.769 = Pump control +channel-type.luxtronikheatpump.errorCodeX.state.option.770 = Low superheat +channel-type.luxtronikheatpump.errorCodeX.state.option.771 = High superheat +channel-type.luxtronikheatpump.errorCodeX.state.option.776 = limit of application-CP +channel-type.luxtronikheatpump.errorCodeX.state.option.777 = Expansion valve +channel-type.luxtronikheatpump.errorCodeX.state.option.778 = Low pressure sensor +channel-type.luxtronikheatpump.errorCodeX.state.option.779 = High pressure sensor +channel-type.luxtronikheatpump.errorCodeX.state.option.780 = EVI sensor +channel-type.luxtronikheatpump.errorCodeX.state.option.781 = Liquid temp. sensor before EXV +channel-type.luxtronikheatpump.errorCodeX.state.option.782 = Suction gas EVI temp. sensor +channel-type.luxtronikheatpump.errorCodeX.state.option.783 = Communication SEC board - Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.784 = VSS lockdown +channel-type.luxtronikheatpump.errorCodeX.state.option.785 = SEC-Board defective +channel-type.luxtronikheatpump.errorCodeX.state.option.786 = Communication SEC board - Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.787 = VD alert +channel-type.luxtronikheatpump.errorCodeX.state.option.788 = Major VSS fault +channel-type.luxtronikheatpump.errorCodeX.state.option.789 = LIN/Encoding not found +channel-type.luxtronikheatpump.errorCodeX.state.option.790 = Major VSS fault +channel-type.luxtronikheatpump.errorCodeX.state.option.791 = ModBus Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.792 = LIN-connection lost +channel-type.luxtronikheatpump.errorCodeX.state.option.793 = Inverter Temperature +channel-type.luxtronikheatpump.errorCodeX.state.option.794 = Overvoltage +channel-type.luxtronikheatpump.errorCodeX.state.option.795 = Undervoltage +channel-type.luxtronikheatpump.errorCodeX.state.option.796 = Safety switch off +channel-type.luxtronikheatpump.errorCodeX.state.option.797 = MLRH is not supported +channel-type.luxtronikheatpump.errorCodeX.state.option.798 = ModBus Fan +channel-type.luxtronikheatpump.errorCodeX.state.option.799 = ModBus ASB +channel-type.luxtronikheatpump.errorCodeX.state.option.800 = Desuperheater-error +channel-type.luxtronikheatpump.errorCodeX.state.option.802 = Switchbox fan +channel-type.luxtronikheatpump.errorCodeX.state.option.803 = Switchbox fan +channel-type.luxtronikheatpump.errorCodeX.state.option.806 = ModBus SEC +channel-type.luxtronikheatpump.errorCodeX.state.option.807 = Lost ModBus communication diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump_de.properties b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump_de.properties new file mode 100644 index 0000000000000..b1b32f6ff6157 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/i18n/luxtronikheatpump_de.properties @@ -0,0 +1,395 @@ +# binding +binding.luxtronikheatpump.name = Luxtronik Wärmepumpen Binding +binding.luxtronikheatpump.description = Dieses Binding integriert Wärmepumpen mit einer Luxtronik-Steuerung. + +# thing types +thing-type.luxtronikheatpump.heatpump.label = Luxtronik Wärmepumpe +thing-type.luxtronikheatpump.heatpump.description = Integriert eine Wärmepumpe mit einer Luxtronik-Steuerung. + +# thing type configuration +thing-type.config.luxtronikheatpump.heatpump.ipAddress.label = IP Adresse +thing-type.config.luxtronikheatpump.heatpump.ipAddress.description = IP Adresse der Wärmepumpe +thing-type.config.luxtronikheatpump.heatpump.port.label = Port +thing-type.config.luxtronikheatpump.heatpump.port.description = Port der zum Aufbau einer Verbindung zur Wärmepumpe verwendet werden soll. Standardwert ist 8889. Für Wärmepumpen mit einer Firmware version vor V1.73 muss der Port 8888 verwendet werden. +thing-type.config.luxtronikheatpump.heatpump.refresh.label = Aktualisierungsintervall +thing-type.config.luxtronikheatpump.heatpump.refresh.description = Aktualisierungsintervall in Sekunden. Standardwert ist 300 +thing-type.config.luxtronikheatpump.heatpump.showAllChannels.label = Zeige auch nicht verfügbare Kanäle +thing-type.config.luxtronikheatpump.heatpump.showAllChannels.description = Kanäle die auf einer Wärmepumpe nicht verfügbar sind werden standardmäßig ausgeblendet. + +# channel types +channel-type.luxtronikheatpump.temperatureHeatingCircuitFlow.label = Vorlauftemp. Heizkreis +channel-type.luxtronikheatpump.temperatureHeatingCircuitReturn.label = Rücklauftemp. Heizkreis +channel-type.luxtronikheatpump.temperatureHeatingCircuitReturnTarget.label = Rücklauf-Soll Heizkreis +channel-type.luxtronikheatpump.temperatureBufferTankReturn.label = Rücklauftemp. im Trennspeicher +channel-type.luxtronikheatpump.temperatureHotGas.label = Heißgastemp. +channel-type.luxtronikheatpump.temperatureOutside.label = Außentemp. +channel-type.luxtronikheatpump.temperatureOutsideMean.label = Durchschnittstemp. Außen über 24 h (Funktion Heizgrenze) +channel-type.luxtronikheatpump.temperatureHotWater.label = Warmwasser Ist-Temp. +channel-type.luxtronikheatpump.temperatureHotWaterTarget.label = Warmwasser Soll-Temp. +channel-type.luxtronikheatpump.temperatureHeatSourceInlet.label = Wärmequellen-Eintrittstemp. +channel-type.luxtronikheatpump.temperatureHeatSourceOutlet.label = Wärmequellen-Austrittstemp. +channel-type.luxtronikheatpump.temperatureMixingCircuit1Flow.label = Mischkreis 1 Vorlauftemp. +channel-type.luxtronikheatpump.temperatureMixingCircuit1FlowTarget.label = Mischkreis 1 Vorlauf-Soll-Temp. +channel-type.luxtronikheatpump.temperatureRoomStation.label = Raumtemp. Raumstation 1 +channel-type.luxtronikheatpump.temperatureMixingCircuit2Flow.label = Mischkreis 2 Vorlauftemp. +channel-type.luxtronikheatpump.temperatureMixingCircuit2FlowTarget.label = Mischkreis 2 Vorlauf-Soll-Temp. +channel-type.luxtronikheatpump.temperatureSolarCollector.label = Fühler Solarkollektor +channel-type.luxtronikheatpump.temperatureSolarTank.label = Fühler Solarspeicher +channel-type.luxtronikheatpump.temperatureExternalEnergySource.label = Fühler externe Energiequelle +channel-type.luxtronikheatpump.inputASD.label = Eingang "Abtauende, Soledruck, Durchfluss" +channel-type.luxtronikheatpump.inputHotWaterThermostat.label = Eingang "Brauchwarmwasserthermostat" +channel-type.luxtronikheatpump.inputUtilityLock.label = Eingang "EVU-Sperre" +channel-type.luxtronikheatpump.inputHighPressureCoolingCircuit.label = Eingang "Hochdruck Kältekreis" +channel-type.luxtronikheatpump.inputMotorProtectionOK.label = Eingang "Motorschutz OK" +channel-type.luxtronikheatpump.inputLowPressure.label = Eingang "Niederdruck" +channel-type.luxtronikheatpump.inputPEX.label = Eingang "Überwachungskontakt für Potentiostat" +channel-type.luxtronikheatpump.inputSwimmingPoolThermostat.label = Eingang "Schwimmbadthermostat" +channel-type.luxtronikheatpump.outputDefrostValve.label = Ausgang "Abtauventil" +channel-type.luxtronikheatpump.outputBUP.label = Ausgang "Brauchwasserpumpe/Umstellventil" +channel-type.luxtronikheatpump.outputHeatingCirculationPump.label = Ausgang "Heizungsumwälzpumpe" +channel-type.luxtronikheatpump.outputMixingCircuit1Open.label = Ausgang "Mischkreis 1 Auf" +channel-type.luxtronikheatpump.outputMixingCircuit1Closed.label = Ausgang "Mischkreis 1 Zu" +channel-type.luxtronikheatpump.outputVentilation.label = Ausgang "Ventilation (Lüftung)" +channel-type.luxtronikheatpump.outputVBO.label = Ausgang "Solepumpe/Ventilator" +channel-type.luxtronikheatpump.outputCompressor1.label = Ausgang "Verdichter 1" +channel-type.luxtronikheatpump.outputCompressor2.label = Ausgang "Verdichter 2" +channel-type.luxtronikheatpump.outputCirculationPump.label = Ausgang "Zirkulationspumpe" +channel-type.luxtronikheatpump.outputZUP.label = Ausgang "Zusatzumwälzpumpe" +channel-type.luxtronikheatpump.outputControlSignalAdditionalHeating.label = Ausgang "Steuersignal Zusatzheizung v. Heizung" +channel-type.luxtronikheatpump.outputFaultSignalAdditionalHeating.label = Ausgang "Steuersignal Zusatzheizung/Störsignal" +channel-type.luxtronikheatpump.outputAuxiliaryHeater3.label = Ausgang "Zusatzheizung 3" +channel-type.luxtronikheatpump.outputMixingCircuitPump2.label = Ausgang "Pumpe Mischkreis 2" +channel-type.luxtronikheatpump.outputSolarChargePump.label = Ausgang "Solarladepumpe" +channel-type.luxtronikheatpump.outputSwimmingPoolPump.label = Ausgang "Schwimmbadpumpe" +channel-type.luxtronikheatpump.outputMixingCircuit2Closed.label = Ausgang "Mischkreis 2 Zu" +channel-type.luxtronikheatpump.outputMixingCircuit2Open.label = Ausgang "Mischkreis 2 Auf" +channel-type.luxtronikheatpump.runtimeTotalCompressor1.label = Betriebszeit Verdichter 1 +channel-type.luxtronikheatpump.pulsesCompressor1.label = Impulse Verdichter 1 +channel-type.luxtronikheatpump.runtimeTotalCompressor2.label = Betriebszeit Verdichter 2 +channel-type.luxtronikheatpump.pulsesCompressor2.label = Impulse Verdichter 2 +channel-type.luxtronikheatpump.runtimeTotalSecondHeatGenerator1.label = Betriebszeit Zweiter Wärmeerzeuger 1 +channel-type.luxtronikheatpump.runtimeTotalSecondHeatGenerator2.label = Betriebszeit Zweiter Wärmeerzeuger 2 +channel-type.luxtronikheatpump.runtimeTotalSecondHeatGenerator3.label = Betriebszeit Zweiter Wärmeerzeuger 3 +channel-type.luxtronikheatpump.runtimeTotalHeatPump.label = Betriebszeit Wärmepumpe +channel-type.luxtronikheatpump.runtimeTotalHeating.label = Betriebszeit Heizung +channel-type.luxtronikheatpump.runtimeTotalHotWater.label = Betriebszeit Warmwasser +channel-type.luxtronikheatpump.runtimeTotalCooling.label = Betriebszeit Kühlung +channel-type.luxtronikheatpump.runtimeCurrentHeatPump.label = Wärmepumpe läuft seit +channel-type.luxtronikheatpump.runtimeCurrentSecondHeatGenerator1.label = Zweiter Wärmeerzeuger 1 läuft seit +channel-type.luxtronikheatpump.runtimeCurrentSecondHeatGenerator2.label = Zweiter Wärmeerzeuger 2 läuft seit +channel-type.luxtronikheatpump.mainsOnDelay.label = Netzeinschaltverzögerung +channel-type.luxtronikheatpump.switchingCycleLockOff.label = Schaltspielsperre Aus +channel-type.luxtronikheatpump.switchingCycleLockOn.label = Schaltspielsperre Ein +channel-type.luxtronikheatpump.compressorIdleTime.label = Verdichter-Standzeit +channel-type.luxtronikheatpump.heatingControllerMoreTime.label = Heizungsregler Mehr-Zeit +channel-type.luxtronikheatpump.heatingControllerLessTime.label = Heizungsregler Weniger-Zeit +channel-type.luxtronikheatpump.runtimeCurrentThermalDisinfection.label = Thermische Desinfektion läuft seit +channel-type.luxtronikheatpump.timeHotWaterLock.label = Sperre Warmwasser +channel-type.luxtronikheatpump.bivalenceStage.label = Bivalenzstufe +channel-type.luxtronikheatpump.bivalenceStage.state.option.1 = ein Verdichter darf laufen +channel-type.luxtronikheatpump.bivalenceStage.state.option.2 = zwei Verdichter dürfen laufen +channel-type.luxtronikheatpump.bivalenceStage.state.option.3 = zusätzlicher Wärmeerzeuger darf mitlaufen +channel-type.luxtronikheatpump.operatingStatus.label = Betriebszustand +channel-type.luxtronikheatpump.operatingStatus.state.option.0 = Heizen +channel-type.luxtronikheatpump.operatingStatus.state.option.1 = Warmwasser +channel-type.luxtronikheatpump.operatingStatus.state.option.2 = Schwimmbad / Photovoltaik +channel-type.luxtronikheatpump.operatingStatus.state.option.3 = EVU-Sperre +channel-type.luxtronikheatpump.operatingStatus.state.option.4 = Abtauen +channel-type.luxtronikheatpump.operatingStatus.state.option.5 = Keine Anforderung +channel-type.luxtronikheatpump.operatingStatus.state.option.6 = Heizen ext. Energiequelle +channel-type.luxtronikheatpump.operatingStatus.state.option.7 = Kühlbetrieb +channel-type.luxtronikheatpump.errorTime0.label = Zeitstempel Fehler 0 im Speicher +channel-type.luxtronikheatpump.errorTime1.label = Zeitstempel Fehler 1 im Speicher +channel-type.luxtronikheatpump.errorTime2.label = Zeitstempel Fehler 2 im Speicher +channel-type.luxtronikheatpump.errorTime3.label = Zeitstempel Fehler 3 im Speicher +channel-type.luxtronikheatpump.errorTime4.label = Zeitstempel Fehler 4 im Speicher +channel-type.luxtronikheatpump.errorCode0.label = Fehlercode Fehler 0 im Speicher +channel-type.luxtronikheatpump.errorCode1.label = Fehlercode Fehler 1 im Speicher +channel-type.luxtronikheatpump.errorCode2.label = Fehlercode Fehler 2 im Speicher +channel-type.luxtronikheatpump.errorCode3.label = Fehlercode Fehler 3 im Speicher +channel-type.luxtronikheatpump.errorCode4.label = Fehlercode Fehler 4 im Speicher +channel-type.luxtronikheatpump.errorCountInMemory.label = Anzahl der Fehler im Speicher +channel-type.luxtronikheatpump.shutdownReason0.label = Grund Abschaltung 0 im Speicher +channel-type.luxtronikheatpump.shutdownReason1.label = Grund Abschaltung 1 im Speicher +channel-type.luxtronikheatpump.shutdownReason2.label = Grund Abschaltung 2 im Speicher +channel-type.luxtronikheatpump.shutdownReason3.label = Grund Abschaltung 3 im Speicher +channel-type.luxtronikheatpump.shutdownReason4.label = Grund Abschaltung 4 im Speicher +channel-type.luxtronikheatpump.shutdownTime0.label = Zeitstempel Abschaltung 0 im Speicher +channel-type.luxtronikheatpump.shutdownTime1.label = Zeitstempel Abschaltung 1 im Speicher +channel-type.luxtronikheatpump.shutdownTime2.label = Zeitstempel Abschaltung 2 im Speicher +channel-type.luxtronikheatpump.shutdownTime3.label = Zeitstempel Abschaltung 3 im Speicher +channel-type.luxtronikheatpump.shutdownTime4.label = Zeitstempel Abschaltung 4 im Speicher +channel-type.luxtronikheatpump.comfortBoardInstalled.label = Comfort Platine installiert +channel-type.luxtronikheatpump.menuStateLine.label = Status +channel-type.luxtronikheatpump.menuStateLine1.label = Status Zeile 1 +channel-type.luxtronikheatpump.menuStateLine1.state.option.0 = Wärmepumpe läuft +channel-type.luxtronikheatpump.menuStateLine1.state.option.1 = Wärmepumpe steht +channel-type.luxtronikheatpump.menuStateLine1.state.option.2 = Wärmepumpe kommt +channel-type.luxtronikheatpump.menuStateLine1.state.option.3 = Fehlercode Speicherplatz 0 +channel-type.luxtronikheatpump.menuStateLine1.state.option.4 = Abtauen +channel-type.luxtronikheatpump.menuStateLine1.state.option.5 = Warte auf LIN-Verbindung +channel-type.luxtronikheatpump.menuStateLine1.state.option.6 = Verdichter heizt auf +channel-type.luxtronikheatpump.menuStateLine1.state.option.7 = Pumpenvorlauf +channel-type.luxtronikheatpump.menuStateLine2.label = Status Zeile 2 +channel-type.luxtronikheatpump.menuStateLine2.state.option.0 = seit +channel-type.luxtronikheatpump.menuStateLine2.state.option.1 = in +channel-type.luxtronikheatpump.menuStateLine3.label = Status Zeile 3 +channel-type.luxtronikheatpump.menuStateLine3.state.option.0 = Heizbetrieb +channel-type.luxtronikheatpump.menuStateLine3.state.option.1 = Keine Anforderung +channel-type.luxtronikheatpump.menuStateLine3.state.option.2 = Netz-Einschaltverzögerung +channel-type.luxtronikheatpump.menuStateLine3.state.option.3 = Schaltspielsperre +channel-type.luxtronikheatpump.menuStateLine3.state.option.4 = Sperrzeit +channel-type.luxtronikheatpump.menuStateLine3.state.option.5 = Brauchwasser +channel-type.luxtronikheatpump.menuStateLine3.state.option.6 = Info Ausheizprogramm +channel-type.luxtronikheatpump.menuStateLine3.state.option.7 = Abtauen +channel-type.luxtronikheatpump.menuStateLine3.state.option.8 = Pumpenvorlauf +channel-type.luxtronikheatpump.menuStateLine3.state.option.9 = Thermische Desinfektion +channel-type.luxtronikheatpump.menuStateLine3.state.option.10 = Kühlbetrieb +channel-type.luxtronikheatpump.menuStateLine3.state.option.12 = Schwimmbad / Photovoltaik +channel-type.luxtronikheatpump.menuStateLine3.state.option.13 = Heizen ext. Energiequelle +channel-type.luxtronikheatpump.menuStateLine3.state.option.14 = Brauchwasser ext. Energiequelle +channel-type.luxtronikheatpump.menuStateLine3.state.option.16 = Durchflussüberachung +channel-type.luxtronikheatpump.menuStateLine3.state.option.17 = Zweiter Wärmeerzeuger 1 Betrieb +channel-type.luxtronikheatpump.menuStateTime.label = Status Zeit Zeile 2 +channel-type.luxtronikheatpump.bakeoutProgramStage.label = Stufe Ausheizprogramm +channel-type.luxtronikheatpump.bakeoutProgramTemperature.label = Temp. Ausheizprogramm +channel-type.luxtronikheatpump.bakeoutProgramTime.label = Laufzeit Ausheizprogramm +channel-type.luxtronikheatpump.iconHotWater.label = Brauchwasser aktiv/inaktiv Symbol +channel-type.luxtronikheatpump.iconHeater.label = Heizung Symbol +channel-type.luxtronikheatpump.iconMixingCircuit1.label = Mischkreis 1 Symbol +channel-type.luxtronikheatpump.iconMixingCircuit2.label = Mischkreis 2 Symbol +channel-type.luxtronikheatpump.shortProgramSetting.label = Einstellung Kurzprogramm +channel-type.luxtronikheatpump.statusSlave1.label = Status Slave 1 +channel-type.luxtronikheatpump.statusSlave2.label = Status Slave 2 +channel-type.luxtronikheatpump.statusSlave3.label = Status Slave 3 +channel-type.luxtronikheatpump.statusSlave4.label = Status Slave 4 +channel-type.luxtronikheatpump.statusSlave5.label = Status Slave 5 +channel-type.luxtronikheatpump.currentTimestamp.label = Aktuelle Zeit der Wärmepumpe +channel-type.luxtronikheatpump.iconMixingCircuit3.label = Mischkreis 3 Symbol +channel-type.luxtronikheatpump.temperatureMixingCircuit3FlowTarget.label = Mischkreis 3 Vorlauf-Soll-Temp. +channel-type.luxtronikheatpump.temperatureMixingCircuit3Flow.label = Mischkreis 3 Vorlauftemp. +channel-type.luxtronikheatpump.outputMixingCircuit3Close.label = Ausgang "Mischkreis 3 Zu" +channel-type.luxtronikheatpump.outputMixingCircuit3Open.label = Ausgang "Mischkreis 3 Auf" +channel-type.luxtronikheatpump.outputMixingCircuitPump3.label = Pumpe Mischkreis 3 +channel-type.luxtronikheatpump.timeUntilDefrost.label = Zeit bis Abtauen +channel-type.luxtronikheatpump.temperatureRoomStation2.label = Raumtemp. Raumstation 2 +channel-type.luxtronikheatpump.temperatureRoomStation3.label = Raumtemp. Raumstation 3 +channel-type.luxtronikheatpump.iconTimeSwitchSwimmingPool.label = Schaltuhr-Schwimmbad-Symbol +channel-type.luxtronikheatpump.runtimeTotalSwimmingPool.label = Betriebszeit Schwimmbad +channel-type.luxtronikheatpump.coolingRelease.label = Freigabe Kühlung +channel-type.luxtronikheatpump.inputAnalog.label = Analoges Eingangssignal +channel-type.luxtronikheatpump.iconCirculationPump.label = Zirkulationspumpensymbol +channel-type.luxtronikheatpump.heatMeterHeating.label = Wärmemengenzähler Heizung +channel-type.luxtronikheatpump.heatMeterHotWater.label = Wärmemengenzähler Brauchwasser +channel-type.luxtronikheatpump.heatMeterSwimmingPool.label = Wärmemengenzähler Schwimmbad +channel-type.luxtronikheatpump.heatMeterTotalSinceReset.label = Wärmemengenzähler Gesamt (seit Reset) +channel-type.luxtronikheatpump.heatMeterFlowRate.label = Wärmemengenzähler Durchfluss +channel-type.luxtronikheatpump.outputAnalog1.label = Analog Ausgang 1 +channel-type.luxtronikheatpump.outputAnalog2.label = Analog Ausgang 2 +channel-type.luxtronikheatpump.timeLockSecondHotGasCompressor.label = Sperre zweiter Verdichter Heißgas +channel-type.luxtronikheatpump.temperatureSupplyAir.label = Zulufttemp. +channel-type.luxtronikheatpump.temperatureExhaustAir.label = Ablufttemp. +channel-type.luxtronikheatpump.runtimeTotalSolar.label = Betriebszeit Solar +channel-type.luxtronikheatpump.outputAnalog3.label = Analog Ausgang 3 +channel-type.luxtronikheatpump.outputAnalog4.label = Analog Ausgang 4 +channel-type.luxtronikheatpump.outputSupplyAirFan.label = Zuluft Ventilator (Abtaufunktion) +channel-type.luxtronikheatpump.outputExhaustFan.label = Abluft Ventilator +channel-type.luxtronikheatpump.outputVSK.label = Ausgang VSK +channel-type.luxtronikheatpump.outputFRH.label = Ausgang FRH +channel-type.luxtronikheatpump.inputAnalog2.label = Analog Eingang 2 +channel-type.luxtronikheatpump.inputAnalog3.label = Analog Eingang 3 +channel-type.luxtronikheatpump.inputSAX.label = Eingang SAX +channel-type.luxtronikheatpump.inputSPL.label = Eingang SPL +channel-type.luxtronikheatpump.ventilationBoardInstalled.label = Lüftungsplatine verbaut +channel-type.luxtronikheatpump.flowRateHeatSource.label = Durchfluss Wärmequelle +channel-type.luxtronikheatpump.linBusInstalled.label = LIN BUS verbaut +channel-type.luxtronikheatpump.temperatureSuctionEvaporator.label = Temp. Ansaug Verdampfer +channel-type.luxtronikheatpump.temperatureSuctionCompressor.label = Temp. Ansaug Verdichter +channel-type.luxtronikheatpump.temperatureCompressorHeating.label = Temp. Verdichter Heizung +channel-type.luxtronikheatpump.temperatureOverheating.label = Überhitzung +channel-type.luxtronikheatpump.temperatureOverheatingTarget.label = Überhitzung Soll +channel-type.luxtronikheatpump.highPressure.label = Hochdruck +channel-type.luxtronikheatpump.lowPressure.label = Niederdruck +channel-type.luxtronikheatpump.outputCompressorHeating.label = Ausgang Verdichterheizung +channel-type.luxtronikheatpump.controlSignalCirculatingPump.label = Steuersignal Umwälzpumpe +channel-type.luxtronikheatpump.fanSpeed.label = Ventilatordrehzahl +channel-type.luxtronikheatpump.temperatureSafetyLimitFloorHeating.label = Sicherheits-Temp.-Begrenzer Fußbodenheizung +channel-type.luxtronikheatpump.powerTargetValue.label = Leistung Sollwert +channel-type.luxtronikheatpump.powerActualValue.label = Leistung Istwert +channel-type.luxtronikheatpump.temperatureFlowTarget.label = Temp. Vorlauf Soll +channel-type.luxtronikheatpump.operatingStatusSECBoard.label = Betriebszustand SEC Board +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option0 = Aus +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option1 = Kühlung +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option2 = Heizung +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option3 = Störung +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option4 = Übergang +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option5 = Abtauen +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option6 = Warte +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option7 = Warte +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option8 = Übergang +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option9 = Stop +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option10 = Manuell +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option11 = Simulation Start +channel-type.luxtronikheatpump.operatingStatusSECBoard.state.option12 = EVU-Sperre +channel-type.luxtronikheatpump.fourWayValve.label = Vierwegeventil +channel-type.luxtronikheatpump.compressorSpeed.label = Verdichterdrehzahl +channel-type.luxtronikheatpump.temperatureCompressorEVI.label = Verdichtertemp. EVI (Enhanced Vapour Injection) +channel-type.luxtronikheatpump.temperatureIntakeEVI.label = Ansaugtemp. EVI +channel-type.luxtronikheatpump.temperatureOverheatingEVI.label = Überhitzung EVI +channel-type.luxtronikheatpump.temperatureOverheatingTargetEVI.label = Überhitzung EVI Sollwert +channel-type.luxtronikheatpump.temperatureCondensation.label = Kondensationstemp. +channel-type.luxtronikheatpump.temperatureLiquidEEV.label = Flüssigtemp. EEV (elektronisches Expansionsventil) +channel-type.luxtronikheatpump.temperatureHypothermiaEEV.label = Unterkühlung EEV +channel-type.luxtronikheatpump.pressureEVI.label = Druck EVI +channel-type.luxtronikheatpump.voltageInverter.label = Spannung Inverter +channel-type.luxtronikheatpump.temperatureHotGas2.label = Temp.-Fühler Heißgas 2 +channel-type.luxtronikheatpump.temperatureHeatSourceInlet2.label = Temp.-Fühler Wärmequelleneintritt 2 +channel-type.luxtronikheatpump.temperatureIntakeEvaporator2.label = Ansaugtemp. Verdampfer 2 +channel-type.luxtronikheatpump.temperatureIntakeCompressor2.label = Ansaugtemp. Verdichter 2 +channel-type.luxtronikheatpump.temperatureCompressor2Heating.label = Temp. Verdichter 2 Heizung +channel-type.luxtronikheatpump.temperatureOverheating2.label = Überhitzung 2 +channel-type.luxtronikheatpump.temperatureOverheatingTarget2.label = Überhitzung Soll 2 +channel-type.luxtronikheatpump.highPressure2.label = Hochdruck 2 +channel-type.luxtronikheatpump.lowPressure2.label = Niederdruck 2 +channel-type.luxtronikheatpump.inputSwitchHighPressure2.label = Eingang Druckschalter Hochdruck 2 +channel-type.luxtronikheatpump.outputDefrostValve2.label = Ausgang Abtauventil 2 +channel-type.luxtronikheatpump.outputVBO2.label = Ausgang Solepumpe/Ventilator 2 +channel-type.luxtronikheatpump.outputCompressor1_2.label = Ausgang Verdichter 1 / 2 +channel-type.luxtronikheatpump.outputCompressorHeating2.label = Ausgang Verdichter Heizung 2 +channel-type.luxtronikheatpump.secondShutdownReason0.label = Grund Abschaltung 0 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownReason1.label = Grund Abschaltung 1 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownReason2.label = Grund Abschaltung 2 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownReason3.label = Grund Abschaltung 3 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownReason4.label = Grund Abschaltung 4 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownTime0.label = Zeitstempel Abschaltung 0 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownTime1.label = Zeitstempel Abschaltung 1 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownTime2.label = Zeitstempel Abschaltung 2 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownTime3.label = Zeitstempel Abschaltung 3 im Speicher 2 +channel-type.luxtronikheatpump.secondShutdownTime4.label = Zeitstempel Abschaltung 4 im Speicher 2 +channel-type.luxtronikheatpump.temperatureRoom.label = Raumtemp. Istwert +channel-type.luxtronikheatpump.temperatureRoomTarget.label = Raumtemp. Sollwert +channel-type.luxtronikheatpump.temperatureHotWaterTop.label = Temp. Brauchwasser Oben +channel-type.luxtronikheatpump.frequencyCompressor.label = Verdichterfrequenz +channel-type.luxtronikheatpump.temperatureHeatingParallelShift.label = Heizung Temp. (Parallelverschiebung) +channel-type.luxtronikheatpump.heatingMode.label = Heizung Betriebsart +channel-type.luxtronikheatpump.hotWaterMode.label = Warmwasser Betriebsart +channel-type.luxtronikheatpump.thermalDisinfectionMonday.label = Thermische Desinfektion (Mo) +channel-type.luxtronikheatpump.thermalDisinfectionTuesday.label = Thermische Desinfektion (Di) +channel-type.luxtronikheatpump.thermalDisinfectionWednesday.label = Thermische Desinfektion (Mi) +channel-type.luxtronikheatpump.thermalDisinfectionThursday.label = Thermische Desinfektion (Do) +channel-type.luxtronikheatpump.thermalDisinfectionFriday.label = Thermische Desinfektion (Fr) +channel-type.luxtronikheatpump.thermalDisinfectionSaturday.label = Thermische Desinfektion (Sa) +channel-type.luxtronikheatpump.thermalDisinfectionSunday.label = Thermische Desinfektion (So) +channel-type.luxtronikheatpump.thermalDisinfectionPermanent.label = Thermische Desinfektion (Dauerbetrieb) +channel-type.luxtronikheatpump.comfortCoolingMode.label = Comfort Kühlung Betriebsart +channel-type.luxtronikheatpump.comfortCoolingMode.state.option.0 = Aus +channel-type.luxtronikheatpump.comfortCoolingMode.state.option.1 = Auto +channel-type.luxtronikheatpump.temperatureComfortCoolingATRelease.label = Comfort Kühlung AT-Freigabe +channel-type.luxtronikheatpump.temperatureComfortCoolingATReleaseTarget.label = Comfort Kühlung AT-Freigabe Sollwert +channel-type.luxtronikheatpump.comfortCoolingATExcess.label = AT-Überschreitung +channel-type.luxtronikheatpump.comfortCoolingATUndercut.label = AT-Unterschreitung + +channel-type.luxtronikheatpump.operationMode.state.option.0 = Auto +channel-type.luxtronikheatpump.operationMode.state.option.1 = Zuheizer +channel-type.luxtronikheatpump.operationMode.state.option.2 = Party +channel-type.luxtronikheatpump.operationMode.state.option.3 = Ferien +channel-type.luxtronikheatpump.operationMode.state.option.4 = Aus + +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.1 = Wärmepumpe Störung +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.2 = Anlagenstörung +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.3 = Betriebsart Zweiter Wärmeerzeuger +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.4 = EVU-Sperre +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.5 = Lauftabtau (nur LW-Geräte) +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.6 = Temperatur Einsatzgrenze maximal +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.7 = Temperatur Einsatzgrenze minimal +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.8 = Untere Einsatzgrenze +channel-type.luxtronikheatpump.SwitchoffX_file_NrX.state.option.9 = Keine Anforderung + +channel-type.luxtronikheatpump.errorCodeX.state.option.701 = Niederdruckstörung - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.702 = Niederdrucksperre - RESET automatisch +channel-type.luxtronikheatpump.errorCodeX.state.option.703 = Frostschutz - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.704 = Heißgasstörung - Reset in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.705 = Motorschutz VEN - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.706 = Motorschutz BSUP - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.707 = Kodierung Wärmepumpe - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.708 = Fühler Rücklauf - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.709 = Fühler Vorlauf - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.710 = Fühler Heißgas - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.711 = Fühler Außentemp. - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.712 = Fühler Trinkwarmwasser - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.713 = Fühler WQ-Eintritt - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.714 = Heißgas Warmwasser - Reset in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.715 = Hochdruck-Abschaltung - RESET automatisch +channel-type.luxtronikheatpump.errorCodeX.state.option.716 = Hochdruckstörung - Bitte Inst rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.717 = Durchfluss-WQ - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.718 = Maximale Außentemperatur - RESET automatisch +channel-type.luxtronikheatpump.errorCodeX.state.option.719 = Minimale Außentemperatur - RESET automatisch +channel-type.luxtronikheatpump.errorCodeX.state.option.720 = Minimale WQ-Temperatur - RESET automatisch in hh:mm +channel-type.luxtronikheatpump.errorCodeX.state.option.721 = Niederdruckabschaltung - RESET automatisch +channel-type.luxtronikheatpump.errorCodeX.state.option.722 = Temperaturdifferenz Heizwasser - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.723 = Temperaturdifferenz Warmwasser - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.724 = Temperaturdifferenz Abtauen - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.725 = Anlagenfehler Warmwasser - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.726 = Fühler Mischkreis 1 - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.727 = Soledruck - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.728 = Fühler WQ-Austritt - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.729 = Drehfeldfehler - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.730 = Leistung Ausheizen - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.731 = Zeitüberschreitung TDI +channel-type.luxtronikheatpump.errorCodeX.state.option.732 = Störung Kühlung - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.733 = Störung Anode - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.734 = Störung Anode - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.735 = Fühler Externe Energiequelle - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.736 = Fühler Solarkollektor - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.737 = Fühler Solarspeicher - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.738 = Fühler Mischkreis 2 - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.739 = Fühler Mischkreis 3 - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.750 = Fühler Rücklauf extern - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.751 = Phasenüberwachungsfehler +channel-type.luxtronikheatpump.errorCodeX.state.option.752 = Phasenüberwachungs- / Durchflussfehler +channel-type.luxtronikheatpump.errorCodeX.state.option.755 = Verbindung zu Slave verloren - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.756 = Verbindung zu Master verloren - Bitte Inst. rufen +channel-type.luxtronikheatpump.errorCodeX.state.option.757 = ND-Störung bei W/W-Gerät +channel-type.luxtronikheatpump.errorCodeX.state.option.758 = Störung Abtauung +channel-type.luxtronikheatpump.errorCodeX.state.option.759 = Meldung TDI +channel-type.luxtronikheatpump.errorCodeX.state.option.760 = Störung Abtauung +channel-type.luxtronikheatpump.errorCodeX.state.option.761 = LIN-Verbindung unterbrochen +channel-type.luxtronikheatpump.errorCodeX.state.option.762 = Fühler Ansaug-Verdichter +channel-type.luxtronikheatpump.errorCodeX.state.option.763 = Fühler Ansaug-Verdampfer +channel-type.luxtronikheatpump.errorCodeX.state.option.764 = Fühler Verdichterheizung +channel-type.luxtronikheatpump.errorCodeX.state.option.765 = Überhitzung +channel-type.luxtronikheatpump.errorCodeX.state.option.766 = Einsatzgrenzen-VD +channel-type.luxtronikheatpump.errorCodeX.state.option.767 = STB E-Stab +channel-type.luxtronikheatpump.errorCodeX.state.option.768 = Durchflussüberwachung +channel-type.luxtronikheatpump.errorCodeX.state.option.769 = Pumpenansteuerung +channel-type.luxtronikheatpump.errorCodeX.state.option.770 = Niedrige Überhitzung +channel-type.luxtronikheatpump.errorCodeX.state.option.771 = Hohe Überhitzung +channel-type.luxtronikheatpump.errorCodeX.state.option.776 = Einsatzgrenzen-VD +channel-type.luxtronikheatpump.errorCodeX.state.option.777 = Expansionsventil +channel-type.luxtronikheatpump.errorCodeX.state.option.778 = Fühler Niederdruck +channel-type.luxtronikheatpump.errorCodeX.state.option.779 = Fühler Hochdruck +channel-type.luxtronikheatpump.errorCodeX.state.option.780 = Fühler EVI +channel-type.luxtronikheatpump.errorCodeX.state.option.781 = Fühler Flüssig, vor Ex-Ventil +channel-type.luxtronikheatpump.errorCodeX.state.option.782 = Fühler EVI Sauggas +channel-type.luxtronikheatpump.errorCodeX.state.option.783 = Kommunikation SEC-Platine / Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.784 = VSS gesperrt +channel-type.luxtronikheatpump.errorCodeX.state.option.785 = SEC-Platine defekt +channel-type.luxtronikheatpump.errorCodeX.state.option.786 = Kommunikation SEC-Platine / Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.787 = VD Alarm +channel-type.luxtronikheatpump.errorCodeX.state.option.788 = Schwerw. Inverter Fehler +channel-type.luxtronikheatpump.errorCodeX.state.option.789 = LIN/Kodierung nicht vorhanden +channel-type.luxtronikheatpump.errorCodeX.state.option.790 = Schwerw. Inverter Fehler +channel-type.luxtronikheatpump.errorCodeX.state.option.791 = ModBus Inverter +channel-type.luxtronikheatpump.errorCodeX.state.option.792 = LIN-Verbindung unterbrochen +channel-type.luxtronikheatpump.errorCodeX.state.option.793 = Inverter Temperatur +channel-type.luxtronikheatpump.errorCodeX.state.option.794 = Überspannung +channel-type.luxtronikheatpump.errorCodeX.state.option.795 = Unterspannung +channel-type.luxtronikheatpump.errorCodeX.state.option.796 = Sicherheitsabschaltung +channel-type.luxtronikheatpump.errorCodeX.state.option.797 = MLRH wird nicht unterstützt +channel-type.luxtronikheatpump.errorCodeX.state.option.798 = ModBus Ventilator +channel-type.luxtronikheatpump.errorCodeX.state.option.799 = ModBus ASB +channel-type.luxtronikheatpump.errorCodeX.state.option.800 = Enthitzer-Fehler +channel-type.luxtronikheatpump.errorCodeX.state.option.802 = Ventilator Schaltkasten +channel-type.luxtronikheatpump.errorCodeX.state.option.803 = Ventilator Schaltkasten +channel-type.luxtronikheatpump.errorCodeX.state.option.806 = ModBus SEC +channel-type.luxtronikheatpump.errorCodeX.state.option.807 = ModBus Verbindung verloren diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/channels.xml new file mode 100644 index 0000000000000..134311476dce5 --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,2078 @@ + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Number:Time + + Time + + + + + Number:Dimensionless + + + + + + Number:Time + + Time + + + + + Number:Dimensionless + + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + + Number + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + + Switch + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + Number:Time + + Time + + + + + String + + + + + + Number + + + + + + Number:Temperature + + Temperature + + + + + Number:Time + + Time + + + + + Switch + + + + + + Number + + + + + + Number + + + + + + Number + + + + + + Number + + + + + + Number + + + + + Number + + + + + Number + + + + + Number + + + + + Number + + + + + + DateTime + + Time + + + + + Number + + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Number:Time + + Time + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number + + + + + + Number:Time + + Time + + + + + Switch + + + + + + Number:ElectricPotential + + Energy + + + + + Number + + + + + + Number:Energy + + Energy + + + + + Number:Energy + + Energy + + + + + Number:Energy + + Energy + + + + + Number:Energy + + Energy + + + + + Number:VolumetricFlowRate + + Flow + + + + + Number:ElectricPotential + + Energy + + + + + Number:ElectricPotential + + Energy + + + + + Number:Time + + Time + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Time + + Time + + + + + Number:ElectricPotential + + Energy + + + + + Number:ElectricPotential + + Energy + + + + + Number:ElectricPotential + + Energy + + + + + Number:ElectricPotential + + Energy + + + + + Switch + + + + + + Switch + + + + + + Number:ElectricPotential + + Energy + + + + + Number:ElectricPotential + + Energy + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Number:VolumetricFlowRate + + Flow + + + + + Switch + + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Pressure + + Pressure + + + + + Number:Pressure + + Pressure + + + + + Switch + + + + + + Number:Energy + + Energy + + + + + Number + + Frequency + + + + + Switch + + + + + + Number:Energy + + Energy + + + + + Number:Energy + + Energy + + + + + Number:Temperature + + Temperature + + + + + Number + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + Number + + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Pressure + + Pressure + + + + + Number:ElectricPotential + + Energy + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Pressure + + Pressure + + + + + Number:Pressure + + Pressure + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Switch + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + DateTime + + Time + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Frequency + + + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Number + + + + + + + + + + + Number:Temperature + + Temperature + + + + + Number:Temperature + + Temperature + + + + + Number:Time + + Time + + + + + Number:Time + + Time + + + + diff --git a/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..d4589d1384aed --- /dev/null +++ b/bundles/org.openhab.binding.luxtronikheatpump/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,259 @@ + + + + + Integrates a heatpump with a Luxtronik control. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + network-address + + IP address of the heat pump to connect to + + + + 8889 + Port number of the heat pump to connect to. Defaults to 8889. For heatpumps running firmware version + before V1.73, port 8888 needs to be used. + + + + 300 + s + Refresh inverval in seconds. Defaults to 300 + + + + false + By default channels that aren't supported are hidden. + + + + + + diff --git a/bundles/org.openhab.binding.magentatv/README.md b/bundles/org.openhab.binding.magentatv/README.md index 02acd4a1b12f8..ddfcf61040014 100644 --- a/bundles/org.openhab.binding.magentatv/README.md +++ b/bundles/org.openhab.binding.magentatv/README.md @@ -117,7 +117,7 @@ For security reasons the credentials are automatically deleted from the thing co | |key |String |Send key code to the receiver (see code table below) | | |mute |Switch |Mute volume (mute the speaker) | |status |playMode |String |Current play mode - this info is not reliable | -| |channelCode |Number  |The channel code from the EPG. | +| |channelCode |Number |The channel code from the EPG. | |program |title |String |Title of the running program or video being played | | |text |String |Some description (as reported by the receiver, could be empty) | | |start |DateTime |Time when the program started | @@ -327,4 +327,4 @@ to switch it ON and to switch it off. -After an openHAB restart you need to make sure that OH and receiver are in sync, because the binding can't read the power status at startup. +After an openHAB restart you need to make sure that OH and receiver are in sync, because the binding can't read the power status at startup. \ No newline at end of file diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVBindingConstants.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVBindingConstants.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVConsoleHandler.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVConsoleHandler.java index 6f041ddb7c85d..7c62e31b15683 100644 --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVConsoleHandler.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVConsoleHandler.java @@ -20,18 +20,15 @@ import java.util.Arrays; import java.util.List; -import javax.ws.rs.client.ClientBuilder; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.magentatv.internal.network.MagentaTVOAuth; import org.openhab.core.io.console.Console; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.io.net.http.HttpClientFactory; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,14 +44,12 @@ public class MagentaTVConsoleHandler extends AbstractConsoleCommandExtension { private static final String CMD_LOGIN = "login"; private final Logger logger = LoggerFactory.getLogger(MagentaTVConsoleHandler.class); - private final MagentaTVOAuth oauth = new MagentaTVOAuth(); - - @Reference(cardinality = ReferenceCardinality.OPTIONAL) - private @Nullable ClientBuilder injectedClientBuilder; + private final MagentaTVOAuth oauth; @Activate - public MagentaTVConsoleHandler() { + public MagentaTVConsoleHandler(@Reference HttpClientFactory httpClientFactory) { super(BINDING_ID, "Interact with the " + BINDING_ID + " integration."); + oauth = new MagentaTVOAuth(httpClientFactory.getCommonHttpClient()); } @Override diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVException.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVException.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVGsonDTO.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVGsonDTO.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVHandlerFactory.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVHandlerFactory.java old mode 100644 new mode 100755 index 87484939660ab..7508121483e1b --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVHandlerFactory.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/MagentaTVHandlerFactory.java @@ -19,10 +19,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager.MagentaTVDevice; import org.openhab.binding.magentatv.internal.handler.MagentaTVHandler; import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork; import org.openhab.binding.magentatv.internal.network.MagentaTVPoweroffListener; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.net.HttpServiceUtil; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Thing; @@ -51,6 +53,7 @@ public class MagentaTVHandlerFactory extends BaseThingHandlerFactory { private final MagentaTVNetwork network = new MagentaTVNetwork(); private final MagentaTVDeviceManager manager; + private final HttpClient httpClient; private @Nullable MagentaTVPoweroffListener upnpListener; private boolean servletInitialized = false; @@ -64,11 +67,11 @@ public class MagentaTVHandlerFactory extends BaseThingHandlerFactory { @Activate public MagentaTVHandlerFactory(@Reference NetworkAddressService networkAddressService, - @Reference MagentaTVDeviceManager manager, ComponentContext componentContext, - Map configProperties) throws IOException { + @Reference HttpClientFactory httpClientFactory, @Reference MagentaTVDeviceManager manager, + ComponentContext componentContext, Map configProperties) throws IOException { super.activate(componentContext); this.manager = manager; - + this.httpClient = httpClientFactory.getCommonHttpClient(); try { logger.debug("Initialize network access"); System.setProperty("java.net.preferIPv4Stack", "true"); @@ -99,7 +102,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { logger.debug("Create thing type {}", thing.getThingTypeUID().getAsString()); if (THING_TYPE_RECEIVER.equals(thingTypeUID)) { - return new MagentaTVHandler(manager, thing, network); + return new MagentaTVHandler(manager, thing, network, httpClient); } return null; diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/config/MagentaTVThingConfiguration.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/config/MagentaTVThingConfiguration.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/discovery/MagentaTVDiscoveryParticipant.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/discovery/MagentaTVDiscoveryParticipant.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVControl.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVControl.java old mode 100644 new mode 100755 index f67ff2b4fa8a3..68bed02f44245 --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVControl.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVControl.java @@ -23,6 +23,7 @@ import java.util.StringTokenizer; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.magentatv.internal.MagentaTVException; import org.openhab.binding.magentatv.internal.config.MagentaTVDynamicConfig; import org.openhab.binding.magentatv.internal.network.MagentaTVHttp; @@ -44,7 +45,7 @@ public class MagentaTVControl { private final MagentaTVNetwork network; private final MagentaTVHttp http = new MagentaTVHttp(); - private final MagentaTVOAuth oauth = new MagentaTVOAuth(); + private final MagentaTVOAuth oauth; private final MagentaTVDynamicConfig config; private boolean initialized = false; private String thingId = ""; @@ -52,11 +53,13 @@ public class MagentaTVControl { public MagentaTVControl() { config = new MagentaTVDynamicConfig(); network = new MagentaTVNetwork(); + oauth = new MagentaTVOAuth(new HttpClient()); } - public MagentaTVControl(MagentaTVDynamicConfig config, MagentaTVNetwork network) { + public MagentaTVControl(MagentaTVDynamicConfig config, MagentaTVNetwork network, HttpClient httpClient) { thingId = config.getFriendlyName(); this.network = network; + this.oauth = new MagentaTVOAuth(httpClient); this.config = config; this.config.setTerminalID(computeMD5(network.getLocalMAC().toUpperCase() + config.getUDN())); this.config.setLocalIP(network.getLocalIP()); @@ -391,7 +394,8 @@ private String getKeyCode(String key) { // direct key code return key; } - return KEY_MAP.getOrDefault(key, ""); + String code = KEY_MAP.get(key); + return code != null ? code : ""; } /** diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVHandler.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVHandler.java old mode 100644 new mode 100755 index 66efbe059c746..3d9a9f6716908 --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVHandler.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVHandler.java @@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager; import org.openhab.binding.magentatv.internal.MagentaTVException; import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEvent; @@ -88,6 +89,7 @@ public class MagentaTVHandler extends BaseThingHandler implements MagentaTVListe private final Gson gson; protected final MagentaTVNetwork network; protected final MagentaTVDeviceManager manager; + private final HttpClient httpClient; protected MagentaTVControl control = new MagentaTVControl(); private String thingId = ""; @@ -102,10 +104,12 @@ public class MagentaTVHandler extends BaseThingHandler implements MagentaTVListe * @param thing * @param bindingConfig */ - public MagentaTVHandler(MagentaTVDeviceManager manager, Thing thing, MagentaTVNetwork network) { + public MagentaTVHandler(MagentaTVDeviceManager manager, Thing thing, MagentaTVNetwork network, + HttpClient httpClient) { super(thing); this.manager = manager; this.network = network; + this.httpClient = httpClient; gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new MRProgramInfoEventInstanceCreator()) .registerTypeAdapter(OAuthTokenResponse.class, new MRProgramStatusInstanceCreator()) .registerTypeAdapter(OAuthAuthenticateResponse.class, new MRShortProgramInfoInstanceCreator()) @@ -150,7 +154,7 @@ private void initializeThing() { } config.setMacAddress(macAddress); } - control = new MagentaTVControl(config, network); + control = new MagentaTVControl(config, network, httpClient); config.updateNetwork(control.getConfig()); // get network parameters from control // Check for emoty credentials (e.g. missing in .things file) @@ -185,7 +189,7 @@ private void initializeThing() { } /** - * This routine is called every time the Thing configuration has been changed. + * This routine is called every time the Thing configuration has been changed */ @Override public void handleConfigurationUpdate(Map configurationParameters) { diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVListener.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/handler/MagentaTVListener.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVHttp.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVHttp.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNetwork.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNetwork.java old mode 100644 new mode 100755 index 0a2ab367fb886..c2554689312e2 --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNetwork.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNetwork.java @@ -20,7 +20,6 @@ import java.net.SocketException; import java.net.UnknownHostException; -import org.apache.commons.net.util.SubnetUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.magentatv.internal.MagentaTVException; @@ -76,34 +75,6 @@ public void initLocalNet(String localIP, String localPort) throws MagentaTVExcep "Unable to get local IP / MAC address, check network settings in openHAB system configuration!"); } - /** - * Checks if client ip equals or is in range of ip networks provided by - * semicolon separated list - * - * @param clientIp in numeric form like "192.168.0.10" - * @param ipList like "127.0.0.1;192.168.0.0/24;10.0.0.0/8" - * @return true if client ip from the list os ips and networks - */ - public static boolean isIpInSubnet(String clientIp, String ipList) { - if (ipList.isEmpty()) { - // No ip address provided - return true; - } - String[] subnetMasks = ipList.split(";"); - for (String subnetMask : subnetMasks) { - subnetMask = subnetMask.trim(); - if (clientIp.equals(subnetMask)) { - return true; - } - if (subnetMask.contains("/")) { - if (new SubnetUtils(subnetMask).getInfo().isInRange(clientIp)) { - return true; - } - } - } - return false; - } - @Nullable public NetworkInterface getLocalInterface() { return localInterface; diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNotifyServlet.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVNotifyServlet.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVOAuth.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVOAuth.java old mode 100644 new mode 100755 index 36acdb4ebc287..a6b6539ba6248 --- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVOAuth.java +++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVOAuth.java @@ -13,21 +13,34 @@ package org.openhab.binding.magentatv.internal.network; import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*; -import static org.openhab.binding.magentatv.internal.MagentaTVUtil.substringAfterLast; +import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpCookie; import java.net.URLEncoder; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.ws.rs.HttpMethod; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.magentatv.internal.MagentaTVException; import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse; import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponseInstanceCreator; @@ -37,7 +50,6 @@ import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentialsInstanceCreator; import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthKeyValue; import org.openhab.binding.magentatv.internal.handler.MagentaTVControl; -import org.openhab.core.io.net.http.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +62,7 @@ * * @author Markus Michels - Initial contribution * - * Deutsche Telekom uses a OAuth-based authentication to access the EPG portal. The + * Deutsche Telekom uses an OAuth-based authentication to access the EPG portal. The * communication between the MR and the remote app requires a pairing before the receiver could be * controlled by sending keys etc. The so called userID is not directly derived from any local parameters * (like terminalID as a has from the mac address), but will be returned as a result from the OAuth @@ -63,9 +75,12 @@ @NonNullByDefault public class MagentaTVOAuth { private final Logger logger = LoggerFactory.getLogger(MagentaTVOAuth.class); - final Gson gson; + private HttpClient httpClient; + private final Gson gson; + private List cookies = new ArrayList<>(); - public MagentaTVOAuth() { + public MagentaTVOAuth(HttpClient httpClient) { + this.httpClient = httpClient; gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new OauthCredentialsInstanceCreator()) .registerTypeAdapter(OAuthTokenResponse.class, new OAuthTokenResponseInstanceCreator()) .registerTypeAdapter(OAuthAuthenticateResponse.class, new OAuthAuthenticateResponseInstanceCreator()) @@ -78,124 +93,209 @@ public String getUserId(String accountName, String accountPassword) throws Magen throw new MagentaTVException("Credentials for OAuth missing, check thing config!"); } - String step = "initialize"; String url = ""; - Properties httpHeader; + Properties httpHeader = initHttpHeader(); String postData = ""; String httpResponse = ""; - InputStream dataStream = null; // OAuth autentication results String oAuthScope = ""; String oAuthService = ""; String epghttpsurl = ""; - String retcode = ""; - String retmsg = ""; + // Get credentials + url = OAUTH_GET_CRED_URL + ":" + OAUTH_GET_CRED_PORT + OAUTH_GET_CRED_URI; + httpHeader.setProperty(HttpHeader.HOST.toString(), substringAfterLast(OAUTH_GET_CRED_URL, "/")); + httpResponse = httpRequest(HttpMethod.GET, url, httpHeader, ""); + OauthCredentials cred = gson.fromJson(httpResponse, OauthCredentials.class); + epghttpsurl = getString(cred.epghttpsurl); + if (epghttpsurl.isEmpty()) { + throw new MagentaTVException("Unable to determine EPG url"); + } + if (!epghttpsurl.contains("/EPG")) { + epghttpsurl = epghttpsurl + "/EPG"; + } + logger.debug("OAuth: epghttpsurl = {}", epghttpsurl); + + // get OAuth data from response + if (cred.sam3Para != null) { + for (OauthKeyValue si : cred.sam3Para) { + logger.trace("sam3Para.{} = {}", si.key, si.value); + if (si.key.equalsIgnoreCase("oAuthScope")) { + oAuthScope = si.value; + } else if (si.key.equalsIgnoreCase("SAM3ServiceURL")) { + oAuthService = si.value; + } + } + } + if (oAuthScope.isEmpty() || oAuthService.isEmpty()) { + throw new MagentaTVException("OAuth failed: Can't get Scope and Service: " + httpResponse); + } + + // Get OAuth token (New flow based on WebTV) + String userId = ""; + String terminalId = UUID.randomUUID().toString(); + String cnonce = MagentaTVControl.computeMD5(terminalId); + + url = oAuthService + "/oauth2/tokens"; + postData = MessageFormat.format( + "password={0}&scope={1}+offline_access&grant_type=password&username={2}&x_telekom.access_token.format=CompactToken&x_telekom.access_token.encoding=text%2Fbase64&client_id=10LIVESAM30000004901NGTVWEB0000000000000", + urlEncode(accountPassword), oAuthScope, urlEncode(accountName)); + url = oAuthService + "/oauth2/tokens"; + httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData); + OAuthTokenResponse resp = gson.fromJson(httpResponse, OAuthTokenResponse.class); + if (resp.accessToken.isEmpty()) { + String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} ({2})", + accountName, getString(resp.errorDescription), getString(resp.error)); + logger.warn("{}", errorMessage); + throw new MagentaTVException(errorMessage); + } + logger.debug("OAuth: Access Token retrieved"); + + // General authentication + logger.debug("OAuth: Generating CSRF token"); + url = "https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate"; + httpHeader = initHttpHeader(); + httpHeader.setProperty(HttpHeader.HOST.toString(), "api.prod.sngtv.magentatv.de"); + httpHeader.setProperty("Origin", "https://web.magentatv.de"); + httpHeader.setProperty(HttpHeader.REFERER.toString(), "https://web.magentatv.de/"); + postData = "{\"areaid\":\"1\",\"cnonce\":\"" + cnonce + "\",\"mac\":\"" + terminalId + + "\",\"preSharedKeyID\":\"NGTV000001\",\"subnetId\":\"4901\",\"templatename\":\"NGTV\",\"terminalid\":\"" + + terminalId + + "\",\"terminaltype\":\"WEB-MTV\",\"terminalvendor\":\"WebTV\",\"timezone\":\"Europe/Berlin\",\"usergroup\":\"-1\",\"userType\":3,\"utcEnable\":1}"; + httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData); + String csrf = ""; + for (HttpCookie c : cookies) { // get CRSF Token + String value = c.getValue(); + if (value.contains("CSRFSESSION")) { + csrf = substringBetween(value, "CSRFSESSION" + "=", ";"); + } + } + if (csrf.isEmpty()) { + throw new MagentaTVException("OAuth: Unable to get CSRF token!"); + } + + // Final step: Retrieve userId + url = "https://api.prod.sngtv.magentatv.de/EPG/JSON/DTAuthenticate"; + httpHeader = initHttpHeader(); + httpHeader.setProperty(HttpHeader.HOST.toString(), "api.prod.sngtv.magentatv.de"); + httpHeader.setProperty("Origin", "https://web.magentatv.de"); + httpHeader.setProperty(HttpHeader.REFERER.toString(), "https://web.magentatv.de/"); + httpHeader.setProperty("X_CSRFToken", csrf); + postData = "{\"areaid\":\"1\",\"cnonce\":\"" + cnonce + "\",\"mac\":\"" + terminalId + "\"," + + "\"preSharedKeyID\":\"NGTV000001\",\"subnetId\":\"4901\",\"templatename\":\"NGTV\"," + + "\"terminalid\":\"" + terminalId + "\",\"terminaltype\":\"WEB-MTV\",\"terminalvendor\":\"WebTV\"," + + "\"timezone\":\"Europe/Berlin\",\"usergroup\":\"\",\"userType\":\"1\",\"utcEnable\":1," + + "\"accessToken\":\"" + resp.accessToken + + "\",\"caDeviceInfo\":[{\"caDeviceId\":\"4ef4d933-9a43-41d3-9e3a-84979f22c9eb\"," + + "\"caDeviceType\":8}],\"connectType\":1,\"osversion\":\"Mac OS 10.15.7\",\"softwareVersion\":\"1.33.4.3\"," + + "\"terminalDetail\":[{\"key\":\"GUID\",\"value\":\"" + terminalId + "\"}," + + "{\"key\":\"HardwareSupplier\",\"value\":\"WEB-MTV\"},{\"key\":\"DeviceClass\",\"value\":\"TV\"}," + + "{\"key\":\"DeviceStorage\",\"value\":0},{\"key\":\"DeviceStorageSize\",\"value\":0}]}"; + httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData); + OAuthAuthenticateResponse authResp = gson.fromJson(httpResponse, OAuthAuthenticateResponse.class); + if (authResp.userID.isEmpty()) { + String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} {2}", + accountName, getString(authResp.retcode), getString(authResp.desc)); + logger.warn("{}", errorMessage); + throw new MagentaTVException(errorMessage); + } + userId = getString(authResp.userID); + if (userId.isEmpty()) { + throw new MagentaTVException("No userID received!"); + } + String hashedUserID = MagentaTVControl.computeMD5(userId).toUpperCase(); + logger.trace("done, userID = {}", hashedUserID); + return hashedUserID; + } + + private String httpRequest(String method, String url, Properties headers, String data) throws MagentaTVException { + String result = ""; try { - step = "get credentials"; - httpHeader = initHttpHeader(); - url = OAUTH_GET_CRED_URL + ":" + OAUTH_GET_CRED_PORT + OAUTH_GET_CRED_URI; - httpHeader.setProperty(HEADER_HOST, substringAfterLast(OAUTH_GET_CRED_URL, "/")); - logger.trace("{} from {}", step, url); - httpResponse = HttpUtil.executeUrl(HttpMethod.GET, url, httpHeader, null, null, NETWORK_TIMEOUT_MS); - logger.trace("http response = {}", httpResponse); - OauthCredentials cred = gson.fromJson(httpResponse, OauthCredentials.class); - epghttpsurl = getString(cred.epghttpsurl); - if (epghttpsurl.isEmpty()) { - throw new MagentaTVException("Unable to determine EPG url"); + Request request = httpClient.newRequest(url).method(method).timeout(NETWORK_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + for (Enumeration e = headers.keys(); e.hasMoreElements();) { + String key = (String) e.nextElement(); + String val = (String) headers.get(key); + request.header(key, val); } - if (!epghttpsurl.contains("/EPG")) { - epghttpsurl = epghttpsurl + "/EPG"; + if (method.equals(HttpMethod.POST)) { + fillPostData(request, data); } - logger.debug("epghttpsurl = {}", epghttpsurl); - - // get OAuth data from response - if (cred.sam3Para != null) { - for (OauthKeyValue si : cred.sam3Para) { - logger.trace("sam3Para.{} = {}", si.key, si.value); - if (si.key.equalsIgnoreCase("oAuthScope")) { - oAuthScope = si.value; - } else if (si.key.equalsIgnoreCase("SAM3ServiceURL")) { - oAuthService = si.value; - } + if (cookies.size() > 0) { + // Add cookies + String cookieValue = ""; + for (HttpCookie c : cookies) { + cookieValue = cookieValue + substringBefore(c.getValue(), ";") + "; "; } + request.header("Cookie", substringBeforeLast(cookieValue, ";")); } + logger.debug("OAuth: HTTP Request\n\tHTTP {} {}\n\tData={}", method, url, data.isEmpty() ? "" : data); + logger.trace("\n\tHeaders={}\tCookies={}", request.getHeaders(), request.getCookies()); - if (oAuthScope.isEmpty() || oAuthService.isEmpty()) { - throw new MagentaTVException("OAuth failed: Can't get Scope and Service: " + httpResponse); - } + ContentResponse contentResponse = request.send(); + result = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim(); + int status = contentResponse.getStatus(); + logger.debug("OAuth: HTTP Response\n\tStatus={} {}\n\tData={}", status, contentResponse.getReason(), + result.isEmpty() ? "" : result); + logger.trace("\n\tHeaders={}", contentResponse.getHeaders()); - // Get OAuth token - step = "get token"; - url = oAuthService + "/oauth2/tokens"; - logger.debug("{} from {}", step, url); - - String userId = ""; - String uuid = UUID.randomUUID().toString(); - String cnonce = MagentaTVControl.computeMD5(uuid); - // New flow based on WebTV - postData = MessageFormat.format( - "password={0}&scope={1}+offline_access&grant_type=password&username={2}&x_telekom.access_token.format=CompactToken&x_telekom.access_token.encoding=text%2Fbase64&client_id=10LIVESAM30000004901NGTVWEB0000000000000", - URLEncoder.encode(accountPassword, UTF_8), oAuthScope, URLEncoder.encode(accountName, UTF_8)); - url = oAuthService + "/oauth2/tokens"; - dataStream = new ByteArrayInputStream(postData.getBytes(Charset.forName("UTF-8"))); - httpResponse = HttpUtil.executeUrl(HttpMethod.POST, url, httpHeader, dataStream, null, NETWORK_TIMEOUT_MS); - logger.trace("http response={}", httpResponse); - OAuthTokenResponse resp = gson.fromJson(httpResponse, OAuthTokenResponse.class); - if (resp.accessToken.isEmpty()) { - String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} ({2})", - accountName, getString(resp.errorDescription), getString(resp.error)); - logger.warn("{}", errorMessage); - throw new MagentaTVException(errorMessage); + // validate response, API errors are reported as Json + HttpFields responseHeaders = contentResponse.getHeaders(); + for (HttpField f : responseHeaders) { + if (f.getName().equals("Set-Cookie")) { + HttpCookie c = new HttpCookie(f.getName(), f.getValue()); + cookies.add(c); + } } - uuid = "t_" + MagentaTVControl.computeMD5(accountName); - url = "https://web.magentatv.de/EPG/JSON/DTAuthenticate?SID=user&T=Mac_chrome_81"; - postData = "{\"userType\":1,\"terminalid\":\"" + uuid + "\",\"mac\":\"" + uuid + "\"" - + ",\"terminaltype\":\"MACWEBTV\",\"utcEnable\":1,\"timezone\":\"Europe/Berlin\"," - + "\"terminalDetail\":[{\"key\":\"GUID\",\"value\":\"" + uuid + "\"}," - + "{\"key\":\"HardwareSupplier\",\"value\":\"\"},{\"key\":\"DeviceClass\",\"value\":\"PC\"}," - + "{\"key\":\"DeviceStorage\",\"value\":\"1\"},{\"key\":\"DeviceStorageSize\",\"value\":\"\"}]," - + "\"softwareVersion\":\"\",\"osversion\":\"\",\"terminalvendor\":\"Unknown\"," - + "\"caDeviceInfo\":[{\"caDeviceType\":6,\"caDeviceId\":\"" + uuid + "\"}]," + "\"accessToken\":\"" - + resp.accessToken + "\",\"preSharedKeyID\":\"PC01P00002\",\"cnonce\":\"" + cnonce + "\"}"; - dataStream = new ByteArrayInputStream(postData.getBytes(Charset.forName("UTF-8"))); - logger.debug("HTTP POST {}, postData={}", url, postData); - httpResponse = HttpUtil.executeUrl(HttpMethod.POST, url, httpHeader, dataStream, null, NETWORK_TIMEOUT_MS); - - logger.trace("http response={}", httpResponse); - OAuthAuthenticateResponse authResp = gson.fromJson(httpResponse, OAuthAuthenticateResponse.class); - if (authResp.userID.isEmpty()) { - String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} {2}", - accountName, getString(authResp.retcode), getString(authResp.desc)); - logger.warn("{}", errorMessage); - throw new MagentaTVException(errorMessage); + if (status != HttpStatus.OK_200) { + String error = "HTTP reqaest failed for URL " + url + ", Code=" + contentResponse.getReason() + "(" + + status + ")"; + throw new MagentaTVException(error); } - userId = getString(authResp.userID); - if (userId.isEmpty()) { - throw new MagentaTVException("No userID received!"); - } - String hashedUserID = MagentaTVControl.computeMD5(userId).toUpperCase(); - logger.trace("done, userID = {}", hashedUserID); - return hashedUserID; - } catch (IOException e) { - throw new MagentaTVException(e, - "Unable to authenticate {0}: {1} failed; serviceURL={2}, rc={3}/{4}, response={5}", accountName, - step, oAuthService, retcode, retmsg, httpResponse); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + String error = "HTTP reqaest failed for URL " + url; + logger.info("{}", error, e); + throw new MagentaTVException(e, error); } + return result; } private Properties initHttpHeader() { Properties httpHeader = new Properties(); - httpHeader.setProperty(HEADER_USER_AGENT, OAUTH_USER_AGENT); - httpHeader.setProperty(HEADER_ACCEPT, "*/*"); - httpHeader.setProperty(HEADER_LANGUAGE, "de-de"); - httpHeader.setProperty(HEADER_CACHE_CONTROL, "no-cache"); + httpHeader.setProperty(HttpHeader.ACCEPT.toString(), "*/*"); + httpHeader.setProperty(HttpHeader.ACCEPT_LANGUAGE.toString(), "en-US,en;q=0.9,de;q=0.8"); + httpHeader.setProperty(HttpHeader.CACHE_CONTROL.toString(), "no-cache"); return httpHeader; } + private void fillPostData(Request request, String data) { + if (!data.isEmpty()) { + StringContentProvider postData; + if (request.getHeaders().contains(HttpHeader.CONTENT_TYPE)) { + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + postData = new StringContentProvider(contentType, data, StandardCharsets.UTF_8); + } else { + boolean json = data.startsWith("{"); + postData = new StringContentProvider(json ? "application/json" : "application/x-www-form-urlencoded", + data, StandardCharsets.UTF_8); + } + request.content(postData); + request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength())); + } + } + private String getString(@Nullable String value) { return value != null ? value : ""; } + + private String urlEncode(String url) { + try { + return URLEncoder.encode(url, UTF_8); + } catch (UnsupportedEncodingException e) { + logger.warn("OAuth: Unable to URL encode string {}", url, e); + return ""; + } + } } diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/binding/binding.xml old mode 100644 new mode 100755 diff --git a/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/i18n/magentatv_de.properties b/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/i18n/magentatv_de.properties index 6374fc9c8c23d..14734cd785591 100644 --- a/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/i18n/magentatv_de.properties +++ b/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/i18n/magentatv_de.properties @@ -9,8 +9,8 @@ thing-type.magentatv.receiver.description = Media Receiver zum Epmfang von Magen # Thing configuration thing-type.config.magentatv.receiver.ipAddress.label = IP-Adresse thing-type.config.magentatv.ipAddress.description = IP Adresse des Media Receivers -thing-type.config.magentatv.receiver.userId.label = UID -thing-type.config.magentatv.receiver.userId.description = Technische Benutzerkennung (User ID), siehe Dokumentation; wird automatisch gef�llt, wenn Login-Name und Passwort angegeben sind. +thing-type.config.magentatv.receiver.userId.label = User ID +thing-type.config.magentatv.receiver.userId.description = Technische Benutzerkennung, siehe Dokumentation thing-type.config.magentatv.receiver.accountName.label = Login-Name thing-type.config.magentatv.receiver.accountName.description = Login-Name (E-Mail) zur Anmeldung im Telekom Kundencenter thing-type.config.magentatv.receiver.accountPassword.label = Passwort diff --git a/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/thing/thing-types.xml old mode 100644 new mode 100755 index 36b0ced037670..96e6cfeff2ca3 --- a/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.magentatv/src/main/resources/OH-INF/thing/thing-types.xml @@ -20,13 +20,12 @@ - macAddress + - + IP address of the receiver - true network-address @@ -40,12 +39,11 @@ - Technical User ID required for pairing process, auto-filled when account credentials are given + Technical User ID required for pairing process - + The UDN identifies the Media Receiver - true true diff --git a/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/thing/thing-types.xml index 5bec8cce229f5..a7eaebf65c34e 100644 --- a/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/thing/thing-types.xml @@ -118,7 +118,6 @@ - true diff --git a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/MCommand.java b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/MCommand.java index 9bcf43a95ca9e..96a71d69fff65 100644 --- a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/MCommand.java +++ b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/MCommand.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.TreeSet; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.max.internal.Utils; import org.openhab.binding.max.internal.device.Device; diff --git a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/TCommand.java b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/TCommand.java index 8927ac272f8a6..c5ac9eed0fe6d 100644 --- a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/TCommand.java +++ b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/command/TCommand.java @@ -16,7 +16,7 @@ import java.util.Base64; import java.util.List; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.max.internal.Utils; diff --git a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/message/CMessage.java b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/message/CMessage.java index a90f9aaaae3c9..a4b326e3874bb 100644 --- a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/message/CMessage.java +++ b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/message/CMessage.java @@ -25,7 +25,7 @@ import java.util.HashMap; import java.util.Map; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.max.internal.Utils; import org.openhab.binding.max.internal.device.DeviceType; @@ -278,7 +278,7 @@ public void debug(Logger logger) { logger.debug("RoomID: {}", roomId); for (String key : properties.keySet()) { if (!key.startsWith("Unknown")) { - String propertyName = StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(key), ' '); + String propertyName = String.join(" ", StringUtils.splitByCharacterTypeCamelCase(key)); logger.debug("{}: {}", propertyName, properties.get(key)); } else { logger.debug("{}: {}", key, properties.get(key)); diff --git a/bundles/org.openhab.binding.mcp23017/pom.xml b/bundles/org.openhab.binding.mcp23017/pom.xml index 839f2a02e36d0..1c8a534d99ac2 100644 --- a/bundles/org.openhab.binding.mcp23017/pom.xml +++ b/bundles/org.openhab.binding.mcp23017/pom.xml @@ -18,13 +18,13 @@ com.pi4j pi4j-core - 1.2 + 1.4 compile com.pi4j pi4j-gpio-extension - 1.2 + 1.3 compile
diff --git a/bundles/org.openhab.binding.mecmeter/NOTICE b/bundles/org.openhab.binding.mecmeter/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.mecmeter/README.md b/bundles/org.openhab.binding.mecmeter/README.md new file mode 100644 index 0000000000000..aa8f2f8fbdcf7 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/README.md @@ -0,0 +1,247 @@ +# MecMeter Binding + +This binding reads data from MEC power meter for providing electrical information for the electric circuit. + +To use this binding the meter must be installed, initialized and connected to the same network as openHAB. + +## Supported Things + +The mecMeter is supported with firmware version starting from 2.0.0. +There is exactly one supported thing type `meter`, which represents the electric meter. +Its unique ID is the serial number. + +## Discovery + +MecMeters are automatically discovered via mDNS. +The IP of the Power Meter is automatically set and can be changed manually if needed. +The default update interval is set to 5 seconds. Intervals from 1 to 300 seconds can be set manually. + +## Thing Configuration + +The thing has a few configuration parameters: + +| Parameter | Description | +|-----------------|-----------------------------------------------------------------------| +| password | User-defined password during initializing the Power Meter. Mandatory. | +| ip | The IP address of the meter. Mandatory. | +| refreshInterval | Refresh interval in second. Optional, the default value is 5 seconds. | + + +## Channels + +The meter has the following channels: + +| Channel Type ID | Item Type | Label | Description | +|--------------------------------------------------------------|--------------------------|-----------------------------------|------------------------------------------| +| general_group#frequency | Number:Frequency | Main Frequency | Frequency in Hertz | +| general_group#temperature | Number:Temperature | Internal Temperature | Internal Temperature of the energy meter | +| general_group#op_time | Number:Time | Time in Operation | Time in Operation | +| voltage_group#voltage_phase1 | Number:ElectricPotential | Voltage P1 | Voltage in Volt | +| voltage_group#voltage_phase2 | Number:ElectricPotential | Voltage P2 | Voltage in Volt | +| voltage_group#voltage_phase3 | Number:ElectricPotential | Voltage P3 | Voltage in Volt | +| voltage_group#avg_phase_phase_voltage | Number:ElectricPotential | Average Phase – Phase Voltage | Average Phase – Phase Voltage in Volt | +| voltage_group#avg_neutral_phase_voltage | Number:ElectricPotential | Average Voltage | Average N – Phase Voltage in Volt | +| current_group#current_allphase | Number:ElectricCurrent | Current | Current in Ampere | +| current_group#current_phase1 | Number:ElectricCurrent | Current P1 | Current in Ampere | +| current_group#current_phase2 | Number:ElectricCurrent | Current P2 | Current in Ampere | +| current_group#current_phase3 | Number:ElectricCurrent | Current P3 | Current in Ampere | +| angle_group#phase_angle_currvolt_phase1 | Number:Angle | Current P1 | Angle Current to Voltage in Degree | +| angle_group#phase_angle_currvolt_phase2 | Number:Angle | Current P2 | Angle Current to Voltage in Degree | +| angle_group#phase_angle_currvolt_phase3 | Number:Angle | Current P3 | Angle Current to Voltage in Degree | +| angle_group#phase_angle_phase1-3 | Number:Angle | Angle Voltage to Voltage | Angle Voltage to Voltage in Degree | +| angle_group#phase_angle_phase2-3 | Number:Angle | Angle Voltage to Voltage | Angle Voltage to Voltage in Degree | +| activepower_group#activepower_allphase | Number:Power | Active Power | Active power consumed | +| activepower_group#activepower_phase1 | Number:Power | Active Power P1 | Active power consumed | +| activepower_group#activepower_phase2 | Number:Power | Active Power P2 | Active power consumed | +| activepower_group#activepower_phase3 | Number:Power | Active Power P3 | Active power consumed | +| activefundpower_group#activefundpower_allphase | Number:Power | Active Fundamental Power | Active fundamental power | +| activefundpower_group#activefundpower_phase1 | Number:Power | Active Fund Power P1 | Active fundamental power | +| activefundpower_group#activefundpower_phase2 | Number:Power | Active Fund Power P2 | Active fundamental power | +| activefundpower_group#activefundpower_phase3 | Number:Power | Active Fund Power P3 | Active fundamental power | +| activeharmpower_group#activeharmpower_allphase | Number:Power | Active Harmonic Power | Active harmonic power | +| activeharmpower_group#activeharmpower_phase1 | Number:Power | Active Harm Power P1 | Active harmonic power | +| activeharmpower_group#activeharmpower_phase2 | Number:Power | Active Harm Power P2 | Active harmonic power | +| activeharmpower_group#activeharmpower_phase3 | Number:Power | Active Harm Power P3 | Active harmonic power | +| reactivepower_group#reactivepower_allphase | Number:Power | Reactive Power | Reactive power consumed | +| reactivepower_group#reactivepower_phase1 | Number:Power | Reactive Power P1 | Reactive power consumed | +| reactivepower_group#reactivepower_phase2 | Number:Power | Reactive Power P2 | Reactive power consumed | +| reactivepower_group#reactivepower_phase3 | Number:Power | Reactive Power P3 | Reactive power consumed | +| powerfactor_group#powerFactor_allphase | Number:Dimensionless | Power Factor | Power Factor | +| powerfactor_group#powerFactor_phase1 | Number:Dimensionless | Power Factor P1 | Power Factor | +| powerfactor_group#powerFactor_phase2 | Number:Dimensionless | Power Factor P2 | Power Factor | +| powerfactor_group#powerFactor_phase3 | Number:Dimensionless | Power Factor P3 | Power Factor | +| apppower_group#apppower_allphase | Number:Power | Apparent Power | Apparent power consumed | +| apppower_group#apppower_phase1 | Number:Power | Apparent Power P1 | Apparent power consumed | +| apppower_group#apppower_phase2 | Number:Power | Apparent Power P2 | Apparent power consumed | +| apppower_group#apppower_phase3 | Number:Power | Apparent Power P3 | Apparent power consumed | +| fwd_active_energy_group#fwd_active_energy_allphase | Number:Energy | Forward Active Energy | Forward Active Energy in kWh | +| fwd_active_energy_group#fwd_active_energy_phase1 | Number:Energy | Fwd Active Energy P1 | Forward Active Energy in kWh | +| fwd_active_energy_group#fwd_active_energy_phase2 | Number:Energy | Fwd Active Energy P2 | Forward Active Energy in kWh | +| fwd_active_energy_group#fwd_active_energy_phase3 | Number:Energy | Fwd Active Energy P3 | Forward Active Energy in kWh | +| fwd_active_fund_energy_group#fwd_active_fund_energy_allphase | Number:Energy | Forward Active Fundamental Energy | Forward Active Fundamental Energy in kWh | +| fwd_active_fund_energy_group#fwd_active_fund_energy_phase1 | Number:Energy | Fwd Active Fund Energy P1 | Forward Active Fundamental Energy in kWh | +| fwd_active_fund_energy_group#fwd_active_fund_energy_phase2 | Number:Energy | Fwd Active Fund Energy P2 | Forward Active Fundamental Energy in kWh | +| fwd_active_fund_energy_group#fwd_active_fund_energy_phase3 | Number:Energy | Fwd Active Fund Energy P3 | Forward Active Fundamental Energy in kWh | +| fwd_active_harm_energy_group#fwd_active_harm_energy_allphase | Number:Energy | Forward Active Harmonic Energy | Forward Active Harmonic Energy in kWh | +| fwd_active_harm_energy_group#fwd_active_harm_energy_phase1 | Number:Energy | Fwd Active Harm Energy P1 | Forward Active Harmonic Energy in kWh | +| fwd_active_harm_energy_group#fwd_active_harm_energy_phase2 | Number:Energy | Fwd Active Harm Energy P2 | Forward Active Harmonic Energy in kWh | +| fwd_active_harm_energy_group#fwd_active_harm_energy_phase3 | Number:Energy | Fwd Active Harm Energy P3 | Forward Active Harmonic Energy in kWh | +| fwd_reactive_energy_group#fwd_reactive_energy_allphase | Number:Energy | Forward Reactive Energy | Forward Reactive Energy in VArh | +| fwd_reactive_energy_group#fwd_reactive_energy_phase1 | Number:Energy | Fwd Reactive Energy P1 | Forward Reactive Energy in VArh | +| fwd_reactive_energy_group#fwd_reactive_energy_phase2 | Number:Energy | Fwd Reactive Energy P2 | Forward Reactive Energy in VArh | +| fwd_reactive_energy_group#fwd_reactive_energy_phase3 | Number:Energy | Fwd Reactive Energy P3 | Forward Reactive Energy in VArh | +| rev_active_energy_group#rev_active_energy_allphase | Number:Energy | Reverse Active Energy | Reverse Active Energy in kWh | +| rev_active_energy_group#rev_active_energy_phase1 | Number:Energy | Rev Active Energy P1 | Reverse Active Energy in kWh | +| rev_active_energy_group#rev_active_energy_phase2 | Number:Energy | Rev Active Energy P2 | Reverse Active Energy in kWh | +| rev_active_energy_group#rev_active_energy_phase3 | Number:Energy | Rev Active Energy P3 | Reverse Active Energy in kWh | +| rev_active_fund_energy_group#rev_active_fund_energy_allphase | Number:Energy | Reverse Active Fundamental Energy | Reverse Active Fundamental Energy in kWh | +| rev_active_fund_energy_group#rev_active_fund_energy_phase1 | Number:Energy | Rev Active Fund Energy P1 | Reverse Active Fundamental Energy in kWh | +| rev_active_fund_energy_group#rev_active_fund_energy_phase2 | Number:Energy | Rev Active Fund Energy P2 | Reverse Active Fundamental Energy in kWh | +| rev_active_fund_energy_group#rev_active_fund_energy_phase3 | Number:Energy | Rev Active Fund Energy P3 | Reverse Active Fundamental Energy in kWh | +| rev_active_harm_energy_group#rev_active_harm_energy_allphase | Number:Energy | Reverse Active Harmonic Energy | Reverse Active Harmonic Energy in kWh | +| rev_active_harm_energy_group#rev_active_harm_energy_phase1 | Number:Energy | Rev Active Harm Energy P1 | Reverse Active Harmonic Energy in kWh | +| rev_active_harm_energy_group#rev_active_harm_energy_phase2 | Number:Energy | Rev Active Harm Energy P2 | Reverse Active Harmonic Energy in kWh | +| rev_active_harm_energy_group#rev_active_harm_energy_phase3 | Number:Energy | Rev Active Harm Energy P3 | Reverse Active Harmonic Energy in kWh | +| rev_reactive_energy_group#rev_reactive_energy_allphase | Number:Energy | Reverse Reactive Energy | Reverse Reactive Energy in VArh | +| rev_reactive_energy_group#rev_reactive_energy_phase1 | Number:Energy | Rev Reactive Energy P1 | Reverse Reactive Energy in VArh | +| rev_reactive_energy_group#rev_reactive_energy_phase2 | Number:Energy | Rev Reactive Energy P2 | Reverse Reactive Energy in VArh | +| rev_reactive_energy_group#rev_reactive_energy_phase3 | Number:Energy | Rev Reactive Energy P3 | Reverse Reactive Energy in VArh | +| app_energy_group#appenergy_consumption_allphase | Number:Energy | Apparent Energy Consumption | Apparent Energy Consumption in VArh | +| app_energy_group#appenergy_consumption_phase1 | Number:Energy | Apparent Energy P1 | Apparent Energy Consumption in VArh | +| app_energy_group#appenergy_consumption_phase2 | Number:Energy | Apparent Energy P2 | Apparent Energy Consumption in VArh | +| app_energy_group#appenergy_consumption_phase3 | Number:Energy | Apparent Energy P3 | Apparent Energy Consumption in VArh | + +## Full Example + +### mecmeter.things + +``` +mecmeter:meter:1 [ password="Test1234", ip="192.168.1.16", refreshInterval="10" ] +``` + +### mecmeter.items + +``` +Number:Frequency MainFrequency { channel="mecmeter:meter:1:general_group#frequency" } +Number:Temperature InternalTemperature { channel="mecmeter:meter:1:general_group#temperature" } +Number:Time TimeinOperation { channel="mecmeter:meter:1:general_group#op_time" } + +Number:Power ActivePower { channel="mecmeter:meter:1:activepower_group#activepower_allphase" } +Number:Power ActivePowerP1 { channel="mecmeter:meter:1:activepower_group#activepower_phase1" } +Number:Power ActivePowerP2 { channel="mecmeter:meter:1:activepower_group#activepower_phase2" } +Number:Power ActivePowerP3 { channel="mecmeter:meter:1:activepower_group#activepower_phase3" } + +Number:ElectricPotential VoltageP1 { channel="mecmeter:meter:1:voltage_group#voltage_phase1" } +Number:ElectricPotential VoltageP2 { channel="mecmeter:meter:1:voltage_group#voltage_phase2" } +Number:ElectricPotential VoltageP3 { channel="mecmeter:meter:1:voltage_group#voltage_phase3" } +Number:ElectricPotential AveragePhasePhaseVoltage { channel="mecmeter:meter:1:voltage_group#avg_phase_phase_voltage" } +Number:ElectricPotential AverageVoltage { channel="mecmeter:meter:1:voltage_group#avg_neutral_phase_voltage" } + +Number:ElectricCurrent Current { channel="mecmeter:meter:1:current_group#current_allphase" } +Number:ElectricCurrent Current_Group_Current_Phase1 { channel="mecmeter:meter:1:current_group#current_phase1" } +Number:ElectricCurrent Current_Group_Current_Phase2 { channel="mecmeter:meter:1:current_group#current_phase2" } +Number:ElectricCurrent Current_Group_Current_Phase3 { channel="mecmeter:meter:1:current_group#current_phase3" } + +Number:Power ActiveFundamentalPower { channel="mecmeter:meter:1:activefundpower_group#activefundpower_allphase" } +Number:Power ActiveFundPowerP1 { channel="mecmeter:meter:1:activefundpower_group#activefundpower_phase1" } +Number:Power ActiveFundPowerP2 { channel="mecmeter:meter:1:activefundpower_group#activefundpower_phase2" } +Number:Power ActiveFundPowerP3 { channel="mecmeter:meter:1:activefundpower_group#activefundpower_phase3" } + +Number:Power ActiveHarmonicPower { channel="mecmeter:meter:1:activeharmpower_group#activeharmpower_allphase" } +Number:Power ActiveHarmPowerP1 { channel="mecmeter:meter:1:activeharmpower_group#activeharmpower_phase1" } +Number:Power ActiveHarmPowerP2 { channel="mecmeter:meter:1:activeharmpower_group#activeharmpower_phase2" } +Number:Power ActiveHarmPowerP3 { channel="mecmeter:meter:1:activeharmpower_group#activeharmpower_phase3" } + +Number:Angle Angle_Group_Phase_Angle_Currvolt_Phase1 { channel="mecmeter:meter:1:angle_group#phase_angle_currvolt_phase1" } +Number:Angle Angle_Group_Phase_Angle_Currvolt_Phase2 { channel="mecmeter:meter:1:angle_group#phase_angle_currvolt_phase2" } +Number:Angle Angle_Group_Phase_Angle_Currvolt_Phase3 { channel="mecmeter:meter:1:angle_group#phase_angle_currvolt_phase3" } +Number:Angle Angle_Group_Phase_Angle_Phase13 { channel="mecmeter:meter:1:angle_group#phase_angle_phase1-3" } +Number:Angle Angle_Group_Phase_Angle_Phase23 { channel="mecmeter:meter:1:angle_group#phase_angle_phase2-3" } + +Number:Energy ApparentEnergyConsumption { channel="mecmeter:meter:1:app_energy_group#appenergy_consumption_allphase" } +Number:Energy ApparentEnergyP1 { channel="mecmeter:meter:1:app_energy_group#appenergy_consumption_phase1" } +Number:Energy ApparentEnergyP2 { channel="mecmeter:meter:1:app_energy_group#appenergy_consumption_phase2" } +Number:Energy ApparentEnergyP3 { channel="mecmeter:meter:1:app_energy_group#appenergy_consumption_phase3" } + +Number:Power ApparentPower { channel="mecmeter:meter:1:apppower_group#apppower_allphase" } +Number:Power ApparentPowerP1 { channel="mecmeter:meter:1:apppower_group#apppower_phase1" } +Number:Power ApparentPowerP2 { channel="mecmeter:meter:1:apppower_group#apppower_phase2" } +Number:Power ApparentPowerP3 { channel="mecmeter:meter:1:apppower_group#apppower_phase3" } + +Number:Energy ForwardActiveEnergy { channel="mecmeter:meter:1:fwd_active_energy_group#fwd_active_energy_allphase" } +Number:Energy ForwardActiveFundamentalEnergy { channel="mecmeter:meter:1:fwd_active_fund_energy_group#fwd_active_fund_energy_allphase" } +Number:Energy ForwardActiveHarmonicEnergy { channel="mecmeter:meter:1:fwd_active_harm_energy_group#fwd_active_harm_energy_allphase" } +Number:Energy ForwardReactiveEnergy { channel="mecmeter:meter:1:fwd_reactive_energy_group#fwd_reactive_energy_allphase" } + +Number:Energy FwdActiveEnergyP1 { channel="mecmeter:meter:1:fwd_active_energy_group#fwd_active_energy_phase1" } +Number:Energy FwdActiveEnergyP2 { channel="mecmeter:meter:1:fwd_active_energy_group#fwd_active_energy_phase2" } +Number:Energy FwdActiveEnergyP3 { channel="mecmeter:meter:1:fwd_active_energy_group#fwd_active_energy_phase3" } + +Number:Energy FwdActiveFundEnergyP1 { channel="mecmeter:meter:1:fwd_active_fund_energy_group#fwd_active_fund_energy_phase1" } +Number:Energy FwdActiveFundEnergyP2 { channel="mecmeter:meter:1:fwd_active_fund_energy_group#fwd_active_fund_energy_phase2" } +Number:Energy FwdActiveFundEnergyP3 { channel="mecmeter:meter:1:fwd_active_fund_energy_group#fwd_active_fund_energy_phase3" } + +Number:Energy FwdActiveHarmEnergyP1 { channel="mecmeter:meter:1:fwd_active_harm_energy_group#fwd_active_harm_energy_phase1" } +Number:Energy FwdActiveHarmEnergyP2 { channel="mecmeter:meter:1:fwd_active_harm_energy_group#fwd_active_harm_energy_phase2" } +Number:Energy FwdActiveHarmEnergyP3 { channel="mecmeter:meter:1:fwd_active_harm_energy_group#fwd_active_harm_energy_phase3" } + +Number:Energy FwdReactiveEnergyP1 { channel="mecmeter:meter:1:fwd_reactive_energy_group#fwd_reactive_energy_phase1" } +Number:Energy FwdReactiveEnergyP2 { channel="mecmeter:meter:1:fwd_reactive_energy_group#fwd_reactive_energy_phase2" } +Number:Energy FwdReactiveEnergyP3 { channel="mecmeter:meter:1:fwd_reactive_energy_group#fwd_reactive_energy_phase3" } + +Number:Energy PowerFactor { channel="mecmeter:meter:1:powerfactor_group#powerFactor_allphase" } +Number:Energy PowerFactorP1 { channel="mecmeter:meter:1:powerfactor_group#powerFactor_phase1" } +Number:Energy PowerFactorP2 { channel="mecmeter:meter:1:powerfactor_group#powerFactor_phase2" } +Number:Energy PowerFactorP3 { channel="mecmeter:meter:1:powerfactor_group#powerFactor_phase3" } + +Number:Power ReactivePower { channel="mecmeter:meter:1:reactivepower_group#reactivepower_allphase" } +Number:Power ReactivePowerP1 { channel="mecmeter:meter:1:reactivepower_group#reactivepower_phase1" } +Number:Power ReactivePowerP2 { channel="mecmeter:meter:1:reactivepower_group#reactivepower_phase2" } +Number:Power ReactivePowerP3 { channel="mecmeter:meter:1:reactivepower_group#reactivepower_phase3" } + +Number:Energy ReverseActiveEnergy { channel="mecmeter:meter:1:rev_active_energy_group#rev_active_energy_allphase" } +Number:Energy RevActiveEnergyP1 { channel="mecmeter:meter:1:rev_active_energy_group#rev_active_energy_phase1" } +Number:Energy RevActiveEnergyP2 { channel="mecmeter:meter:1:rev_active_energy_group#rev_active_energy_phase2" } +Number:Energy RevActiveEnergyP3 { channel="mecmeter:meter:1:rev_active_energy_group#rev_active_energy_phase3" } + +Number:Energy ReverseActiveFundamentalEnergy { channel="mecmeter:meter:1:rev_active_fund_energy_group#rev_active_fund_energy_allphase" } +Number:Energy RevActiveFundEnergyP1 { channel="mecmeter:meter:1:rev_active_fund_energy_group#rev_active_fund_energy_phase1" } +Number:Energy RevActiveFundEnergyP2 { channel="mecmeter:meter:1:rev_active_fund_energy_group#rev_active_fund_energy_phase2" } +Number:Energy RevActiveFundEnergyP3 { channel="mecmeter:meter:1:rev_active_fund_energy_group#rev_active_fund_energy_phase3" } + +Number:Energy ReverseActiveHarmonicEnergy { channel="mecmeter:meter:1:rev_active_harm_energy_group#rev_active_harm_energy_allphase" } +Number:Energy RevActiveHarmEnergyP1 { channel="mecmeter:meter:1:rev_active_harm_energy_group#rev_active_harm_energy_phase1" } +Number:Energy RevActiveHarmEnergyP2 { channel="mecmeter:meter:1:rev_active_harm_energy_group#rev_active_harm_energy_phase2" } +Number:Energy RevActiveHarmEnergyP3 { channel="mecmeter:meter:1:rev_active_harm_energy_group#rev_active_harm_energy_phase3" } + +Number:Energy ReverseReactiveEnergy { channel="mecmeter:meter:1:rev_reactive_energy_group#rev_reactive_energy_allphase" } +Number:Energy RevReactiveEnergyP1 { channel="mecmeter:meter:1:rev_reactive_energy_group#rev_reactive_energy_phase1" } +Number:Energy RevReactiveEnergyP2 { channel="mecmeter:meter:1:rev_reactive_energy_group#rev_reactive_energy_phase2" } +Number:Energy RevReactiveEnergyP3 { channel="mecmeter:meter:1:rev_reactive_energy_group#rev_reactive_energy_phase3" } +``` + +### mecmeter.sitemap + +``` +sitemap mecmeter label="MecMeter" +{ + Frame label="General" { + Text item=MainFrequency + Text item=InternalTemperature + } + + Frame label="Power" { + Text item=ActivePower + Text item=ActivePowerP1 + Text item=ActivePowerP2 + Text item=ActivePowerP3 + } + + Frame label="Electric Potential" { + Text item=VoltageP1 + Text item=VoltageP2 + Text item=VoltageP3 + } + +} +``` diff --git a/bundles/org.openhab.binding.mecmeter/pom.xml b/bundles/org.openhab.binding.mecmeter/pom.xml new file mode 100644 index 0000000000000..3f454bac78ba7 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.mecmeter + + openHAB Add-ons :: Bundles :: mecMeter Binding + + diff --git a/bundles/org.openhab.binding.mecmeter/src/main/feature/feature.xml b/bundles/org.openhab.binding.mecmeter/src/main/feature/feature.xml new file mode 100644 index 0000000000000..2ddad66bd474a --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.mecmeter/${project.version} + + diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterBindingConstants.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterBindingConstants.java new file mode 100644 index 0000000000000..b594cf4e39e96 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterBindingConstants.java @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link MecMeterBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + */ +@NonNullByDefault +public class MecMeterBindingConstants { + + private static final String BINDING_ID = "mecmeter"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_METER = new ThingTypeUID(BINDING_ID, "meter"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_METER); + + /* + * List of all Groups + */ + public static final String GENERAL_GROUP = "general_group"; + public static final String VOLTAGE_GROUP = "voltage_group"; + public static final String CURRENT_GROUP = "current_group"; + public static final String ANGLE_GROUP = "angle_group"; + + public static final String ACTIVE_POWER_GROUP = "activepower_group"; + public static final String ACTIVE_FUND_POWER_GROUP = "activefundpower_group"; + public static final String POWER_FACTOR_GROUP = "powerfactor_group"; + public static final String ACTIVE_HARM_POWER_GROUP = "activeharmpower_group"; + public static final String REACTIVE_POWER_GROUP = "reactivepower_group"; + + public static final String APP_POWER_GROUP = "apppower_group"; + + public static final String FWD_ACTIVE_ENERGY_GROUP = "fwd_active_energy_group"; + public static final String FWD_ACTIVE_FUND_ENERGY_GROUP = "fwd_active_fund_energy_group"; + public static final String FWD_ACTIVE_HARM_ENERGY_GROUP = "fwd_active_harm_energy_group"; + public static final String FWD_REACTIVE_ENERGY_GROUP = "fwd_reactive_energy_group"; + + public static final String REV_ACTIVE_ENERGY_GROUP = "rev_active_energy_group"; + public static final String REV_ACTIVE_FUND_ENERGY_GROUP = "rev_active_fund_energy_group"; + public static final String REV_ACTIVE_HARM_ENERGY_GROUP = "rev_active_harm_energy_group"; + public static final String REV_REACTIVE_ENERGY_GROUP = "rev_reactive_energy_group"; + + public static final String APP_ENERGY_GROUP = "app_energy_group"; + + /* + * List of all Channels + */ + // General Channels + public static final String FREQUENCY = GENERAL_GROUP + "#" + "frequency"; + public static final String TEMPERATURE = GENERAL_GROUP + "#" + "temperature"; + public static final String OPERATIONAL_TIME = GENERAL_GROUP + "#" + "op_time"; + + // Voltage Channels + public static final String VOLTAGE_PHASE_1 = VOLTAGE_GROUP + "#" + "voltage_phase1"; + public static final String VOLTAGE_PHASE_2 = VOLTAGE_GROUP + "#" + "voltage_phase2"; + public static final String VOLTAGE_PHASE_3 = VOLTAGE_GROUP + "#" + "voltage_phase3"; + + public static final String VOLTAGE_PHASE_3_TO_PHASE_2 = VOLTAGE_GROUP + "#" + "voltage_phase3-2"; + public static final String VOLTAGE_PHASE_2_TO_PHASE_1 = VOLTAGE_GROUP + "#" + "voltage_phase2-1"; + public static final String VOLTAGE_PHASE_1_TO_PHASE_3 = VOLTAGE_GROUP + "#" + "voltage_phase1-3"; + + public static final String AVERAGE_VOLTAGE_PHASE_2_PHASE = VOLTAGE_GROUP + "#" + "avg_phase_phase_voltage"; + public static final String AVERAGE_VOLTAGE_NEUTRAL_2_PHASE = VOLTAGE_GROUP + "#" + "avg_neutral_phase_voltage"; + + // Current Channels + public static final String CURRENT_PHASE_1 = CURRENT_GROUP + "#" + "current_phase1"; + public static final String CURRENT_PHASE_2 = CURRENT_GROUP + "#" + "current_phase2"; + public static final String CURRENT_PHASE_3 = CURRENT_GROUP + "#" + "current_phase3"; + public static final String CURRENT_SUM = CURRENT_GROUP + "#" + "current_allphase"; + + // Angle Channels + public static final String PHASE_ANGLE_TO_CURRENT_PHASE_1 = ANGLE_GROUP + "#" + "phase_angle_currvolt_phase1"; + public static final String PHASE_ANGLE_TO_CURRENT_PHASE_2 = ANGLE_GROUP + "#" + "phase_angle_currvolt_phase2"; + public static final String PHASE_ANGLE_TO_CURRENT_PHASE_3 = ANGLE_GROUP + "#" + "phase_angle_currvolt_phase3"; + + public static final String PHASE_ANGLE_PHASE_1_3 = ANGLE_GROUP + "#" + "phase_angle_phase1-3"; + public static final String PHASE_ANGLE_PHASE_2_3 = ANGLE_GROUP + "#" + "phase_angle_phase2-3"; + + // Power Channels + public static final String ACTIVE_POWER_PHASE_1 = ACTIVE_POWER_GROUP + "#" + "activepower_phase1"; + public static final String ACTIVE_POWER_PHASE_2 = ACTIVE_POWER_GROUP + "#" + "activepower_phase2"; + public static final String ACTIVE_POWER_PHASE_3 = ACTIVE_POWER_GROUP + "#" + "activepower_phase3"; + public static final String ACTIVE_POWER_SUM = ACTIVE_POWER_GROUP + "#" + "activepower_allphase"; + + public static final String ACTIVE_FUND_POWER_PHASE_1 = ACTIVE_FUND_POWER_GROUP + "#" + "activefundpower_phase1"; + public static final String ACTIVE_FUND_POWER_PHASE_2 = ACTIVE_FUND_POWER_GROUP + "#" + "activefundpower_phase2"; + public static final String ACTIVE_FUND_POWER_PHASE_3 = ACTIVE_FUND_POWER_GROUP + "#" + "activefundpower_phase3"; + public static final String ACTIVE_FUND_POWER_ALL = ACTIVE_FUND_POWER_GROUP + "#" + "activefundpower_allphase"; + + public static final String POWER_FACTOR_PHASE_1 = POWER_FACTOR_GROUP + "#" + "powerFactor_phase1"; + public static final String POWER_FACTOR_PHASE_2 = POWER_FACTOR_GROUP + "#" + "powerFactor_phase2"; + public static final String POWER_FACTOR_PHASE_3 = POWER_FACTOR_GROUP + "#" + "powerFactor_phase3"; + public static final String POWER_FACTOR_ALL = POWER_FACTOR_GROUP + "#" + "powerFactor_allphase"; + + public static final String ACTIVE_HARM_POWER_PHASE_1 = ACTIVE_HARM_POWER_GROUP + "#" + "activeharmpower_phase1"; + public static final String ACTIVE_HARM_POWER_PHASE_2 = ACTIVE_HARM_POWER_GROUP + "#" + "activeharmpower_phase2"; + public static final String ACTIVE_HARM_POWER_PHASE_3 = ACTIVE_HARM_POWER_GROUP + "#" + "activeharmpower_phase3"; + public static final String ACTIVE_HARM_POWER_ALL = ACTIVE_HARM_POWER_GROUP + "#" + "activeharmpower_allphase"; + + public static final String REACTIVE_POWER_PHASE_1 = REACTIVE_POWER_GROUP + "#" + "reactivepower_phase1"; + public static final String REACTIVE_POWER_PHASE_2 = REACTIVE_POWER_GROUP + "#" + "reactivepower_phase2"; + public static final String REACTIVE_POWER_PHASE_3 = REACTIVE_POWER_GROUP + "#" + "reactivepower_phase3"; + public static final String REACTIVE_POWER_ALL = REACTIVE_POWER_GROUP + "#" + "reactivepower_allphase"; + + public static final String APP_POWER_PHASE_1 = APP_POWER_GROUP + "#" + "apppower_phase1"; + public static final String APP_POWER_PHASE_2 = APP_POWER_GROUP + "#" + "apppower_phase2"; + public static final String APP_POWER_PHASE_3 = APP_POWER_GROUP + "#" + "apppower_phase3"; + public static final String APP_POWER_ALL = APP_POWER_GROUP + "#" + "apppower_allphase"; + + // Forward Energy Channels + public static final String FORWARD_ACTIVE_ENERGY_PHASE_1 = FWD_ACTIVE_ENERGY_GROUP + "#" + + "fwd_active_energy_phase1"; + public static final String FORWARD_ACTIVE_ENERGY_PHASE_2 = FWD_ACTIVE_ENERGY_GROUP + "#" + + "fwd_active_energy_phase2"; + public static final String FORWARD_ACTIVE_ENERGY_PHASE_3 = FWD_ACTIVE_ENERGY_GROUP + "#" + + "fwd_active_energy_phase3"; + public static final String FORWARD_ACTIVE_ENERGY_ALL = FWD_ACTIVE_ENERGY_GROUP + "#" + "fwd_active_energy_allphase"; + + public static final String FORWARD_ACTIVE_FUND_ENERGY_PHASE_1 = FWD_ACTIVE_FUND_ENERGY_GROUP + "#" + + "fwd_active_fund_energy_phase1"; + public static final String FORWARD_ACTIVE_FUND_ENERGY_PHASE_2 = FWD_ACTIVE_FUND_ENERGY_GROUP + "#" + + "fwd_active_fund_energy_phase2"; + public static final String FORWARD_ACTIVE_FUND_ENERGY_PHASE_3 = FWD_ACTIVE_FUND_ENERGY_GROUP + "#" + + "fwd_active_fund_energy_phase3"; + public static final String FORWARD_ACTIVE_FUND_ENERGY_ALL = FWD_ACTIVE_FUND_ENERGY_GROUP + "#" + + "fwd_active_fund_energy_allphase"; + + public static final String FORWARD_ACTIVE_HARM_ENERGY_PHASE_1 = FWD_ACTIVE_HARM_ENERGY_GROUP + "#" + + "fwd_active_harm_energy_phase1"; + public static final String FORWARD_ACTIVE_HARM_ENERGY_PHASE_2 = FWD_ACTIVE_HARM_ENERGY_GROUP + "#" + + "fwd_active_harm_energy_phase2"; + public static final String FORWARD_ACTIVE_HARM_ENERGY_PHASE_3 = FWD_ACTIVE_HARM_ENERGY_GROUP + "#" + + "fwd_active_harm_energy_phase3"; + public static final String FORWARD_ACTIVE_HARM_ENERGY_ALL = FWD_ACTIVE_HARM_ENERGY_GROUP + "#" + + "fwd_active_harm_energy_allphase"; + + public static final String FORWARD_REACTIVE_ENERGY_PHASE_1 = FWD_REACTIVE_ENERGY_GROUP + "#" + + "fwd_reactive_energy_phase1"; + public static final String FORWARD_REACTIVE_ENERGY_PHASE_2 = FWD_REACTIVE_ENERGY_GROUP + "#" + + "fwd_reactive_energy_phase2"; + public static final String FORWARD_REACTIVE_ENERGY_PHASE_3 = FWD_REACTIVE_ENERGY_GROUP + "#" + + "fwd_reactive_energy_phase3"; + public static final String FORWARD_REACTIVE_ENERGY_ALL = FWD_REACTIVE_ENERGY_GROUP + "#" + + "fwd_reactive_energy_allphase"; + + // Reverse Energy Channels + public static final String REVERSE_ACTIVE_ENERGY_PHASE_1 = REV_ACTIVE_ENERGY_GROUP + "#" + + "rev_active_energy_phase1"; + public static final String REVERSE_ACTIVE_ENERGY_PHASE_2 = REV_ACTIVE_ENERGY_GROUP + "#" + + "rev_active_energy_phase2"; + public static final String REVERSE_ACTIVE_ENERGY_PHASE_3 = REV_ACTIVE_ENERGY_GROUP + "#" + + "rev_active_energy_phase3"; + public static final String REVERSE_ACTIVE_ENERGY_ALL = REV_ACTIVE_ENERGY_GROUP + "#" + "rev_active_energy_allphase"; + + public static final String REVERSE_ACTIVE_FUND_ENERGY_PHASE_1 = REV_ACTIVE_FUND_ENERGY_GROUP + "#" + + "rev_active_fund_energy_phase1"; + public static final String REVERSE_ACTIVE_FUND_ENERGY_PHASE_2 = REV_ACTIVE_FUND_ENERGY_GROUP + "#" + + "rev_active_fund_energy_phase2"; + public static final String REVERSE_ACTIVE_FUND_ENERGY_PHASE_3 = REV_ACTIVE_FUND_ENERGY_GROUP + "#" + + "rev_active_fund_energy_phase3"; + public static final String REVERSE_ACTIVE_FUND_ENERGY_ALL = REV_ACTIVE_FUND_ENERGY_GROUP + "#" + + "rev_active_fund_energy_allphase"; + + public static final String REVERSE_ACTIVE_HARM_ENERGY_PHASE_1 = REV_ACTIVE_HARM_ENERGY_GROUP + "#" + + "rev_active_harm_energy_phase1"; + public static final String REVERSE_ACTIVE_HARM_ENERGY_PHASE_2 = REV_ACTIVE_HARM_ENERGY_GROUP + "#" + + "rev_active_harm_energy_phase2"; + public static final String REVERSE_ACTIVE_HARM_ENERGY_PHASE_3 = REV_ACTIVE_HARM_ENERGY_GROUP + "#" + + "rev_active_harm_energy_phase3"; + public static final String REVERSE_ACTIVE_HARM_ENERGY_ALL = REV_ACTIVE_HARM_ENERGY_GROUP + "#" + + "rev_active_harm_energy_allphase"; + + public static final String REVERSE_REACTIVE_ENERGY_PHASE_1 = REV_REACTIVE_ENERGY_GROUP + "#" + + "rev_reactive_energy_phase1"; + public static final String REVERSE_REACTIVE_ENERGY_PHASE_2 = REV_REACTIVE_ENERGY_GROUP + "#" + + "rev_reactive_energy_phase2"; + public static final String REVERSE_REACTIVE_ENERGY_PHASE_3 = REV_REACTIVE_ENERGY_GROUP + "#" + + "rev_reactive_energy_phase3"; + public static final String REVERSE_REACTIVE_ENERGY_ALL = REV_REACTIVE_ENERGY_GROUP + "#" + + "rev_reactive_energy_allphase"; + + // Apparent Energy Channels + public static final String APP_ENERGY_PHASE_1 = APP_ENERGY_GROUP + "#" + "appenergy_consumption_phase1"; + public static final String APP_ENERGY_PHASE_2 = APP_ENERGY_GROUP + "#" + "appenergy_consumption_phase2"; + public static final String APP_ENERGY_PHASE_3 = APP_ENERGY_GROUP + "#" + "appenergy_consumption_phase3"; + public static final String APP_ENERGY_ALL = APP_ENERGY_GROUP + "#" + "appenergy_consumption_allphase"; + + // list of all URLs + public static final String POWERMETER_DATA_URL = "http://%IP%/wizard/public/api/measurements"; +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterDeviceConfiguration.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterDeviceConfiguration.java new file mode 100644 index 0000000000000..605a1678cd513 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/MecMeterDeviceConfiguration.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link MecMeterDeviceConfiguration} is the class used to match the + * thing configuration. + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + */ +@NonNullByDefault +public class MecMeterDeviceConfiguration { + public String ip = ""; + public String password = "12345"; + public int refreshInterval = 5; + + public String getIp() { + return ip; + } + + public void setIp(String inetaddress) { + ip = inetaddress; + } + + public String getPassword() { + return password; + } + + public void setPassword(String pw) { + password = pw; + } + + public int getRefreshInterval() { + return refreshInterval; + } + + public void setRefreshInterval(int ri) { + refreshInterval = ri; + } + + public @Nullable String isValid() { + if (ip.isBlank()) { + return "Missing IP"; + } + if (password.isBlank()) { + return "Password is missing"; + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/handler/MecMeterHandler.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/handler/MecMeterHandler.java new file mode 100644 index 0000000000000..c66aa985123d5 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/handler/MecMeterHandler.java @@ -0,0 +1,408 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter.handler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.mecmeter.MecMeterBindingConstants; +import org.openhab.binding.mecmeter.MecMeterDeviceConfiguration; +import org.openhab.binding.mecmeter.internal.dto.MecMeterResponse; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +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.binding.BaseThingHandler; +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 com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link MecMeterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + * @author Kai Kreuzer - Refactoring for openHAB 3 + */ +@NonNullByDefault +public class MecMeterHandler extends BaseThingHandler { + + private static final int API_TIMEOUT = 5000; // set on 5000ms - not specified in datasheet + + private static final String USERNAME = "admin"; + + private final Logger logger = LoggerFactory.getLogger(MecMeterHandler.class); + + private Gson gson = new Gson(); + + private final HttpClient httpClient; + + private @Nullable ScheduledFuture pollFuture; + + private @Nullable MecMeterResponse powerMeterResponse; + + public MecMeterHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.httpClient = httpClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateChannel(channelUID.getId()); + } else { + logger.debug("Received unsupported command {}.", command); + } + } + + /** + * function which is called to refresh the data + */ + public void refresh() { + updateData(); + updateChannels(); + } + + @Override + public void dispose() { + super.dispose(); + logger.debug("removing thing.."); + if (pollFuture != null) { + pollFuture.cancel(true); + } + } + + @Override + public void initialize() { + MecMeterDeviceConfiguration config = getConfig().as(MecMeterDeviceConfiguration.class); + String configCheck = config.isValid(); + + if (configCheck != null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configCheck); + return; + } + updateStatus(ThingStatus.UNKNOWN); + + if (pollFuture != null) { + pollFuture.cancel(false); + } + pollFuture = scheduler.scheduleWithFixedDelay(() -> { + refresh(); + }, 0, config.refreshInterval, TimeUnit.SECONDS); + } + + /** + * Get new data + * Function to save Response of the powermeter + */ + private void updateData() { + powerMeterResponse = getRealtimeData(); + } + + /** + * Get new realtime data over the network + * + * @return MecMeterResponse class where json values "are saved" + */ + private @Nullable MecMeterResponse getRealtimeData() { + MecMeterResponse result = null; + boolean resultOk = false; + String errorMsg = null; + + MecMeterDeviceConfiguration config = getConfig().as(MecMeterDeviceConfiguration.class); + + try { + String basicAuthentication = "Basic " + Base64.getEncoder() + .encodeToString(new String(USERNAME + ":" + config.password).getBytes(StandardCharsets.ISO_8859_1)); + + String location = MecMeterBindingConstants.POWERMETER_DATA_URL.replace("%IP%", config.ip.strip()); + + ContentResponse response = httpClient.newRequest(location).method(HttpMethod.GET) + .header(HttpHeader.AUTHORIZATION, basicAuthentication).timeout(API_TIMEOUT, TimeUnit.MILLISECONDS) + .send(); + if (response.getStatus() != 200) { + errorMsg = "Reading meter did not succeed: " + response.getReason(); + logger.error("Request to meter failed: HTTP {}: {}", response.getStatus(), response.getReason()); + } else { + result = gson.fromJson(response.getContentAsString(), MecMeterResponse.class); + if (result == null) { + errorMsg = "no data returned"; + logger.error("no data returned from meter at {}", location); + } else { + resultOk = true; + } + } + } catch (JsonSyntaxException e) { + errorMsg = "Configuration is incorrect"; + logger.error("Error running power meter request: {}", e.getMessage()); + } catch (IllegalStateException e) { + errorMsg = "Connection failed"; + logger.error("Error running powermeter request: {}", e.getMessage()); + } catch (InterruptedException e) { + logger.debug("Http request has been interrupted: {}", e.getMessage()); + } catch (TimeoutException e) { + logger.debug("Http request ran into a timeout: {}", e.getMessage()); + errorMsg = "Connection to power meter timed out."; + } catch (ExecutionException e) { + logger.debug("Http request did not succeed: {}", e.getMessage()); + errorMsg = "Connection problem: " + e.getMessage(); + } + + // Update the thing status + if (resultOk) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMsg); + } + + return resultOk ? result : null; + } + + /** + * Update all Channels + */ + protected void updateChannels() { + for (Channel channel : getThing().getChannels()) { + updateChannel(channel.getUID().getId()); + } + } + + /** + * Update the channel state + * + * @param channelId the id identifying the channel to be updated + */ + protected void updateChannel(String channelId) { + if (!isLinked(channelId)) { + return; + } + State state = getState(channelId); + if (state != null) { + updateState(channelId, state); + } + } + + /** + * Get the state of a given channel + * + * @param channelId the id identifying the channel to be updated + * @return state of the channel + */ + protected @Nullable State getState(String channelId) { + MecMeterResponse response = powerMeterResponse; + if (response == null) { + return null; + } else { + switch (channelId) { + /* General */ + case MecMeterBindingConstants.FREQUENCY: + return new QuantityType<>(response.getFrequency(), Units.HERTZ); + case MecMeterBindingConstants.TEMPERATURE: + return new QuantityType<>(response.getTemperature(), SIUnits.CELSIUS); + case MecMeterBindingConstants.OPERATIONAL_TIME: + return new QuantityType<>(response.getOperationalTime() / 1000, Units.SECOND); + + /* Voltage */ + case MecMeterBindingConstants.VOLTAGE_PHASE_1: + return new QuantityType<>(response.getVoltagePhase1(), Units.VOLT); + case MecMeterBindingConstants.VOLTAGE_PHASE_2: + return new QuantityType<>(response.getVoltagePhase2(), Units.VOLT); + case MecMeterBindingConstants.VOLTAGE_PHASE_3: + return new QuantityType<>(response.getVoltagePhase3(), Units.VOLT); + case MecMeterBindingConstants.VOLTAGE_PHASE_3_TO_PHASE_2: + return new QuantityType<>(response.getVoltagePhase3ToPhase2(), Units.VOLT); + case MecMeterBindingConstants.VOLTAGE_PHASE_2_TO_PHASE_1: + return new QuantityType<>(response.getVoltagePhase2ToPhase1(), Units.VOLT); + case MecMeterBindingConstants.VOLTAGE_PHASE_1_TO_PHASE_3: + return new QuantityType<>(response.getVoltagePhase1ToPhase3(), Units.VOLT); + case MecMeterBindingConstants.AVERAGE_VOLTAGE_PHASE_2_PHASE: + return new QuantityType<>(response.getAverageVoltagePhaseToPhase(), Units.VOLT); + case MecMeterBindingConstants.AVERAGE_VOLTAGE_NEUTRAL_2_PHASE: + return new QuantityType<>(response.getAverageVoltageNeutralToPhase(), Units.VOLT); + + /* Current */ + case MecMeterBindingConstants.CURRENT_PHASE_1: + return new QuantityType<>(response.getCurrentPhase1(), Units.AMPERE); + case MecMeterBindingConstants.CURRENT_PHASE_2: + return new QuantityType<>(response.getCurrentPhase2(), Units.AMPERE); + case MecMeterBindingConstants.CURRENT_PHASE_3: + return new QuantityType<>(response.getCurrentPhase3(), Units.AMPERE); + case MecMeterBindingConstants.CURRENT_SUM: + return new QuantityType<>(response.getCurrentSum(), Units.AMPERE); + + /* Angles */ + case MecMeterBindingConstants.PHASE_ANGLE_TO_CURRENT_PHASE_1: + return new QuantityType<>(response.getPhaseAngleCurrentToVoltagePhase1(), Units.DEGREE_ANGLE); + case MecMeterBindingConstants.PHASE_ANGLE_TO_CURRENT_PHASE_2: + return new QuantityType<>(response.getPhaseAngleCurrentToVoltagePhase2(), Units.DEGREE_ANGLE); + case MecMeterBindingConstants.PHASE_ANGLE_TO_CURRENT_PHASE_3: + return new QuantityType<>(response.getPhaseAngleCurrentToVoltagePhase3(), Units.DEGREE_ANGLE); + case MecMeterBindingConstants.PHASE_ANGLE_PHASE_1_3: + return new QuantityType<>(response.getPhaseAnglePhase1To3(), Units.DEGREE_ANGLE); + case MecMeterBindingConstants.PHASE_ANGLE_PHASE_2_3: + return new QuantityType<>(response.getPhaseAnglePhase2To3(), Units.DEGREE_ANGLE); + + /* Power */ + case MecMeterBindingConstants.ACTIVE_POWER_PHASE_1: + return new QuantityType<>(response.getActivePowerPhase1(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_POWER_PHASE_2: + return new QuantityType<>(response.getActivePowerPhase2(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_POWER_PHASE_3: + return new QuantityType<>(response.getActivePowerPhase3(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_POWER_SUM: + return new QuantityType<>(response.getActivePowerSum(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_FUND_POWER_PHASE_1: + return new QuantityType<>(response.getActiveFundamentalPowerPhase1(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_FUND_POWER_PHASE_2: + return new QuantityType<>(response.getActiveFundamentalPowerPhase2(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_FUND_POWER_PHASE_3: + return new QuantityType<>(response.getActiveFundamentalPowerPhase3(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_FUND_POWER_ALL: + return new QuantityType<>(response.getActiveFundamentalPowerSum(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_HARM_POWER_PHASE_1: + return new QuantityType<>(response.getActiveHarmonicPowerPhase1(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_HARM_POWER_PHASE_2: + return new QuantityType<>(response.getActiveHarmonicPowerPhase2(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_HARM_POWER_PHASE_3: + return new QuantityType<>(response.getActiveHarmonicPowerPhase3(), Units.WATT); + case MecMeterBindingConstants.ACTIVE_HARM_POWER_ALL: + return new QuantityType<>(response.getActiveHarmonicPowerSum(), Units.WATT); + case MecMeterBindingConstants.REACTIVE_POWER_PHASE_1: + return new QuantityType<>(response.getReactivePowerPhase1(), Units.VAR); + case MecMeterBindingConstants.REACTIVE_POWER_PHASE_2: + return new QuantityType<>(response.getReactivePowerPhase2(), Units.VAR); + case MecMeterBindingConstants.REACTIVE_POWER_PHASE_3: + return new QuantityType<>(response.getReactivePowerPhase3(), Units.VAR); + case MecMeterBindingConstants.REACTIVE_POWER_ALL: + return new QuantityType<>(response.getReactivePowerSum(), Units.VAR); + case MecMeterBindingConstants.APP_POWER_PHASE_1: + return new QuantityType<>(response.getApparentPowerPhase1(), Units.VOLT_AMPERE); + case MecMeterBindingConstants.APP_POWER_PHASE_2: + return new QuantityType<>(response.getApparentPowerPhase2(), Units.VOLT_AMPERE); + case MecMeterBindingConstants.APP_POWER_PHASE_3: + return new QuantityType<>(response.getApparentPowerPhase3(), Units.VOLT_AMPERE); + case MecMeterBindingConstants.APP_POWER_ALL: + return new QuantityType<>(response.getApparentPowerSum(), Units.VOLT_AMPERE); + + /* Forward Energy */ + case MecMeterBindingConstants.FORWARD_ACTIVE_ENERGY_PHASE_1: + return new QuantityType<>(response.getForwardActiveEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_ENERGY_PHASE_2: + return new QuantityType<>(response.getForwardActiveEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_ENERGY_PHASE_3: + return new QuantityType<>(response.getForwardActiveEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_ENERGY_ALL: + return new QuantityType<>(response.getForwardActiveEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_FUND_ENERGY_PHASE_1: + return new QuantityType<>(response.getForwardActiveFundamentalEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_FUND_ENERGY_PHASE_2: + return new QuantityType<>(response.getForwardActiveFundamentalEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_FUND_ENERGY_PHASE_3: + return new QuantityType<>(response.getForwardActiveFundamentalEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_FUND_ENERGY_ALL: + return new QuantityType<>(response.getForwardActiveFundamentalEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_HARM_ENERGY_PHASE_1: + return new QuantityType<>(response.getForwardActiveHarmonicEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_HARM_ENERGY_PHASE_2: + return new QuantityType<>(response.getForwardActiveHarmonicEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_HARM_ENERGY_PHASE_3: + return new QuantityType<>(response.getForwardActiveHarmonicEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_ACTIVE_HARM_ENERGY_ALL: + return new QuantityType<>(response.getForwardActiveHarmonicEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.FORWARD_REACTIVE_ENERGY_PHASE_1: + return new QuantityType<>(response.getForwardReactiveEnergyPhase1(), Units.VAR_HOUR); + case MecMeterBindingConstants.FORWARD_REACTIVE_ENERGY_PHASE_2: + return new QuantityType<>(response.getForwardReactiveEnergyPhase2(), Units.VAR_HOUR); + case MecMeterBindingConstants.FORWARD_REACTIVE_ENERGY_PHASE_3: + return new QuantityType<>(response.getForwardReactiveEnergyPhase3(), Units.VAR_HOUR); + case MecMeterBindingConstants.FORWARD_REACTIVE_ENERGY_ALL: + return new QuantityType<>(response.getForwardReactiveEnergySum(), Units.VAR_HOUR); + + /* Reverse Energy */ + case MecMeterBindingConstants.REVERSE_ACTIVE_ENERGY_PHASE_1: + return new QuantityType<>(response.getReverseActiveEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_ENERGY_PHASE_2: + return new QuantityType<>(response.getReverseActiveEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_ENERGY_PHASE_3: + return new QuantityType<>(response.getReverseActiveEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_ENERGY_ALL: + return new QuantityType<>(response.getReverseActiveEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_FUND_ENERGY_PHASE_1: + return new QuantityType<>(response.getReverseActiveFundamentalEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_FUND_ENERGY_PHASE_2: + return new QuantityType<>(response.getReverseActiveFundamentalEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_FUND_ENERGY_PHASE_3: + return new QuantityType<>(response.getReverseActiveFundamentalEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_FUND_ENERGY_ALL: + return new QuantityType<>(response.getReverseActiveFundamentalEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_HARM_ENERGY_PHASE_1: + return new QuantityType<>(response.getReverseActiveHarmonicEnergyPhase1(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_HARM_ENERGY_PHASE_2: + return new QuantityType<>(response.getReverseActiveHarmonicEnergyPhase2(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_HARM_ENERGY_PHASE_3: + return new QuantityType<>(response.getReverseActiveHarmonicEnergyPhase3(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_ACTIVE_HARM_ENERGY_ALL: + return new QuantityType<>(response.getReverseActiveHarmonicEnergySum(), Units.KILOWATT_HOUR); + case MecMeterBindingConstants.REVERSE_REACTIVE_ENERGY_PHASE_1: + return new QuantityType<>(response.getReverseReactiveEnergyPhase1(), Units.VAR_HOUR); + case MecMeterBindingConstants.REVERSE_REACTIVE_ENERGY_PHASE_2: + return new QuantityType<>(response.getReverseReactiveEnergyPhase2(), Units.VAR_HOUR); + case MecMeterBindingConstants.REVERSE_REACTIVE_ENERGY_PHASE_3: + return new QuantityType<>(response.getReverseReactiveEnergyPhase3(), Units.VAR_HOUR); + case MecMeterBindingConstants.REVERSE_REACTIVE_ENERGY_ALL: + return new QuantityType<>(response.getReverseReactiveEnergySum(), Units.VAR_HOUR); + + /* Apparent Energy */ + case MecMeterBindingConstants.APP_ENERGY_PHASE_1: + return new QuantityType<>(response.getApparentEnergyConsumptionPhase1(), Units.VOLT_AMPERE_HOUR); + case MecMeterBindingConstants.APP_ENERGY_PHASE_2: + return new QuantityType<>(response.getApparentEnergyConsumptionPhase2(), Units.VOLT_AMPERE_HOUR); + case MecMeterBindingConstants.APP_ENERGY_PHASE_3: + return new QuantityType<>(response.getApparentEnergyConsumptionPhase3(), Units.VOLT_AMPERE_HOUR); + case MecMeterBindingConstants.APP_ENERGY_ALL: + return new QuantityType<>(response.getApparentEnergyConsumptionSum(), Units.VOLT_AMPERE_HOUR); + + /* Power Factor */ + case MecMeterBindingConstants.POWER_FACTOR_PHASE_1: + return new QuantityType<>(response.getPowerFactorPhase1(), Units.ONE); + case MecMeterBindingConstants.POWER_FACTOR_PHASE_2: + return new QuantityType<>(response.getPowerFactorPhase2(), Units.ONE); + case MecMeterBindingConstants.POWER_FACTOR_PHASE_3: + return new QuantityType<>(response.getPowerFactorPhase3(), Units.ONE); + case MecMeterBindingConstants.POWER_FACTOR_ALL: + return new QuantityType<>(response.getPowerFactorSum(), Units.ONE); + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/MecMeterHandlerFactory.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/MecMeterHandlerFactory.java new file mode 100644 index 0000000000000..f26fca199601f --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/MecMeterHandlerFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter.internal; + +import static org.openhab.binding.mecmeter.MecMeterBindingConstants.THING_TYPE_METER; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.mecmeter.MecMeterBindingConstants; +import org.openhab.binding.mecmeter.handler.MecMeterHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MecMeterHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + * @author Kai Kreuzer - Refactoring for openHAB 3 + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.mecmeter") +public class MecMeterHandlerFactory extends BaseThingHandlerFactory { + + private final HttpClient httpClient; + + @Activate + public MecMeterHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return MecMeterBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_METER)) { + return new MecMeterHandler(thing, httpClient); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/discovery/MecMeterDiscoveryParticipant.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/discovery/MecMeterDiscoveryParticipant.java new file mode 100644 index 0000000000000..b6b6762d3eca9 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/discovery/MecMeterDiscoveryParticipant.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter.internal.discovery; + +import static org.openhab.binding.mecmeter.MecMeterBindingConstants.THING_TYPE_METER; + +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mecmeter.MecMeterBindingConstants; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.openhab.core.thing.Thing; +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 MecMeterDiscoveryParticipant} is responsible for discovering devices, which are + * sent to inbox. + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + * @author Kai Kreuzer - Refactoring for openHAB 3 + */ +@NonNullByDefault +@Component(service = MDNSDiscoveryParticipant.class) +public class MecMeterDiscoveryParticipant implements MDNSDiscoveryParticipant { + + private Logger logger = LoggerFactory.getLogger(MecMeterDiscoveryParticipant.class); + private static final String SERVICE_TYPE = "_http._tcp.local."; + + /** + * Match the serial number, vendor and model of the discovered PowerMeter. + * Input is like "vpmAA11BB33CC55" + */ + private static final Pattern MECMETER_PATTERN = Pattern + .compile("^(vpm|mec)[A-F0-9]{12}\\._http\\._tcp\\.local\\.$"); + + @Override + public Set getSupportedThingTypeUIDs() { + return MecMeterBindingConstants.SUPPORTED_THING_TYPES_UIDS; + } + + @Override + public String getServiceType() { + return SERVICE_TYPE; + } + + @Override + public @Nullable DiscoveryResult createResult(ServiceInfo service) { + String qualifiedName = service.getQualifiedName(); + logger.debug("Device found: {}", qualifiedName); + ThingUID uid = getThingUID(service); + if (uid == null) { + return null; + } + + String serial = qualifiedName.substring(3, 15); + String vendor = "MEC"; + + InetAddress ip = getIpAddress(service); + if (ip == null) { + return null; + } + String inetAddress = ip.toString().substring(1); + + Map properties = new HashMap<>(3); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, serial); + properties.put(Thing.PROPERTY_VENDOR, vendor); + properties.put("ip", inetAddress); + + String label = "MEC Power Meter"; + return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label) + .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build(); + } + + private @Nullable InetAddress getIpAddress(ServiceInfo service) { + if (service.getInet4Addresses().length > 0) { + return service.getInet4Addresses()[0]; + } else { + return null; + } + } + + @Override + public @Nullable ThingUID getThingUID(ServiceInfo service) { + Matcher matcher = MECMETER_PATTERN.matcher(service.getQualifiedName()); + if (matcher.matches()) { + String serial = service.getQualifiedName().substring(3, 15); // Qualified Name like "mecABCDEF123456", we + // want "ABCDEF123456" + return new ThingUID(THING_TYPE_METER, serial); + } else { + logger.debug("The discovered device is not supported, ignoring it."); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/dto/MecMeterResponse.java b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/dto/MecMeterResponse.java new file mode 100644 index 0000000000000..bcd042707684d --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/java/org/openhab/binding/mecmeter/internal/dto/MecMeterResponse.java @@ -0,0 +1,856 @@ +/** + * Copyright (c) 2010-2021 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.mecmeter.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link MecMeterResponse} is responsible for storing + * the "data" node of the JSON response + * + * @author Florian Pazour - Initial contribution + * @author Klaus Berger - Initial contribution + * @author Kai Kreuzer - Refactoring for openHAB 3 + */ +public class MecMeterResponse { + /* General */ + @SerializedName("F") + private float frequency; + @SerializedName("T") + private float temperature; + @SerializedName("TIME") + private long operationalTime; + + /* Voltage */ + @SerializedName("VA") + private float voltagePhase1; + @SerializedName("VB") + private float voltagePhase2; + @SerializedName("VC") + private float voltagePhase3; + @SerializedName("VCB") + private float voltagePhase3ToPhase2; + @SerializedName("VBA") + private float voltagePhase2ToPhase1; + @SerializedName("VAC") + private float voltagePhase1ToPhase3; + @SerializedName("VPT") + private float averageVoltagePhaseToPhase; + @SerializedName("VT") + private float averageVoltageNeutralToPhase; + + /* Current */ + @SerializedName("IA") + private float currentPhase1; + @SerializedName("IB") + private float currentPhase2; + @SerializedName("IC") + private float currentPhase3; + @SerializedName("IN") + private float currentSum; + + /* Angles */ + @SerializedName("IAA") + private float phaseAngleCurrentToVoltagePhase1; + @SerializedName("IAB") + private float phaseAngleCurrentToVoltagePhase2; + @SerializedName("IAC") + private float phaseAngleCurrentToVoltagePhase3; + @SerializedName("UAA") + private float phaseAnglePhase1To3; + @SerializedName("UAB") + private float phaseAnglePhase2To3; + + /* Power */ + @SerializedName("PA") + private float activePowerPhase1; + @SerializedName("PB") + private float activePowerPhase2; + @SerializedName("PC") + private float activePowerPhase3; + @SerializedName("PT") + private float activePowerSum; + + @SerializedName("PAF") + private float activeFundamentalPowerPhase1; + @SerializedName("PBF") + private float activeFundamentalPowerPhase2; + @SerializedName("PCF") + private float activeFundamentalPowerPhase3; + @SerializedName("PTF") + private float activeFundamentalPowerSum; + + @SerializedName("PFA") + private float powerFactorPhase1; + @SerializedName("PFB") + private float powerFactorPhase2; + @SerializedName("PFC") + private float powerFactorPhase3; + @SerializedName("PFT") + private float powerFactorSum; + + @SerializedName("PAH") + private float activeHarmonicPowerPhase1; + @SerializedName("PBH") + private float activeHarmonicPowerPhase2; + @SerializedName("PCH") + private float activeHarmonicPowerPhase3; + @SerializedName("PTH") + private float activeHarmonicPowerSum; + + @SerializedName("QA") + private float reactivePowerPhase1; + @SerializedName("QB") + private float reactivePowerPhase2; + @SerializedName("QC") + private float reactivePowerPhase3; + @SerializedName("QT") + private float reactivePowerSum; + + @SerializedName("SA") + private float apparentPowerPhase1; + @SerializedName("SB") + private float apparentPowerPhase2; + @SerializedName("SC") + private float apparentPowerPhase3; + @SerializedName("ST") + private float apparentPowerSum; + + /* Forward Energy */ + @SerializedName("EFAA") + private float forwardActiveEnergyPhase1; + @SerializedName("EFAB") + private float forwardActiveEnergyPhase2; + @SerializedName("EFAC") + private float forwardActiveEnergyPhase3; + @SerializedName("EFAT") + private float forwardActiveEnergySum; + + @SerializedName("EFAF") + private float forwardActiveFundamentalEnergyPhase1; + @SerializedName("EFBF") + private float forwardActiveFundamentalEnergyPhase2; + @SerializedName("EFCF") + private float forwardActiveFundamentalEnergyPhase3; + @SerializedName("EFTF") + private float forwardActiveFundamentalEnergySum; + + @SerializedName("EFAH") + private float forwardActiveHarmonicEnergyPhase1; + @SerializedName("EFBH") + private float forwardActiveHarmonicEnergyPhase2; + @SerializedName("EFCH") + private float forwardActiveHarmonicEnergyPhase3; + @SerializedName("EFTH") + private float forwardActiveHarmonicEnergySum; + + @SerializedName("EFRA") + private float forwardReactiveEnergyPhase1; + @SerializedName("EFRB") + private float forwardReactiveEnergyPhase2; + @SerializedName("EFRC") + private float forwardReactiveEnergyPhase3; + @SerializedName("EFRT") + private float forwardReactiveEnergySum; + + /* Reverse Energy */ + @SerializedName("ERAA") + private float reverseActiveEnergyPhase1; + @SerializedName("ERAB") + private float reverseActiveEnergyPhase2; + @SerializedName("ERAC") + private float reverseActiveEnergyPhase3; + @SerializedName("ERAT") + private float reverseActiveEnergySum; + + @SerializedName("ERAF") + private float reverseActiveFundamentalEnergyPhase1; + @SerializedName("ERBF") + private float reverseActiveFundamentalEnergyPhase2; + @SerializedName("ERCF") + private float reverseActiveFundamentalEnergyPhase3; + @SerializedName("ERTF") + private float reverseActiveFundamentalEnergySum; + + @SerializedName("ERAH") + private float reverseActiveHarmonicEnergyPhase1; + @SerializedName("ERBH") + private float reverseActiveHarmonicEnergyPhase2; + @SerializedName("ERCH") + private float reverseActiveHarmonicEnergyPhase3; + @SerializedName("ERTH") + private float reverseActiveHarmonicEnergySum; + + @SerializedName("ERRA") + private float reverseReactiveEnergyPhase1; + @SerializedName("ERRB") + private float reverseReactiveEnergyPhase2; + @SerializedName("ERRC") + private float reverseReactiveEnergyPhase3; + @SerializedName("ERRT") + private float reverseReactiveEnergySum; + + /* apparent Energy */ + @SerializedName("ESA") + private float apparentEnergyConsumptionPhase1; + @SerializedName("ESB") + private float apparentEnergyConsumptionPhase2; + @SerializedName("ESC") + private float apparentEnergyConsumptionPhase3; + @SerializedName("EST") + private float apparentEnergyConsumptionSum; + + /* Constants */ + private static final int KILO = 1000; + + /* Getters and Setters */ + public float getFrequency() { + return frequency; + } + + public void setFrequency(float frequency) { + this.frequency = frequency; + } + + public float getTemperature() { + return temperature; + } + + public void setTemperature(float temperature) { + this.temperature = temperature; + } + + public long getOperationalTime() { + return operationalTime; + } + + public void setOperationalTime(long operationalTime) { + this.operationalTime = operationalTime; + } + + public float getVoltagePhase1() { + return voltagePhase1; + } + + public void setVoltagePhase1(float voltagePhase1) { + this.voltagePhase1 = voltagePhase1; + } + + public float getVoltagePhase2() { + return voltagePhase2; + } + + public void setVoltagePhase2(float voltagePhase2) { + this.voltagePhase2 = voltagePhase2; + } + + public float getVoltagePhase3() { + return voltagePhase3; + } + + public void setVoltagePhase3(float voltagePhase3) { + this.voltagePhase3 = voltagePhase3; + } + + public float getVoltagePhase3ToPhase2() { + return voltagePhase3ToPhase2; + } + + public void setVoltagePhase3ToPhase2(float voltagePhase3ToPhase2) { + this.voltagePhase3ToPhase2 = voltagePhase3ToPhase2; + } + + public float getVoltagePhase2ToPhase1() { + return voltagePhase2ToPhase1; + } + + public void setVoltagePhase2ToPhase1(float voltagePhase2ToPhase1) { + this.voltagePhase2ToPhase1 = voltagePhase2ToPhase1; + } + + public float getVoltagePhase1ToPhase3() { + return voltagePhase1ToPhase3; + } + + public void setVoltagePhase1ToPhase3(float voltagePhase1ToPhase3) { + this.voltagePhase1ToPhase3 = voltagePhase1ToPhase3; + } + + public float getAverageVoltagePhaseToPhase() { + return averageVoltagePhaseToPhase; + } + + public void setAverageVoltagePhaseToPhase(float averageVoltagePhaseToPhase) { + this.averageVoltagePhaseToPhase = averageVoltagePhaseToPhase; + } + + public float getAverageVoltageNeutralToPhase() { + return averageVoltageNeutralToPhase; + } + + public void setAverageVoltageNeutralToPhase(float averageVoltageNeutralToPhase) { + this.averageVoltageNeutralToPhase = averageVoltageNeutralToPhase; + } + + public float getCurrentPhase1() { + return currentPhase1; + } + + public void setCurrentPhase1(float currentPhase1) { + this.currentPhase1 = currentPhase1; + } + + public float getCurrentPhase2() { + return currentPhase2; + } + + public void setCurrentPhase2(float currentPhase2) { + this.currentPhase2 = currentPhase2; + } + + public float getCurrentPhase3() { + return currentPhase3; + } + + public void setCurrentPhase3(float currentPhase3) { + this.currentPhase3 = currentPhase3; + } + + public float getCurrentSum() { + return currentSum; + } + + public void setCurrentSum(float currentSum) { + this.currentSum = currentSum; + } + + public float getPhaseAngleCurrentToVoltagePhase1() { + return phaseAngleCurrentToVoltagePhase1; + } + + public void setPhaseAngleCurrentToVoltagePhase1(float phaseAngleCurrentToVoltagePhase1) { + this.phaseAngleCurrentToVoltagePhase1 = phaseAngleCurrentToVoltagePhase1; + } + + public float getPhaseAngleCurrentToVoltagePhase2() { + return phaseAngleCurrentToVoltagePhase2; + } + + public void setPhaseAngleCurrentToVoltagePhase2(float phaseAngleCurrentToVoltagePhase2) { + this.phaseAngleCurrentToVoltagePhase2 = phaseAngleCurrentToVoltagePhase2; + } + + public float getPhaseAngleCurrentToVoltagePhase3() { + return phaseAngleCurrentToVoltagePhase3; + } + + public void setPhaseAngleCurrentToVoltagePhase3(float phaseAngleCurrentToVoltagePhase3) { + this.phaseAngleCurrentToVoltagePhase3 = phaseAngleCurrentToVoltagePhase3; + } + + public float getPhaseAnglePhase1To3() { + return phaseAnglePhase1To3; + } + + public void setPhaseAnglePhase1To3(float phaseAnglePhase1To3) { + this.phaseAnglePhase1To3 = phaseAnglePhase1To3; + } + + public float getPhaseAnglePhase2To3() { + return phaseAnglePhase2To3; + } + + public void setPhaseAnglePhase2To3(float phaseAnglePhase2To3) { + this.phaseAnglePhase2To3 = phaseAnglePhase2To3; + } + + public float getActivePowerPhase1() { + return activePowerPhase1; + } + + public void setActivePowerPhase1(float activePowerPhase1) { + this.activePowerPhase1 = activePowerPhase1; + } + + public float getActivePowerPhase2() { + return activePowerPhase2; + } + + public void setActivePowerPhase2(float activePowerPhase2) { + this.activePowerPhase2 = activePowerPhase2; + } + + public float getActivePowerPhase3() { + return activePowerPhase3; + } + + public void setActivePowerPhase3(float activePowerPhase3) { + this.activePowerPhase3 = activePowerPhase3; + } + + public float getActivePowerSum() { + return activePowerSum; + } + + public void setActivePowerSum(float activePowerSum) { + this.activePowerSum = activePowerSum; + } + + public float getActiveFundamentalPowerPhase1() { + return activeFundamentalPowerPhase1; + } + + public void setActiveFundamentalPowerPhase1(float activeFundamentalPowerPhase1) { + this.activeFundamentalPowerPhase1 = activeFundamentalPowerPhase1; + } + + public float getPowerFactorPhase1() { + return powerFactorPhase1; + } + + public void setPowerFactorPhase1(float powerFactorPhase1) { + this.powerFactorPhase1 = powerFactorPhase1; + } + + public float getPowerFactorPhase2() { + return powerFactorPhase2; + } + + public void setPowerFactorPhase2(float powerFactorPhase2) { + this.powerFactorPhase2 = powerFactorPhase2; + } + + public float getPowerFactorPhase3() { + return powerFactorPhase3; + } + + public void setPowerFactorPhase3(float powerFactorPhase3) { + this.powerFactorPhase3 = powerFactorPhase3; + } + + public float getPowerFactorSum() { + return powerFactorSum; + } + + public void setPowerFactorSum(float powerFactorSum) { + this.powerFactorSum = powerFactorSum; + } + + public float getActiveFundamentalPowerPhase2() { + return activeFundamentalPowerPhase2; + } + + public void setActiveFundamentalPowerPhase2(float activeFundamentalPowerPhase2) { + this.activeFundamentalPowerPhase2 = activeFundamentalPowerPhase2; + } + + public float getActiveFundamentalPowerPhase3() { + return activeFundamentalPowerPhase3; + } + + public void setActiveFundamentalPowerPhase3(float activeFundamentalPowerPhase3) { + this.activeFundamentalPowerPhase3 = activeFundamentalPowerPhase3; + } + + public float getActiveFundamentalPowerSum() { + return activeFundamentalPowerSum; + } + + public void setActiveFundamentalPowerSum(float activeFundamentalPowerSum) { + this.activeFundamentalPowerSum = activeFundamentalPowerSum; + } + + public float getActiveHarmonicPowerPhase1() { + return activeHarmonicPowerPhase1; + } + + public void setActiveHarmonicPowerPhase1(float activeHarmonicPowerPhase1) { + this.activeHarmonicPowerPhase1 = activeHarmonicPowerPhase1; + } + + public float getActiveHarmonicPowerPhase2() { + return activeHarmonicPowerPhase2; + } + + public void setActiveHarmonicPowerPhase2(float activeHarmonicPowerPhase2) { + this.activeHarmonicPowerPhase2 = activeHarmonicPowerPhase2; + } + + public float getActiveHarmonicPowerPhase3() { + return activeHarmonicPowerPhase3; + } + + public void setActiveHarmonicPowerPhase3(float activeHarmonicPowerPhase3) { + this.activeHarmonicPowerPhase3 = activeHarmonicPowerPhase3; + } + + public float getActiveHarmonicPowerSum() { + return activeHarmonicPowerSum; + } + + public void setActiveHarmonicPowerSum(float activeHarmonicPowerSum) { + this.activeHarmonicPowerSum = activeHarmonicPowerSum; + } + + public float getReactivePowerPhase1() { + return reactivePowerPhase1; + } + + public void setReactivePowerPhase1(float reactivePowerPhase1) { + this.reactivePowerPhase1 = reactivePowerPhase1; + } + + public float getReactivePowerPhase2() { + return reactivePowerPhase2; + } + + public void setReactivePowerPhase2(float reactivePowerPhase2) { + this.reactivePowerPhase2 = reactivePowerPhase2; + } + + public float getReactivePowerPhase3() { + return reactivePowerPhase3; + } + + public void setReactivePowerPhase3(float reactivePowerPhase3) { + this.reactivePowerPhase3 = reactivePowerPhase3; + } + + public float getReactivePowerSum() { + return reactivePowerSum; + } + + public void setReactivePowerSum(float reactivePowerSum) { + this.reactivePowerSum = reactivePowerSum; + } + + public float getApparentPowerPhase1() { + return apparentPowerPhase1; + } + + public void setApparentPowerPhase1(float apparentPowerPhase1) { + this.apparentPowerPhase1 = apparentPowerPhase1; + } + + public float getApparentPowerPhase2() { + return apparentPowerPhase2; + } + + public void setApparentPowerPhase2(float apparentPowerPhase2) { + this.apparentPowerPhase2 = apparentPowerPhase2; + } + + public float getApparentPowerPhase3() { + return apparentPowerPhase3; + } + + public void setApparentPowerPhase3(float apparentPowerPhase3) { + this.apparentPowerPhase3 = apparentPowerPhase3; + } + + public float getApparentPowerSum() { + return apparentPowerSum; + } + + public void setApparentPowerSum(float apparentPowerSum) { + this.apparentPowerSum = apparentPowerSum; + } + + public float getForwardActiveEnergyPhase1() { + return forwardActiveEnergyPhase1 / KILO; + } + + public void setForwardActiveEnergyPhase1(float forwardActiveEnergyPhase1) { + this.forwardActiveEnergyPhase1 = forwardActiveEnergyPhase1; + } + + public float getForwardActiveEnergyPhase2() { + return forwardActiveEnergyPhase2 / KILO; + } + + public void setForwardActiveEnergyPhase2(float forwardActiveEnergyPhase2) { + this.forwardActiveEnergyPhase2 = forwardActiveEnergyPhase2; + } + + public float getForwardActiveEnergyPhase3() { + return forwardActiveEnergyPhase3 / KILO; + } + + public void setForwardActiveEnergyPhase3(float forwardActiveEnergyPhase3) { + this.forwardActiveEnergyPhase3 = forwardActiveEnergyPhase3; + } + + public float getForwardActiveEnergySum() { + return forwardActiveEnergySum / KILO; + } + + public void setForwardActiveEnergySum(float forwardActiveEnergySum) { + this.forwardActiveEnergySum = forwardActiveEnergySum; + } + + public float getForwardActiveFundamentalEnergyPhase1() { + return forwardActiveFundamentalEnergyPhase1 / KILO; + } + + public void setForwardActiveFundamentalEnergyPhase1(float forwardActiveFundamentalEnergyPhase1) { + this.forwardActiveFundamentalEnergyPhase1 = forwardActiveFundamentalEnergyPhase1; + } + + public float getForwardActiveFundamentalEnergyPhase2() { + return forwardActiveFundamentalEnergyPhase2 / KILO; + } + + public void setForwardActiveFundamentalEnergyPhase2(float forwardActiveFundamentalEnergyPhase2) { + this.forwardActiveFundamentalEnergyPhase2 = forwardActiveFundamentalEnergyPhase2; + } + + public float getForwardActiveFundamentalEnergyPhase3() { + return forwardActiveFundamentalEnergyPhase3 / KILO; + } + + public void setForwardActiveFundamentalEnergyPhase3(float forwardActiveFundamentalEnergyPhase3) { + this.forwardActiveFundamentalEnergyPhase3 = forwardActiveFundamentalEnergyPhase3; + } + + public float getForwardActiveFundamentalEnergySum() { + return forwardActiveFundamentalEnergySum / KILO; + } + + public void setForwardActiveFundamentalEnergySum(float forwardActiveFundamentalEnergySum) { + this.forwardActiveFundamentalEnergySum = forwardActiveFundamentalEnergySum; + } + + public float getForwardActiveHarmonicEnergyPhase1() { + return forwardActiveHarmonicEnergyPhase1 / KILO; + } + + public void setForwardActiveHarmonicEnergyPhase1(float forwardActiveHarmonicEnergyPhase1) { + this.forwardActiveHarmonicEnergyPhase1 = forwardActiveHarmonicEnergyPhase1; + } + + public float getForwardActiveHarmonicEnergyPhase2() { + return forwardActiveHarmonicEnergyPhase2 / KILO; + } + + public void setForwardActiveHarmonicEnergyPhase2(float forwardActiveHarmonicEnergyPhase2) { + this.forwardActiveHarmonicEnergyPhase2 = forwardActiveHarmonicEnergyPhase2; + } + + public float getForwardActiveHarmonicEnergyPhase3() { + return forwardActiveHarmonicEnergyPhase3 / KILO; + } + + public void setForwardActiveHarmonicEnergyPhase3(float forwardActiveHarmonicEnergyPhase3) { + this.forwardActiveHarmonicEnergyPhase3 = forwardActiveHarmonicEnergyPhase3; + } + + public float getForwardActiveHarmonicEnergySum() { + return forwardActiveHarmonicEnergySum / KILO; + } + + public void setForwardActiveHarmonicEnergySum(float forwardActiveHarmonicEnergySum) { + this.forwardActiveHarmonicEnergySum = forwardActiveHarmonicEnergySum; + } + + public float getForwardReactiveEnergyPhase1() { + return forwardReactiveEnergyPhase1; + } + + public void setForwardReactiveEnergyPhase1(float forwardReactiveEnergyPhase1) { + this.forwardReactiveEnergyPhase1 = forwardReactiveEnergyPhase1; + } + + public float getForwardReactiveEnergyPhase2() { + return forwardReactiveEnergyPhase2; + } + + public void setForwardReactiveEnergyPhase2(float forwardReactiveEnergyPhase2) { + this.forwardReactiveEnergyPhase2 = forwardReactiveEnergyPhase2; + } + + public float getForwardReactiveEnergyPhase3() { + return forwardReactiveEnergyPhase3; + } + + public void setForwardReactiveEnergyPhase3(float forwardReactiveEnergyPhase3) { + this.forwardReactiveEnergyPhase3 = forwardReactiveEnergyPhase3; + } + + public float getForwardReactiveEnergySum() { + return forwardReactiveEnergySum; + } + + public void setForwardReactiveEnergySum(float forwardReactiveEnergySum) { + this.forwardReactiveEnergySum = forwardReactiveEnergySum; + } + + public float getReverseActiveEnergyPhase1() { + return reverseActiveEnergyPhase1 / KILO; + } + + public void setReverseActiveEnergyPhase1(float reverseActiveEnergyPhase1) { + this.reverseActiveEnergyPhase1 = reverseActiveEnergyPhase1; + } + + public float getReverseActiveEnergyPhase2() { + return reverseActiveEnergyPhase2 / KILO; + } + + public void setReverseActiveEnergyPhase2(float reverseActiveEnergyPhase2) { + this.reverseActiveEnergyPhase2 = reverseActiveEnergyPhase2; + } + + public float getReverseActiveEnergyPhase3() { + return reverseActiveEnergyPhase3 / KILO; + } + + public void setReverseActiveEnergyPhase3(float reverseActiveEnergyPhase3) { + this.reverseActiveEnergyPhase3 = reverseActiveEnergyPhase3; + } + + public float getReverseActiveEnergySum() { + return reverseActiveEnergySum / KILO; + } + + public void setReverseActiveEnergySum(float reverseActiveEnergySum) { + this.reverseActiveEnergySum = reverseActiveEnergySum; + } + + public float getReverseActiveFundamentalEnergyPhase1() { + return reverseActiveFundamentalEnergyPhase1 / KILO; + } + + public void setReverseActiveFundamentalEnergyPhase1(float reverseActiveFundamentalEnergyPhase1) { + this.reverseActiveFundamentalEnergyPhase1 = reverseActiveFundamentalEnergyPhase1; + } + + public float getReverseActiveFundamentalEnergyPhase2() { + return reverseActiveFundamentalEnergyPhase2 / KILO; + } + + public void setReverseActiveFundamentalEnergyPhase2(float reverseActiveFundamentalEnergyPhase2) { + this.reverseActiveFundamentalEnergyPhase2 = reverseActiveFundamentalEnergyPhase2; + } + + public float getReverseActiveFundamentalEnergyPhase3() { + return reverseActiveFundamentalEnergyPhase3 / KILO; + } + + public void setReverseActiveFundamentalEnergyPhase3(float reverseActiveFundamentalEnergyPhase3) { + this.reverseActiveFundamentalEnergyPhase3 = reverseActiveFundamentalEnergyPhase3; + } + + public float getReverseActiveFundamentalEnergySum() { + return reverseActiveFundamentalEnergySum / KILO; + } + + public void setReverseActiveFundamentalEnergySum(float reverseActiveFundamentalEnergySum) { + this.reverseActiveFundamentalEnergySum = reverseActiveFundamentalEnergySum; + } + + public float getReverseActiveHarmonicEnergyPhase1() { + return reverseActiveHarmonicEnergyPhase1 / KILO; + } + + public void setReverseActiveHarmonicEnergyPhase1(float reverseActiveHarmonicEnergyPhase1) { + this.reverseActiveHarmonicEnergyPhase1 = reverseActiveHarmonicEnergyPhase1; + } + + public float getReverseActiveHarmonicEnergyPhase2() { + return reverseActiveHarmonicEnergyPhase2 / KILO; + } + + public void setReverseActiveHarmonicEnergyPhase2(float reverseActiveHarmonicEnergyPhase2) { + this.reverseActiveHarmonicEnergyPhase2 = reverseActiveHarmonicEnergyPhase2; + } + + public float getReverseActiveHarmonicEnergyPhase3() { + return reverseActiveHarmonicEnergyPhase3 / KILO; + } + + public void setReverseActiveHarmonicEnergyPhase3(float reverseActiveHarmonicEnergyPhase3) { + this.reverseActiveHarmonicEnergyPhase3 = reverseActiveHarmonicEnergyPhase3; + } + + public float getReverseActiveHarmonicEnergySum() { + return reverseActiveHarmonicEnergySum / KILO; + } + + public void setReverseActiveHarmonicEnergySum(float reverseActiveHarmonicEnergySum) { + this.reverseActiveHarmonicEnergySum = reverseActiveHarmonicEnergySum; + } + + public float getReverseReactiveEnergyPhase1() { + return reverseReactiveEnergyPhase1; + } + + public void setReverseReactiveEnergyPhase1(float reverseReactiveEnergyPhase1) { + this.reverseReactiveEnergyPhase1 = reverseReactiveEnergyPhase1; + } + + public float getReverseReactiveEnergyPhase2() { + return reverseReactiveEnergyPhase2; + } + + public void setReverseReactiveEnergyPhase2(float reverseReactiveEnergyPhase2) { + this.reverseReactiveEnergyPhase2 = reverseReactiveEnergyPhase2; + } + + public float getReverseReactiveEnergyPhase3() { + return reverseReactiveEnergyPhase3; + } + + public void setReverseReactiveEnergyPhase3(float reverseReactiveEnergyPhase3) { + this.reverseReactiveEnergyPhase3 = reverseReactiveEnergyPhase3; + } + + public float getReverseReactiveEnergySum() { + return reverseReactiveEnergySum; + } + + public void setReverseReactiveEnergySum(float reverseReactiveEnergySum) { + this.reverseReactiveEnergySum = reverseReactiveEnergySum; + } + + public float getApparentEnergyConsumptionPhase1() { + return apparentEnergyConsumptionPhase1; + } + + public void setApparentEnergyConsumptionPhase1(float apparentEnergyConsumptionPhase1) { + this.apparentEnergyConsumptionPhase1 = apparentEnergyConsumptionPhase1; + } + + public float getApparentEnergyConsumptionPhase2() { + return apparentEnergyConsumptionPhase2; + } + + public void setApparentEnergyConsumptionPhase2(float apparentEnergyConsumptionPhase2) { + this.apparentEnergyConsumptionPhase2 = apparentEnergyConsumptionPhase2; + } + + public float getApparentEnergyConsumptionPhase3() { + return apparentEnergyConsumptionPhase3; + } + + public void setApparentEnergyConsumptionPhase3(float apparentEnergyConsumptionPhase3) { + this.apparentEnergyConsumptionPhase3 = apparentEnergyConsumptionPhase3; + } + + public float getApparentEnergyConsumptionSum() { + return apparentEnergyConsumptionSum; + } + + public void setApparentEnergyConsumptionSum(float apparentEnergyConsumptionSum) { + this.apparentEnergyConsumptionSum = apparentEnergyConsumptionSum; + } +} diff --git a/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..b55da22752445 --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + mecMeter Binding + This is the binding for the din-rail power meter from MEC. + + diff --git a/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..064ee8a5a0a4e --- /dev/null +++ b/bundles/org.openhab.binding.mecmeter/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,561 @@ + + + + + + Power Meter from MEC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter the password + password + + + + The IP address of the mecMeter + network-address + + + + 5 + Refresh interval in seconds, default 5 seconds, range 1 to 300 seconds + + + + + + + + General Channels + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number:Frequency + + Frequency in Hertz + + + + Number:Temperature + + Internal Temperature of the energy meter + + + + Number:Time + + Time in Operation + + + + + + Number:ElectricPotential + + Average N – Phase Voltage in Volt + + + + Number:ElectricPotential + + Voltage in Volt + + + + Number:ElectricPotential + + Average Phase – Phase Voltage in Volt + + + + + + Number:ElectricCurrent + + Current in Ampere + + + + + + Number:Angle + + Angle Current to Voltage in Degree + + + + + Number:Angle + + Angle Voltage to Voltage in Degree + + + + + + Number:Power + + Active power consumed + + + + + Number:Power + + Active fundamental power + + + + + Number:Power + + Active harmonic power + + + + + Number:Power + + Reactive power consumed + + + + + Number:Power + + Apparent power consumed + + + + + + Number:Energy + + Forward Active Energy in kWh + Energy + + + + Number:Energy + + Forward Active Energy in kWh + Energy + + + + Number:Energy + + Forward Active Energy all phase in kWh + Energy + + + + + Number:Energy + + Forward Active Fundamental Energy in kWh + Energy + + + + + Number:Energy + + Forward Active Harmonic Energy in kWh + Energy + + + + + Number:Energy + + Forward Reactive Energy in VArh + Energy + + + + + + Number:Energy + + Reverse Active Energy in kWh + Energy + + + + Number:Energy + + Reverse Active Energy in kWh + Energy + + + + + Number:Energy + + Reverse Active Fundamental Energy in kWh + Energy + + + + + Number:Energy + + Reverse Active Harmonic Energy in kWh + Energy + + + + + Number:Energy + + Reverse Reactive Energy in VArh + Energy + + + + + + Number:Energy + + Apparent Energy Consumption in VArh + + + + + + Number:Dimensionless + + Power Factor + + + diff --git a/bundles/org.openhab.binding.melcloud/src/main/java/org/openhab/binding/melcloud/internal/discovery/MelCloudDiscoveryService.java b/bundles/org.openhab.binding.melcloud/src/main/java/org/openhab/binding/melcloud/internal/discovery/MelCloudDiscoveryService.java index 4d090ea269203..975d2b449e7ef 100644 --- a/bundles/org.openhab.binding.melcloud/src/main/java/org/openhab/binding/melcloud/internal/discovery/MelCloudDiscoveryService.java +++ b/bundles/org.openhab.binding.melcloud/src/main/java/org/openhab/binding/melcloud/internal/discovery/MelCloudDiscoveryService.java @@ -29,6 +29,7 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandler; @@ -46,8 +47,10 @@ */ public class MelCloudDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(MelCloudDiscoveryService.class); + private static final String PROPERTY_DEVICE_ID = "deviceID"; private static final int DISCOVER_TIMEOUT_SECONDS = 10; private MelCloudAccountHandler melCloudHandler; @@ -126,9 +129,9 @@ private void discoverDevices() { device.getDeviceID().toString()); Map deviceProperties = new HashMap<>(); - deviceProperties.put("deviceID", device.getDeviceID().toString()); - deviceProperties.put("serialNumber", device.getSerialNumber().toString()); - deviceProperties.put("macAddress", device.getMacAddress().toString()); + deviceProperties.put(PROPERTY_DEVICE_ID, device.getDeviceID().toString()); + deviceProperties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber().toString()); + deviceProperties.put(Thing.PROPERTY_MAC_ADDRESS, device.getMacAddress().toString()); deviceProperties.put("deviceName", device.getDeviceName().toString()); deviceProperties.put("buildingID", device.getBuildingID().toString()); @@ -136,9 +139,8 @@ private void discoverDevices() { logger.debug("Found device: {} : {}", label, deviceProperties); thingDiscovered(DiscoveryResultBuilder.create(deviceThing).withLabel(label) - .withProperties(deviceProperties) - .withRepresentationProperty(device.getDeviceID().toString()).withBridge(bridgeUID) - .build()); + .withProperties(deviceProperties).withRepresentationProperty(PROPERTY_DEVICE_ID) + .withBridge(bridgeUID).build()); }); } } catch (MelCloudLoginException e) { diff --git a/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/acDevice.xml b/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/acDevice.xml index 18c1b34153f42..5779708c504ea 100644 --- a/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/acDevice.xml +++ b/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/acDevice.xml @@ -25,6 +25,8 @@ + deviceID + diff --git a/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/heatpumpDevice.xml b/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/heatpumpDevice.xml index aa0d2faf7c96a..3b58cec5d946a 100644 --- a/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/heatpumpDevice.xml +++ b/bundles/org.openhab.binding.melcloud/src/main/resources/OH-INF/thing/heatpumpDevice.xml @@ -23,6 +23,8 @@ + deviceID + diff --git a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/MeteoBlueConfiguration.java b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/MeteoBlueConfiguration.java index dbfe3d76433ee..212525809b806 100644 --- a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/MeteoBlueConfiguration.java +++ b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/MeteoBlueConfiguration.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.meteoblue.internal; -import org.apache.commons.lang.StringUtils; - /** * Model for the meteoblue binding configuration. * @@ -64,15 +62,15 @@ public void parseLocation() { String a2 = split.length > 1 ? split[1] : null; String a3 = split.length > 2 ? split[2] : null; - if (!StringUtils.isBlank(a1)) { + if (a1 != null && !a1.isBlank()) { latitude = tryGetDouble(a1); } - if (!StringUtils.isBlank(a2)) { + if (a2 != null && !a2.isBlank()) { longitude = tryGetDouble(a2); } - if (!StringUtils.isBlank(a3)) { + if (a3 != null && !a3.isBlank()) { altitude = tryGetDouble(a3); } } diff --git a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueBridgeHandler.java b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueBridgeHandler.java index beae447647649..c74cbbb19b5b8 100644 --- a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueBridgeHandler.java +++ b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueBridgeHandler.java @@ -17,7 +17,6 @@ import java.util.Collections; import java.util.Set; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.meteoblue.internal.MeteoBlueBridgeConfig; import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.thing.Bridge; @@ -55,7 +54,7 @@ public void initialize() { MeteoBlueBridgeConfig config = getConfigAs(MeteoBlueBridgeConfig.class); String apiKeyTemp = config.getApiKey(); - if (StringUtils.isBlank(apiKeyTemp)) { + if (apiKeyTemp == null || apiKeyTemp.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot initialize meteoblue bridge. No apiKey provided."); return; diff --git a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java index c35212c19cb11..eb62fa856b436 100644 --- a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java +++ b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java @@ -27,7 +27,6 @@ import javax.imageio.ImageIO; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.meteoblue.internal.Forecast; import org.openhab.binding.meteoblue.internal.MeteoBlueConfiguration; import org.openhab.binding.meteoblue.internal.json.JsonData; @@ -97,13 +96,13 @@ public void initialize() { MeteoBlueConfiguration config = getConfigAs(MeteoBlueConfiguration.class); - if (StringUtils.isBlank(config.serviceType)) { + if (config.serviceType == null || config.serviceType.isBlank()) { config.serviceType = MeteoBlueConfiguration.SERVICETYPE_NONCOMM; logger.debug("Using default service type ({}).", config.serviceType); return; } - if (StringUtils.isBlank(config.location)) { + if (config.location == null || config.location.isBlank()) { flagBadConfig("The location was not configured."); return; } @@ -315,7 +314,7 @@ private boolean updateWeatherData() { if (config.altitude != null) { builder.append("&asl=" + config.altitude); } - if (StringUtils.isNotBlank(config.timeZone)) { + if (config.timeZone != null && !config.timeZone.isBlank()) { builder.append("&tz=" + config.timeZone); } url = url.replace("#FORMAT_PARAMS#", builder.toString()); diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/discovery/MieleApplianceDiscoveryService.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/discovery/MieleApplianceDiscoveryService.java index a82acc26b2ed2..b85ce8e11763d 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/discovery/MieleApplianceDiscoveryService.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/discovery/MieleApplianceDiscoveryService.java @@ -20,7 +20,6 @@ import java.util.Map; import java.util.Set; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.miele.internal.handler.ApplianceStatusListener; import org.openhab.binding.miele.internal.handler.MieleApplianceHandler; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler; @@ -46,6 +45,9 @@ */ public class MieleApplianceDiscoveryService extends AbstractDiscoveryService implements ApplianceStatusListener { + private static final String MIELE_APPLIANCE_CLASS = "com.miele.xgw3000.gateway.hdm.deviceclasses.MieleAppliance"; + private static final String MIELE_CLASS = "com.miele.xgw3000.gateway.hdm.deviceclasses.Miele"; + private final Logger logger = LoggerFactory.getLogger(MieleApplianceDiscoveryService.class); private static final int SEARCH_TIME = 60; @@ -103,10 +105,9 @@ private void onApplianceAddedInternal(HomeDevice appliance) { properties.put(APPLIANCE_ID, appliance.getApplianceId()); for (JsonElement dc : appliance.DeviceClasses) { - if (dc.getAsString().contains("com.miele.xgw3000.gateway.hdm.deviceclasses.Miele") - && !dc.getAsString().equals("com.miele.xgw3000.gateway.hdm.deviceclasses.MieleAppliance")) { - properties.put(DEVICE_CLASS, StringUtils.right(dc.getAsString(), dc.getAsString().length() - - new String("com.miele.xgw3000.gateway.hdm.deviceclasses.Miele").length())); + String dcStr = dc.getAsString(); + if (dcStr.contains(MIELE_CLASS) && !dcStr.equals(MIELE_APPLIANCE_CLASS)) { + properties.put(DEVICE_CLASS, dcStr.substring(MIELE_CLASS.length())); break; } } @@ -145,17 +146,16 @@ private ThingUID getThingUID(HomeDevice appliance) { String modelID = null; for (JsonElement dc : appliance.DeviceClasses) { - if (dc.getAsString().contains("com.miele.xgw3000.gateway.hdm.deviceclasses.Miele") - && !dc.getAsString().equals("com.miele.xgw3000.gateway.hdm.deviceclasses.MieleAppliance")) { - modelID = StringUtils.right(dc.getAsString(), dc.getAsString().length() - - new String("com.miele.xgw3000.gateway.hdm.deviceclasses.Miele").length()); + String dcStr = dc.getAsString(); + if (dcStr.contains(MIELE_CLASS) && !dcStr.equals(MIELE_APPLIANCE_CLASS)) { + modelID = dcStr.substring(MIELE_CLASS.length()); break; } } if (modelID != null) { ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, - StringUtils.lowerCase(modelID.replaceAll("[^a-zA-Z0-9_]", "_"))); + modelID.replaceAll("[^a-zA-Z0-9_]", "_").toLowerCase()); if (getSupportedThingTypes().contains(thingTypeUID)) { ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, appliance.getId()); diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java index 8f0db9dd3c706..b3ba33c1969cf 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java @@ -20,7 +20,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceOperation; @@ -144,7 +144,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void onApplianceStateChanged(String UID, DeviceClassObject dco) { - String myUID = ((String) getThing().getProperties().get(PROTOCOL_PROPERTY_NAME)) + String myUID = (getThing().getProperties().get(PROTOCOL_PROPERTY_NAME)) + (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID); String modelID = StringUtils.right(dco.DeviceClass, dco.DeviceClass.length() - new String("com.miele.xgw3000.gateway.hdm.deviceclasses.Miele").length()); @@ -177,7 +177,7 @@ public void onApplianceStateChanged(String UID, DeviceClassObject dco) { @Override public void onAppliancePropertyChanged(String UID, DeviceProperty dp) { - String myUID = ((String) getThing().getProperties().get(PROTOCOL_PROPERTY_NAME)) + String myUID = (getThing().getProperties().get(PROTOCOL_PROPERTY_NAME)) + (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID); if (myUID.equals(UID)) { @@ -186,8 +186,7 @@ public void onAppliancePropertyChanged(String UID, DeviceProperty dp) { if (dp.Metadata == null) { String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim()); if (metadata != null) { - JsonParser parser = new JsonParser(); - JsonObject jsonMetaData = (JsonObject) parser.parse(metadata); + JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata); dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class); // only keep the enum, if any - that's all we care for events we receive via multicast // all other fields are nulled @@ -199,8 +198,7 @@ public void onAppliancePropertyChanged(String UID, DeviceProperty dp) { } if (dp.Metadata != null) { String metadata = StringUtils.replace(dp.Metadata.toString(), "enum", "MieleEnum"); - JsonParser parser = new JsonParser(); - JsonObject jsonMetaData = (JsonObject) parser.parse(metadata); + JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata); dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class); metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata); } diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleBridgeHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleBridgeHandler.java index 7306393008dbe..b02df0947f593 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleBridgeHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleBridgeHandler.java @@ -44,7 +44,7 @@ import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -480,8 +480,7 @@ protected JsonElement invokeRPC(String methodName, Object[] args) { if (responseData != null) { logger.debug("The request '{}' yields '{}'", requestData, responseData); - JsonParser parser = new JsonParser(); - JsonObject resp = (JsonObject) parser.parse(new StringReader(responseData)); + JsonObject resp = (JsonObject) JsonParser.parseReader(new StringReader(responseData)); result = resp.get("result"); JsonElement error = resp.get("error"); diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java index 810a317cf1c97..7f1e43fee6478 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java @@ -18,7 +18,7 @@ import java.util.Map.Entry; import java.util.TimeZone; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/coffeemachine.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/coffeemachine.xml index d4d6f8588f028..8a34bfc992d19 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/coffeemachine.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/coffeemachine.xml @@ -23,10 +23,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml index 5aae629917f2c..902fbaced0548 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml @@ -26,10 +26,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridge.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridge.xml index 0e76fa038fc95..c68c643376971 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridge.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridge.xml @@ -23,10 +23,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridgefreezer.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridgefreezer.xml index 0982b2581a42c..585ad21d9c4e2 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridgefreezer.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/fridgefreezer.xml @@ -28,10 +28,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hob.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hob.xml index c091f9ad6c0b3..71e6ab990a580 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hob.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hob.xml @@ -36,10 +36,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hood.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hood.xml index 668c81ef11a2a..aedd3eade74ae 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hood.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/hood.xml @@ -21,10 +21,9 @@ - + The identifier identifies the appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml index 0e42274429a32..4090178d656ac 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml @@ -32,10 +32,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml index 22c81484ff98c..24035a4211b48 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml @@ -28,10 +28,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml index 3330fd36ef4b5..2a41f546dc636 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml @@ -29,10 +29,9 @@ - + The identifier identifies one certain appliance on the ZigBee network. - true diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/xgw3000.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/xgw3000.xml index 36cb38e137c65..184c3daaccec2 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/xgw3000.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/xgw3000.xml @@ -14,31 +14,27 @@ - + network-address Network address of the Miele@home gateway. - true - + network-address Network address of openHAB host interface where the binding will listen for multicast events coming from the Miele@home gateway - true - + Name of a registered Miele@home user. - false - + password Password for the registered Miele@home - false diff --git a/bundles/org.openhab.binding.mihome/README.md b/bundles/org.openhab.binding.mihome/README.md index 4ae3fd2e4ddc4..4e0a574d8fec9 100644 --- a/bundles/org.openhab.binding.mihome/README.md +++ b/bundles/org.openhab.binding.mihome/README.md @@ -8,7 +8,7 @@ The sensors run on a coin cell battery for over a year. After setup, you can disconnect the gateway from the internet to keep your sensor information private. -Please note that using the Xiaomi gateway with OpenHAB requires enabling the developer mode and that multiple user reports suggest that it is no longer posible. +Please note that using the Xiaomi gateway with openHAB requires enabling the developer mode and that multiple user reports suggest that it is no longer possible. Zigbee2Mqtt provides an alternative method to integrate Xiaomi devices. ## Supported devices @@ -102,7 +102,7 @@ __Hints:__ ## Removing devices from the gateway -If you remove a Thing in PapaerUI it will also trigger the gateway to unpair the device. +If you remove a Thing in PaperUI it will also trigger the gateway to unpair the device. It will only reappear in your Inbox, if you connect it to the gateway again. Just follow the instructions in ["Connecting devices to the gateway"](#connecting-devices-to-the-gateway). @@ -110,13 +110,17 @@ Just follow the instructions in ["Connecting devices to the gateway"](#connectin - The binding requires port `9898` to not be used by any other service on the system. - Make sure multicast traffic is correctly routed between the gateway and your openHAB instance +- To correctly receive multicast traffic, when your openHAB machine is using multiple network interfaces, you might need to configure the optional `interface` property on the `Bridge` Thing, like so: +``` +Bridge mihome:bridge:f0b429XXXXXX "Xiaomi Gateway" [ ..., interface="eth0", ... ] { +``` ## Configuration examples ### xiaomi.things: ``` -Bridge mihome:bridge:f0b429XXXXXX "Xiaomi Gateway" [ serialNumber="f0b429XXXXXX", ipAddress="192.168.0.3", port=9898, key="XXXXXXXXXXXXXXXX", pollingInterval=6000 ] { +Bridge mihome:bridge:f0b429XXXXXX "Xiaomi Gateway" [ serialNumber="f0b429XXXXXX", ipAddress="192.168.0.3", port=9898, key="XXXXXXXXXXXXXXXX" ] { Things: gateway f0b429XXXXXX "Xiaomi Mi Smart Home Gateway" [itemId="f0b429XXXXXX"] sensor_ht 158d0001XXXXXX "Xiaomi Temperature Sensor" [itemId="158d0001XXXXXX"] @@ -452,10 +456,10 @@ Make sure you have connected your gateway to openHAB and the communication is wo The devices send different types of messages to the gateway. You have to capture as many of them as possible, so that the device is fully supported in the end. -1. Heartbeat (transmitted usually every 60 minutes) +1. Heartbeat (usually transmitted every 60 minutes) 2. Report (device reports new sensor or status values) 3. Read ACK (binding refreshes all sensor values after a restart of openHAB) -4. Write ACK (device has received a command) __not avaiable for sensor-only devices__ +4. Write ACK (device has received a command) __not available for sensor-only devices__ ### Open a new issue or get your hands dirty @@ -465,7 +469,7 @@ Post an issue in the GitHub repository with as much information as possible abou - model name - content of all the different message types -Or implement the support by youself and submit a pull request. +Or implement the support by yourself and submit a pull request. ### Handle the message contents of a basic device thing with items @@ -484,7 +488,7 @@ _Example for the same message from the heartbeat channel - only the data is retu These messages are in JSON format, which also gives you the ability to parse single values. -_Example for the retrieved IP from the heartbeat message and transformed with JSONPATH transfomration: ```String Gateway_IP {channel="mihome:basic:xxx:heartbeatMessage"[profile="transform:JSONPATH", function="$.ip"]}```_ +_Example for the retrieved IP from the heartbeat message and transformed with JSONPATH transformation: ```String Gateway_IP {channel="mihome:basic:xxx:heartbeatMessage"[profile="transform:JSONPATH", function="$.ip"]}```_ The item will get the value `192.168.0.124`. @@ -545,7 +549,18 @@ In case you want to check if the communication between the machine and the gatew - make sure you have __netcat__ installed - Enter ```netcat -ukl 9898``` - At least every 10 seconds you should see a message coming in from the gateway which looks like - ```{"cmd":"heartbeat","model":"gateway","sid":"`xxx","short_id":"0","token":"xxx","data":"{\"ip\":\"`xxx\"}"}``` +```{"cmd":"heartbeat","model":"gateway","sid":"`xxx","short_id":"0","token":"xxx","data":"{\"ip\":\"`xxx\"}"}``` + +#### Multiple network interfaces + +When the computer running openHAB has more than one network interface configured (typically, a VLAN for your segregated IoT devices, and the other for your regular traffic like internet, openHAB panel access, etc), it could be that openHAB will attempt to listen for Multicast traffic of the Gateway on the wrong network interface. That will prevent openHAB and `netcat` from receiving the messages from the Xiaomi Gateway. Within openHAB this manifests by seeing the Gateway and its devices online for a brief period after openHAB startup, after which they timeout and are shown Offline. No channel triggers from the Gateway work in this case. + +In order to verify that traffic is actually received by the machine use `tcpdump` on each interface: +- List your network interfaces `ifconfig | grep MULTICAST` or `ip link | grep MULTICAST` +- Use `tcpdump -i port 9898` for each interface to verify if you receive traffic + +If you already know the correct interface, or you found the correct one through tcpdump: +- Configure the `interface` property of the `Bridge` Thing with the correct name (for example `eth0`, etc) ### Check if your Windows/Mac machine receives multicast traffic @@ -562,6 +577,7 @@ __My gateway shows up in openHAB and I have added all devices, but I don't get a - Make sure the gateway and the machine are in the same subnet - Try to connect your machine via Ethernet instead of Wifi - Make sure you don't have any firewall rules blocking multicast + - If you have multiple network interfaces, try to configure the `interface` property of the `Bridge` Thing __I have connected my gateway to the network but it doesn't show up in openHAB:__ - Make sure to have the developer mode enabled in the MiHome app diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/XiaomiGatewayBindingConstants.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/XiaomiGatewayBindingConstants.java index bed8f71e45143..bf944e3b406fe 100644 --- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/XiaomiGatewayBindingConstants.java +++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/XiaomiGatewayBindingConstants.java @@ -123,6 +123,7 @@ public class XiaomiGatewayBindingConstants { public static final String SERIAL_NUMBER = "serialNumber"; public static final String HOST = "ipAddress"; public static final String PORT = "port"; + public static final String INTERFACE = "interface"; public static final String TOKEN = "token"; // Item config properties diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiBridgeHandler.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiBridgeHandler.java index 2977214685a10..d84956104cc67 100644 --- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiBridgeHandler.java +++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiBridgeHandler.java @@ -67,7 +67,6 @@ public class XiaomiBridgeHandler extends ConfigStatusBridgeHandler implements Xi private static final String NO = "no"; public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE); - private static final JsonParser PARSER = new JsonParser(); private static final EncryptionHelper CRYPTER = new EncryptionHelper(); private static Map retentionInbox = new ConcurrentHashMap<>(); @@ -131,7 +130,7 @@ public void initialize() { return; } logger.debug("Init socket on Port: {}", port); - socket = new XiaomiBridgeSocket(port, getThing().getUID().getId()); + socket = new XiaomiBridgeSocket(port, (String) config.get(INTERFACE), getThing().getUID().getId()); socket.initialize(); socket.registerListener(this); @@ -175,7 +174,7 @@ public void onDataReceived(JsonObject message) { } break; case "get_id_list_ack": - JsonArray devices = PARSER.parse(message.get("data").getAsString()).getAsJsonArray(); + JsonArray devices = JsonParser.parseString(message.get("data").getAsString()).getAsJsonArray(); for (JsonElement deviceId : devices) { String device = deviceId.getAsString(); sendCommandToBridge("read", device); diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiDeviceBaseHandler.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiDeviceBaseHandler.java index a71935fb9117d..43ad72b4720fa 100644 --- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiDeviceBaseHandler.java +++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/handler/XiaomiDeviceBaseHandler.java @@ -81,8 +81,6 @@ public class XiaomiDeviceBaseHandler extends BaseThingHandler implements XiaomiI private static final long ONLINE_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(2); private ScheduledFuture onlineCheckTask; - private JsonParser parser = new JsonParser(); - private XiaomiBridgeHandler bridgeHandler; private String itemId; @@ -148,7 +146,7 @@ public void onItemUpdate(String sid, String command, JsonObject message) { } logger.debug("Item got update: {}", message); try { - JsonObject data = parser.parse(message.get("data").getAsString()).getAsJsonObject(); + JsonObject data = JsonParser.parseString(message.get("data").getAsString()).getAsJsonObject(); parseCommand(command, data); if (THING_TYPE_BASIC.equals(getThing().getThingTypeUID())) { parseDefault(message); diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiBridgeSocket.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiBridgeSocket.java index ba0ae12ed0823..e5d4f9f60cd42 100644 --- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiBridgeSocket.java +++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiBridgeSocket.java @@ -15,8 +15,10 @@ import java.io.IOException; import java.net.InetAddress; import java.net.MulticastSocket; +import java.net.NetworkInterface; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,9 +32,11 @@ public class XiaomiBridgeSocket extends XiaomiSocket { private final Logger logger = LoggerFactory.getLogger(XiaomiBridgeSocket.class); + private @Nullable final String netIf; - public XiaomiBridgeSocket(int port, String owner) { + public XiaomiBridgeSocket(int port, String netIf, String owner) { super(port, owner); + this.netIf = netIf; } /** @@ -51,10 +55,16 @@ protected synchronized void setupSocket() { try { logger.debug("Setup socket"); socket = new MulticastSocket(getPort()); + + if (netIf != null) { + socket.setNetworkInterface(NetworkInterface.getByName(netIf)); + } + setSocket(socket); // must bind receive side socket.joinGroup(InetAddress.getByName(MCAST_ADDR)); - logger.debug("Initialized socket to {}:{} on {}:{}", socket.getRemoteSocketAddress(), socket.getPort(), - socket.getLocalAddress(), socket.getLocalPort()); + logger.debug("Initialized socket to {}:{} on {}:{} bound to {} network interface", + socket.getRemoteSocketAddress(), socket.getPort(), socket.getLocalAddress(), socket.getLocalPort(), + socket.getNetworkInterface()); } catch (IOException e) { logger.error("Setup socket error", e); } diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java index fc80a269b1582..4e1ce434c77c7 100644 --- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java +++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java @@ -44,7 +44,6 @@ public abstract class XiaomiSocket { static final String MCAST_ADDR = "224.0.0.50"; private static final int BUFFER_LENGTH = 1024; - private static final JsonParser PARSER = new JsonParser(); private final Logger logger = LoggerFactory.getLogger(XiaomiSocket.class); @@ -197,7 +196,7 @@ private void receiveData() { logger.debug("Received Datagram from {}:{} on port {}", address.getHostAddress(), datagramPacket.getPort(), localPort); String sentence = new String(datagramPacket.getData(), 0, datagramPacket.getLength()); - JsonObject message = PARSER.parse(sentence).getAsJsonObject(); + JsonObject message = JsonParser.parseString(sentence).getAsJsonObject(); notifyListeners(message, address); logger.trace("Data received and notified {} listeners", listeners.size()); } diff --git a/bundles/org.openhab.binding.mihome/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.mihome/src/main/resources/OH-INF/config/config.xml index e20556b87b3de..f34eb55da96eb 100644 --- a/bundles/org.openhab.binding.mihome/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.mihome/src/main/resources/OH-INF/config/config.xml @@ -5,10 +5,9 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + The identifier of this MiHome device - true true @@ -20,26 +19,29 @@ true - + network-address Network address of this Xiaomi bridge - true - + network-address Port of the MiHome communication channel - true 9898 true - + + + Interface to bind to for the MiHome communication channel + true + + + Developer key extracted from Xiaomi's app - false diff --git a/bundles/org.openhab.binding.miio/README.base.md b/bundles/org.openhab.binding.miio/README.base.md index 88306fd08807c..8223a88c09f2a 100644 --- a/bundles/org.openhab.binding.miio/README.base.md +++ b/bundles/org.openhab.binding.miio/README.base.md @@ -245,8 +245,20 @@ Image map "Cleaning Map" (gVacLast) {channel="miio:vacuum:034F0E45:cleaning#map" Note: cleaning map is only available with cloud access. -Additionally depending on the capabilities of your robot vacuum other channels may be enabled at runtime +There are several advanced channels, which may be useful in rules (e.g. for individual room cleaning etc) +In case your vacuum does not support one of these commands, it will show "unsupported_method" for string channels or no value for numeric channels. +| Type | Channel | Description | +|---------|-----------------------------------|----------------------------| +| Number | status#segment_status | Segment Status | +| Number | status#map_status | Map Box Status | +| Number | status#led_status | Led Box Status | +| String | info#carpet_mode | Carpet Mode details | +| String | info#fw_features | Firmware Features | +| String | info#room_mapping | Room Mapping details | +| String | info#multi_maps_list | Maps Listing details | + +Additionally depending on the capabilities of your robot vacuum other channels may be enabled at runtime | Type | Channel | Description | |---------|-----------------------------------|----------------------------| diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index 44218acc5ebb2..b280c4433323c 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -173,11 +173,12 @@ This will change the communication method and the Mi IO binding can communicate # Mi IO Devices -Currently the miio binding supports more than 260 different models. +Currently the miio binding supports more than 280 different models. | Device | ThingType | Device Model | Supported | Remark | |------------------------------|------------------|------------------------|-----------|------------| | AUX Smart Air Conditioner | miio:unsupported | aux.aircondition.v1 | No | | +| Qingping Air Monitor Lite | miio:basic | [cgllc.airm.cgdn1](#cgllc-airm-cgdn1) | Yes | Identified manual actions for execution
`action{"did":"settings-set-start-time","siid":9,"aiid":2,"in":[2.0]}`
`action{"did":"settings-set-end-time","siid":9,"aiid":3,"in":[3.0]}`
`action{"did":"settings-set-frequency","siid":9,"aiid":4,"in":[4.0]}`
`action{"did":"settings-set-screen-off","siid":9,"aiid":5,"in":[5.0]}`
`action{"did":"settings-set-device-off","siid":9,"aiid":6,"in":[6.0]}`
`action{"did":"settings-set-temp-unit","siid":9,"aiid":7,"in":[7.0]}`
Please test and feedback if they are working to they can be linked to a channel. | | Mi Multifunction Air Monitor | miio:basic | [cgllc.airmonitor.b1](#cgllc-airmonitor-b1) | Yes | | | Qingping Air Monitor | miio:basic | [cgllc.airmonitor.s1](#cgllc-airmonitor-s1) | Yes | | | Mi Universal Remote | miio:unsupported | chuangmi.ir.v2 | No | | @@ -197,6 +198,8 @@ Currently the miio binding supports more than 260 different models. | Mi IH Pressure Rice Cooker | miio:unsupported | chunmi.cooker.press1 | No | | | Mi IH Pressure Rice Cooker | miio:unsupported | chunmi.cooker.press2 | No | | | Gosund Smart Plug | miio:basic | [cuco.plug.cp1](#cuco-plug-cp1) | Yes | | +| Mi Smart Antibacterial Humidifier | miio:basic | [deerma.humidifier.jsq](#deerma-humidifier-jsq) | Yes | | +| Mi S Smart humidifer | miio:basic | [deerma.humidifier.jsq1](#deerma-humidifier-jsq1) | Yes | | | Mi Smart Humidifier | miio:basic | [deerma.humidifier.mjjsq](#deerma-humidifier-mjjsq) | Yes | | | Mi Fresh Air Ventilator A1-150 | miio:basic | [dmaker.airfresh.a1](#dmaker-airfresh-a1) | Yes | | | Mi Fresh Air Ventilator | miio:basic | [dmaker.airfresh.t2017](#dmaker-airfresh-t2017) | Yes | | @@ -208,6 +211,10 @@ Currently the miio binding supports more than 260 different models. | Mi Robot Vacuum Mop 1C STYTJ01ZHM | miio:basic | [dreame.vacuum.mc1808](#dreame-vacuum-mc1808) | Yes | Identified manual actions for execution
`action{"did":"battery-start-charge","siid":2,"aiid":1,"in":[]}`
`action{"did":"vacuum-start-sweep","siid":3,"aiid":1,"in":[]}`
`action{"did":"vacuum-stop-sweeping","siid":3,"aiid":2,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":26,"aiid":1,"in":[]}`
`action{"did":"filter-reset-filter-life","siid":27,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":28,"aiid":1,"in":[]}`
`action{"did":"clean-start-clean","siid":18,"aiid":1,"in":[]}`
`action{"did":"clean-stop-clean","siid":18,"aiid":2,"in":[]}`
`action{"did":"remote-start-remote","siid":21,"aiid":1,"in":[1.0, 2.0]}`
`action{"did":"remote-stop-remote","siid":21,"aiid":2,"in":[]}`
`action{"did":"remote-exit-remote","siid":21,"aiid":3,"in":[]}`
`action{"did":"map-map-req","siid":23,"aiid":1,"in":[2.0]}`
`action{"did":"audio-position","siid":24,"aiid":1,"in":[]}`
`action{"did":"audio-set-voice","siid":24,"aiid":2,"in":[]}`
`action{"did":"audio-play-sound","siid":24,"aiid":3,"in":[]}`
Please test and feedback if they are working to they can be linked to a channel.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Dreame Robot Vacuum-Mop F9 | miio:basic | [dreame.vacuum.p2008](#dreame-vacuum-p2008) | Yes | Identified manual actions for execution
`action{"did":"vacuum-start-sweep","siid":2,"aiid":1,"in":[]}`
`action{"did":"vacuum-stop-sweeping","siid":2,"aiid":2,"in":[]}`
`action{"did":"battery-start-charge","siid":3,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":9,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":10,"aiid":1,"in":[]}`
`action{"did":"filter-reset-filter-life","siid":11,"aiid":1,"in":[]}`
`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
Please test and feedback if they are working to they can be linked to a channel.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Dreame Robot Vacuum D9 | miio:basic | [dreame.vacuum.p2009](#dreame-vacuum-p2009) | Yes | Identified manual actions for execution not linked in the database >`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
| +| Trouver Robot LDS Vacuum-Mop Finder | miio:basic | [dreame.vacuum.p2036](#dreame-vacuum-p2036) | Yes | Identified manual actions for execution not linked in the database >`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
| +| Mi Robot Vacuum-Mop 2 Pro+ | miio:basic | [dreame.vacuum.p2041o](#dreame-vacuum-p2041o) | Yes | Identified manual actions for execution
`action{"did":"vacuum-start-sweep","siid":2,"aiid":1,"in":[]}`
`action{"did":"vacuum-stop-sweeping","siid":2,"aiid":2,"in":[]}`
`action{"did":"battery-start-charge","siid":3,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":9,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":10,"aiid":1,"in":[]}`
`action{"did":"filter-reset-filter-life","siid":11,"aiid":1,"in":[]}`
`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
Please test and feedback if they are working to they can be linked to a channel.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| MOVA Z500 Robot Vacuum and Mop Cleaner | miio:basic | [dreame.vacuum.p2156o](#dreame-vacuum-p2156o) | Yes | Identified manual actions for execution
`action{"did":"vacuum-start-sweep","siid":2,"aiid":1,"in":[]}`
`action{"did":"vacuum-stop-sweeping","siid":2,"aiid":2,"in":[]}`
`action{"did":"battery-start-charge","siid":3,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":9,"aiid":1,"in":[]}`
`action{"did":"brush-cleaner-reset-brush-life","siid":10,"aiid":1,"in":[]}`
`action{"did":"filter-reset-filter-life","siid":11,"aiid":1,"in":[]}`
`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
Please test and feedback if they are working to they can be linked to a channel.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| MOVA L600 Robot Vacuum and Mop Cleaner | miio:basic | [dreame.vacuum.p2157](#dreame-vacuum-p2157) | Yes | Identified manual actions for execution not linked in the database >`action{"did":"vacuum-extend-start-clean","siid":4,"aiid":1,"in":[10.0]}`
`action{"did":"vacuum-extend-stop-clean","siid":4,"aiid":2,"in":[]}`
`action{"did":"map-map-req","siid":6,"aiid":1,"in":[2.0]}`
`action{"did":"map-update-map","siid":6,"aiid":2,"in":[4.0]}`
`action{"did":"audio-position","siid":7,"aiid":1,"in":[]}`
`action{"did":"audio-play-sound","siid":7,"aiid":2,"in":[]}`
`action{"did":"time-delete-timer","siid":8,"aiid":1,"in":[3.0]}`
| | HUIZUO ARIES For Bedroom | miio:basic | [huayi.light.ari013](#huayi-light-ari013) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | HUIZUO ARIES For Living Room | miio:basic | [huayi.light.aries](#huayi-light-aries) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | HUIZUO Fan Light | miio:basic | [huayi.light.fanwy](#huayi-light-fanwy) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | @@ -454,6 +461,7 @@ Currently the miio binding supports more than 260 different models. | Mi Air Purifier v5 | miio:basic | [zhimi.airpurifier.v5](#zhimi-airpurifier-v5) | Yes | | | Mi Air Purifier Pro v6 | miio:basic | [zhimi.airpurifier.v6](#zhimi-airpurifier-v6) | Yes | | | Mi Air Purifier Pro v7 | miio:basic | [zhimi.airpurifier.v7](#zhimi-airpurifier-v7) | Yes | | +| Mi Air Purifier Pro H | miio:basic | [zhimi.airpurifier.vb2](#zhimi-airpurifier-vb2) | Yes | | | Mi Air Purifier virtual | miio:unsupported | zhimi.airpurifier.virtual | No | | | Mi Air Purifier 2(Virtual) | miio:unsupported | zhimi.airpurifier.vtl_m1 | No | | | Mi Standing Fan | miio:basic | [zhimi.fan.sa1](#zhimi-fan-sa1) | Yes | | @@ -463,6 +471,12 @@ Currently the miio binding supports more than 260 different models. | Smartmi Inverter Pedestal Fan | miio:basic | [zhimi.fan.za1](#zhimi-fan-za1) | Yes | | | Smartmi Standing Fan 2 | miio:basic | [zhimi.fan.za3](#zhimi-fan-za3) | Yes | | | Smartmi Standing Fan 2S | miio:basic | [zhimi.fan.za4](#zhimi-fan-za4) | Yes | | +| Smartmi Standing Fan 3 | miio:basic | [zhimi.fan.za5](#zhimi-fan-za5) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Mi Smart Space Heater S | miio:basic | [zhimi.heater.ma2](#zhimi-heater-ma2) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Mi Smart Baseboard Heater E | miio:basic | [zhimi.heater.ma3](#zhimi-heater-ma3) | Yes | Identified manual actions for execution
`action{"did":"private-service-toggle-switch","siid":8,"aiid":1,"in":[]}`
Please test and feedback if they are working to they can be linked to a channel.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Mi Smart Space Heater S | miio:basic | [zhimi.heater.mc2](#zhimi-heater-mc2) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Smartmi Smart Fan | miio:basic | [zhimi.heater.na1](#zhimi-heater-na1) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Smartmi Smart Fan Heater | miio:basic | [zhimi.heater.nb1](#zhimi-heater-nb1) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Smartmi Radiant Heater Smart Version | miio:basic | [zhimi.heater.za1](#zhimi-heater-za1) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Smartmi Smart Convector Heater 1S | miio:basic | [zhimi.heater.za2](#zhimi-heater-za2) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Smartmi Smart Convector Heater 1S | miio:basic | [zhimi.heater.zb1](#zhimi-heater-zb1) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | @@ -494,6 +508,25 @@ note: the ADVANCED `actions#commands` and `actions#rpc` channels can be used to e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enable a pre-configured timer. See https://github.com/marcelrv/XiaomiRobotVacuumProtocol for all known available commands. +### Qingping Air Monitor Lite (cgllc.airm.cgdn1) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | Value mapping ["settings-set-start-time"="Set Start Time","settings-set-end-time"="Set End Time","settings-set-frequency"="Set Frequency","settings-set-screen-off"="Set Screen Off","settings-set-device-off"="Set Device Off","settings-set-temp-unit"="Set Temp Unit"] | +| relative_humidity | Number:Dimensionless | Environment - Relative Humidity | | +| pm2_5_density | Number:Density | Environment - PM2 5 Density | | +| pm10_density | Number:Density | Environment - PM10 Density | | +| temperature | Number:Temperature | Environment - Temperature | | +| co2_density | Number:Density | Environment - CO2 Density | | +| battery_level | Number:Dimensionless | Battery - Battery Level | | +| charging_state | Number | Battery - Charging State | Value mapping ["1"="Charging","2"="Not charging","3"="Not chargeable"] | +| voltage | Number:ElectricPotential | Battery - Voltage | | +| mac | String | Mac - Mac | | +| monitoring_frequency | Number:Time | Settings - Monitoring Frequency | Value mapping ["1"="Second","60"="Second","300"="Second","600"="Second","0"="Null"] | +| screen_off | Number:Time | Settings - Screen Off | Value mapping ["15"="Second","30"="Second","60"="Second","300"="Second","0"="Null"] | +| device_off | Number:Time | Settings - Device Off | Value mapping ["15"="Minute","30"="Minute","60"="Minute","0"="Null"] | +| tempature_unit | String | Settings - Tempature Unit | | + ### Mi Multifunction Air Monitor (cgllc.airmonitor.b1) Channels | Channel | Type | Description | Comment | @@ -522,14 +555,14 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl |----------------------|----------------------|------------------------------------------|------------| | on | Switch | Power | | | temperature | Number:Temperature | Temperature | | -| working-time | Number:Duration | Working Time | | +| working-time | Number:Time | Working Time | | | on1 | Switch | Indicator Light - Switch Status | | | power-consumption | Number:Energy | Daily Power Consumption | | | electric-current | Number:Current | Power Consumption - Electric Current | | | voltage | Number:ElectricPotential | Power Consumption - Voltage | | | electric-power | Number:Power | Current Power Consumption - Electric Power | | -| on-duration | Number:Duration | Imilab Timer - On Duration | | -| off-duration | Number:Duration | Imilab Timer - Off Duration | | +| on-duration | Number:Time | Imilab Timer - On Duration | | +| off-duration | Number:Time | Imilab Timer - Off Duration | | | countdown | Number:Time | Imilab Timer - Countdown | | | task-switch | Switch | Imilab Timer - Task Switch | | | countdown-info | Switch | Imilab Timer - Countdown Info | | @@ -546,7 +579,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| power | Switch | Power | If this channel does not respond to on/off replace the model with chuangmi.plug.v3old in the config or upgrade firmware | +| power | Switch | Power | If this channel does not respond to on/off upgrade firmware | | usb | Switch | USB | | | temperature | Number:Temperature | Temperature | | | led | Switch | Wifi LED | | @@ -555,7 +588,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| power | Switch | Power | If this channel does not respond to on/off replace the model with chuangmi.plug.v3old in the config or upgrade firmware | +| power | Switch | Power | If this channel does not respond to on/off upgrade firmware | | usb | Switch | USB | | | temperature | Number:Temperature | Temperature | | | led | Switch | Wifi LED | | @@ -595,7 +628,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| power | Switch | Power | If this channel does not respond to on/off replace the model with chuangmi.plug.v3old in the config or upgrade firmware | +| power | Switch | Power | If this channel does not respond to on/off upgrade firmware | | usb | Switch | USB | | | temperature | Number:Temperature | Temperature | | | led | Switch | Wifi LED | | @@ -610,6 +643,43 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | SerialNumber | String | Device Information-Device Serial Number | | | On | Switch | Switch-Switch Status | | +### Mi Smart Antibacterial Humidifier (deerma.humidifier.jsq) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| power | Switch | Power | | +| mode | Number | Mode | | +| humidity | Number:Dimensionless | Humidity | | +| humidity_set | Number:Dimensionless | Humidity Setting | | +| led | Switch | LED indicator Light | | +| sound | Switch | Notification Sounds | | +| watertankstatus | Number | Watertank Status | | + +### Mi S Smart humidifer (deerma.humidifier.jsq1) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| power | Switch | Power | | +| mode | Number | Mode | | +| humidity | Number:Dimensionless | Humidity | | +| humidity_set | Number:Dimensionless | Humidity Setting | | +| led | Switch | LED indicator Light | | +| sound | Switch | Notification Sounds | | +| watertankstatus | Number | Watertank Status | | +| wet_and_protect | Switch | Wet and Protect | | + +### Mi Smart Humidifier (deerma.humidifier.mjjsq) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| power | Switch | Power | | +| mode | Number | Mode | | +| humidity | Number:Dimensionless | Humidity | | +| humidity_set | Number:Dimensionless | Humidity Setting | | +| led | Switch | LED indicator Light | | +| sound | Switch | Notification Sounds | | +| watertankstatus | Number | Watertank Status | | + ### Mi Fresh Air Ventilator A1-150 (dmaker.airfresh.a1) Channels | Channel | Type | Description | Comment | @@ -837,7 +907,175 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | cleaning-mode | Number | Vacuum Extend - Cleaning Mode | Value mapping ["0"="mode 0","1"="mode 1","2"="mode 2","3"="mode 3"] | | mop-mode | Number | Vacuum Extend - Mop Mode | Value mapping ["1"="low water","2"="medium water","3"="high water"] | | waterbox-status | Number | Vacuum Extend - Waterbox Status | Value mapping ["0"="Status 0","1"="Status 1"] | -| task-status | Number | Vacuum Extend - Task Status | | +| task-status | Number | Vacuum Extend - Task Status | Value mapping ["0"="Notask","1"="AutoClean","2"="CustomClean","3"="SelectAreanClean","4"="SpotArea"] | +| break-point-restart | Number | Vacuum Extend - Break Point Restart | Value mapping ["0"="Off","1"="On"] | +| carpet-press | Number | Vacuum Extend - Carpet Press | Value mapping ["0"="Off","1"="On"] | +| serial-number1 | String | Vacuum Extend - Serial Number | | +| clean-rags-tip | Number:Time | Vacuum Extend - Clean Rags Tip | | +| keep-sweeper-time | Number:Time | Vacuum Extend - Keep Sweeper Time | | +| faults | String | Vacuum Extend - Faults | | +| enable | Switch | Do Not Disturb - Enable | | +| start-time | String | Do Not Disturb - Start Time | | +| end-time | String | Do Not Disturb - End Time | | +| volume | Number:Dimensionless | Audio - Volume | | +| voice-packet-id | String | Audio - Voice Packet Id | | +| voice-change-state | String | Audio - Voice Change State | | +| time-zone | String | Time - Time Zone | | +| timer-clean | String | Time - Timer Clean | | +| first-clean-time | Number | Clean Logs - First Clean Time | | +| total-clean-time | Number:Time | Clean Logs - Total Clean Time | | +| total-clean-times | Number | Clean Logs - Total Clean Times | | +| total-clean-area | Number | Clean Logs - Total Clean Area | | + +### Trouver Robot LDS Vacuum-Mop Finder (dreame.vacuum.p2036) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| vacuumaction | String | Vacuum Action | Value mapping ["sweep"="Sweep","stopsweep"="Stop Sweep","dock"="Goto Dock"] | +| status | Number | Robot Cleaner - Status | Value mapping ["1"="Sweeping","2"="Idle","3"="Paused","4"="Error","5"="Go Charging","6"="Charging","7"="Mopping"] | +| fault | Number | Robot Cleaner - Device Fault | | +| battery-level | Number:Dimensionless | Battery - Battery Level | | +| charging-state | Number | Battery - Charging State | Value mapping ["1"="Charging","2"="Not Charging","5"="Go Charging"] | +| resetConsumable | String | Consumables Reset | Value mapping ["mainbrush-cleaner-reset-brush-life"="Reset Main Brush","sidebrush-cleaner-reset-brush-life"="Reset Side Brush","filter-reset-filter-life"="Reset Filter"] | +| brush-left-time | Number:Time | Main Cleaning Brush - Brush Left Time | | +| brush-life-level | Number:Dimensionless | Main Cleaning Brush - Brush Life Level | | +| brush-left-time1 | Number:Time | Side Cleaning Brush - Brush Left Time | | +| brush-life-level1 | Number:Dimensionless | Side Cleaning Brush - Brush Life Level | | +| filter-life-level | Number:Dimensionless | Filter - Filter Life Level | | +| filter-left-time | Number:Time | Filter - Filter Left Time | | +| work-mode | Number | Vacuum Extend - Work Mode | | +| cleaning-time | Number:Time | Vacuum Extend - Cleaning Time | | +| cleaning-area | Number:Area | Vacuum Extend - Cleaning Area | | +| cleaning-mode | Number | Vacuum Extend - Cleaning Mode | Value mapping ["0"="mode 0","1"="mode 1","2"="mode 2","3"="mode 3"] | +| mop-mode | Number | Vacuum Extend - Mop Mode | Value mapping ["1"="low water","2"="medium water","3"="high water"] | +| waterbox-status | Number | Vacuum Extend - Waterbox Status | Value mapping ["0"="Status 0","1"="Status 1"] | +| task-status | Number | Vacuum Extend - Task Status | Value mapping ["0"="Notask","1"="AutoClean","2"="CustomClean","3"="SelectAreanClean","4"="SpotArea"] | +| break-point-restart | Number | Vacuum Extend - Break Point Restart | Value mapping ["0"="Off","1"="On"] | +| carpet-press | Number | Vacuum Extend - Carpet Press | Value mapping ["0"="Off","1"="On"] | +| serial-number1 | String | Vacuum Extend - Serial Number | | +| clean-rags-tip | Number:Time | Vacuum Extend - Clean Rags Tip | | +| keep-sweeper-time | Number:Time | Vacuum Extend - Keep Sweeper Time | | +| faults | String | Vacuum Extend - Faults | | +| enable | Switch | Do Not Disturb - Enable | | +| start-time | String | Do Not Disturb - Start Time | | +| end-time | String | Do Not Disturb - End Time | | +| volume | Number:Dimensionless | Audio - Volume | | +| voice-packet-id | String | Audio - Voice Packet Id | | +| voice-change-state | String | Audio - Voice Change State | | +| time-zone | String | Time - Time Zone | | +| timer-clean | String | Time - Timer Clean | | +| first-clean-time | Number | Clean Logs - First Clean Time | | +| total-clean-time | Number:Time | Clean Logs - Total Clean Time | | +| total-clean-times | Number | Clean Logs - Total Clean Times | | +| total-clean-area | Number | Clean Logs - Total Clean Area | | + +### Mi Robot Vacuum-Mop 2 Pro+ (dreame.vacuum.p2041o) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | Value mapping ["vacuum-start-sweep"="Start Sweep","vacuum-stop-sweeping"="Stop Sweeping","battery-start-charge"="Start Charge","brush-cleaner-reset-brush-life"="Brush Cleaner Reset Brush Life","brush-cleaner-reset-brush-life"="Brush Cleaner Reset Brush Life","filter-reset-filter-life"="Filter Reset Filter Life","vacuum-extend-start-clean"="Vacuum Extend Start Clean","vacuum-extend-stop-clean"="Vacuum Extend Stop Clean","map-map-req"="Map Map Req","map-update-map"="Map Update Map","audio-position"="Audio Position","audio-play-sound"="Audio Play Sound","time-delete-timer"="Time Delete Timer"] | +| status | Number | Robot Cleaner - Status | Value mapping ["1"="Sweeping","2"="Idle","3"="Paused","4"="Error","5"="Go Charging","6"="Charging","7"="Mopping"] | +| fault | Number | Robot Cleaner - Device Fault | | +| mode | Number | Robot Cleaner - Mode | Value mapping ["0"="Silent","1"="Basic","2"="Strong","3"="Full Speed"] | +| battery_level | Number:Dimensionless | Battery - Battery Level | | +| charging_state | Number | Battery - Charging State | Value mapping ["1"="Charging","2"="Not Charging","5"="Go Charging"] | +| brush_left_time | Number:Time | Main Cleaning Brush - Brush Left Time | | +| brush_life_level | Number:Dimensionless | Main Cleaning Brush - Brush Life Level | | +| brush_left_time1 | Number:Time | Side Cleaning Brush - Brush Left Time | | +| brush_life_level1 | Number:Dimensionless | Side Cleaning Brush - Brush Life Level | | +| filter_life_level | Number:Dimensionless | Filter - Filter Life Level | | +| filter_left_time | Number:Time | Filter - Filter Left Time | | +| work_mode | Number | Vacuum Extend - Work Mode | | +| cleaning_time | Number:Time | Vacuum Extend - Cleaning Time | | +| cleaning-area | Number:Area | Vacuum Extend - Cleaning Area | | +| cleaning_mode | Number | Vacuum Extend - Cleaning Mode | Value mapping ["0"="mode 0","1"="mode 1","2"="mode 2","3"="mode 3"] | +| mop_mode | Number | Vacuum Extend - Mop Mode | Value mapping ["1"="low water","2"="medium water","3"="high water"] | +| waterbox_status | Number | Vacuum Extend - Waterbox Status | Value mapping ["0"="Status 0","1"="Status 1"] | +| task_status | Number | Vacuum Extend - Task Status | Value mapping ["0"="Notask","1"="AutoClean","2"="CustomClean","3"="SelectAreanClean","4"="SpotArea"] | +| break_point_restart | Number | Vacuum Extend - Break Point Restart | Value mapping ["0"="Off","1"="On"] | +| carpet_press | Number | Vacuum Extend - Carpet Press | Value mapping ["0"="Off","1"="On"] | +| serial_number | String | Vacuum Extend - Serial Number | | +| keep_sweeper_time | Number:Time | Vacuum Extend - Keep Sweeper Time | | +| faults | String | Vacuum Extend - Faults | | +| enable | Switch | Do Not Disturb - Enable | | +| start_time | String | Do Not Disturb - Start Time | | +| end_time | String | Do Not Disturb - End Time | | +| volume | Number:Dimensionless | Audio - Volume | | +| voice_packet_id | String | Audio - Voice Packet Id | | +| voice_change_state | String | Audio - Voice Change State | | +| time_zone | String | Time - Time Zone | | +| timer_clean | String | Time - Timer Clean | | +| first_clean_time | Number | Clean Logs - First Clean Time | | +| total_clean_time | Number:Time | Clean Logs - Total Clean Time | | +| total_clean_times | Number | Clean Logs - Total Clean Times | | +| total_clean_area | Number | Clean Logs - Total Clean Area | | +| save_map_status | Number | Vslam Extend - Save Map Status | Value mapping ["0"="Off","1"="On"] | + +### MOVA Z500 Robot Vacuum and Mop Cleaner (dreame.vacuum.p2156o) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | Value mapping ["vacuum-start-sweep"="Start Sweep","vacuum-stop-sweeping"="Stop Sweeping","battery-start-charge"="Start Charge","brush-cleaner-reset-brush-life"="Brush Cleaner Reset Brush Life","brush-cleaner-reset-brush-life"="Brush Cleaner Reset Brush Life","filter-reset-filter-life"="Filter Reset Filter Life","vacuum-extend-start-clean"="Vacuum Extend Start Clean","vacuum-extend-stop-clean"="Vacuum Extend Stop Clean","map-map-req"="Map Map Req","map-update-map"="Map Update Map","audio-position"="Audio Position","audio-play-sound"="Audio Play Sound","time-delete-timer"="Time Delete Timer"] | +| status | Number | Robot Cleaner - Status | Value mapping ["1"="Sweeping","2"="Idle","3"="Paused","4"="Error","5"="Go Charging","6"="Charging","7"="Mopping"] | +| fault | Number | Robot Cleaner - Device Fault | | +| mode | Number | Robot Cleaner - Mode | Value mapping ["0"="Silent","1"="Basic","2"="Strong","3"="Full Speed"] | +| battery_level | Number:Dimensionless | Battery - Battery Level | | +| charging_state | Number | Battery - Charging State | Value mapping ["1"="Charging","2"="Not Charging","5"="Go Charging"] | +| brush_left_time | Number:Time | Main Cleaning Brush - Brush Left Time | | +| brush_life_level | Number:Dimensionless | Main Cleaning Brush - Brush Life Level | | +| brush_left_time1 | Number:Time | Side Cleaning Brush - Brush Left Time | | +| brush_life_level1 | Number:Dimensionless | Side Cleaning Brush - Brush Life Level | | +| filter_life_level | Number:Dimensionless | Filter - Filter Life Level | | +| filter_left_time | Number:Time | Filter - Filter Left Time | | +| work_mode | Number | Vacuum Extend - Work Mode | | +| cleaning_time | Number:Time | Vacuum Extend - Cleaning Time | | +| cleaning-area | Number:Area | Vacuum Extend - Cleaning Area | | +| cleaning_mode | Number | Vacuum Extend - Cleaning Mode | Value mapping ["0"="mode 0","1"="mode 1","2"="mode 2","3"="mode 3"] | +| mop_mode | Number | Vacuum Extend - Mop Mode | Value mapping ["1"="low water","2"="medium water","3"="high water"] | +| waterbox_status | Number | Vacuum Extend - Waterbox Status | Value mapping ["0"="Status 0","1"="Status 1"] | +| task_status | Number | Vacuum Extend - Task Status | Value mapping ["0"="Notask","1"="AutoClean","2"="CustomClean","3"="SelectAreanClean","4"="SpotArea"] | +| break_point_restart | Number | Vacuum Extend - Break Point Restart | Value mapping ["0"="Off","1"="On"] | +| carpet_press | Number | Vacuum Extend - Carpet Press | Value mapping ["0"="Off","1"="On"] | +| serial_number | String | Vacuum Extend - Serial Number | | +| keep_sweeper_time | Number:Time | Vacuum Extend - Keep Sweeper Time | | +| faults | String | Vacuum Extend - Faults | | +| enable | Switch | Do Not Disturb - Enable | | +| start_time | String | Do Not Disturb - Start Time | | +| end_time | String | Do Not Disturb - End Time | | +| volume | Number:Dimensionless | Audio - Volume | | +| voice_packet_id | String | Audio - Voice Packet Id | | +| voice_change_state | String | Audio - Voice Change State | | +| time_zone | String | Time - Time Zone | | +| timer_clean | String | Time - Timer Clean | | +| first_clean_time | Number | Clean Logs - First Clean Time | | +| total_clean_time | Number:Time | Clean Logs - Total Clean Time | | +| total_clean_times | Number | Clean Logs - Total Clean Times | | +| total_clean_area | Number | Clean Logs - Total Clean Area | | +| save_map_status | Number | Vslam Extend - Save Map Status | Value mapping ["0"="Off","1"="On"] | + +### MOVA L600 Robot Vacuum and Mop Cleaner (dreame.vacuum.p2157) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| vacuumaction | String | Vacuum Action | Value mapping ["sweep"="Sweep","stopsweep"="Stop Sweep","dock"="Goto Dock"] | +| status | Number | Robot Cleaner - Status | Value mapping ["1"="Sweeping","2"="Idle","3"="Paused","4"="Error","5"="Go Charging","6"="Charging","7"="Mopping"] | +| fault | Number | Robot Cleaner - Device Fault | | +| battery-level | Number:Dimensionless | Battery - Battery Level | | +| charging-state | Number | Battery - Charging State | Value mapping ["1"="Charging","2"="Not Charging","5"="Go Charging"] | +| resetConsumable | String | Consumables Reset | Value mapping ["mainbrush-cleaner-reset-brush-life"="Reset Main Brush","sidebrush-cleaner-reset-brush-life"="Reset Side Brush","filter-reset-filter-life"="Reset Filter"] | +| brush-left-time | Number:Time | Main Cleaning Brush - Brush Left Time | | +| brush-life-level | Number:Dimensionless | Main Cleaning Brush - Brush Life Level | | +| brush-left-time1 | Number:Time | Side Cleaning Brush - Brush Left Time | | +| brush-life-level1 | Number:Dimensionless | Side Cleaning Brush - Brush Life Level | | +| filter-life-level | Number:Dimensionless | Filter - Filter Life Level | | +| filter-left-time | Number:Time | Filter - Filter Left Time | | +| work-mode | Number | Vacuum Extend - Work Mode | | +| cleaning-time | Number:Time | Vacuum Extend - Cleaning Time | | +| cleaning-area | Number:Area | Vacuum Extend - Cleaning Area | | +| cleaning-mode | Number | Vacuum Extend - Cleaning Mode | Value mapping ["0"="mode 0","1"="mode 1","2"="mode 2","3"="mode 3"] | +| mop-mode | Number | Vacuum Extend - Mop Mode | Value mapping ["1"="low water","2"="medium water","3"="high water"] | +| waterbox-status | Number | Vacuum Extend - Waterbox Status | Value mapping ["0"="Status 0","1"="Status 1"] | +| task-status | Number | Vacuum Extend - Task Status | Value mapping ["0"="Notask","1"="AutoClean","2"="CustomClean","3"="SelectAreanClean","4"="SpotArea"] | | break-point-restart | Number | Vacuum Extend - Break Point Restart | Value mapping ["0"="Off","1"="On"] | | carpet-press | Number | Vacuum Extend - Carpet Press | Value mapping ["0"="Off","1"="On"] | | serial-number1 | String | Vacuum Extend - Serial Number | | @@ -2060,7 +2298,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2084,7 +2322,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2108,7 +2346,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2132,7 +2370,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2156,7 +2394,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2180,7 +2418,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2204,7 +2442,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2228,7 +2466,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2252,7 +2490,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2276,7 +2514,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2300,7 +2538,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2324,7 +2562,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2348,7 +2586,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2372,7 +2610,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2396,7 +2634,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2420,7 +2658,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2444,7 +2682,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2468,7 +2706,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2492,7 +2730,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2516,7 +2754,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | elec-count | Number | Electricity - Count | | | clean | String | Maintenance - Clean | | | examine | String | Maintenance - Examine | | -| running-duration | Number:Duration | Maintenance - Running Duration | | +| running-duration | Number:Time | Maintenance - Running Duration | | | fan-percent | Number:Dimentionless | Fan Speed % | | | timer | String | Enhance - Timer | | @@ -2663,7 +2901,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl |----------------------|----------------------|------------------------------------------|------------| | power | Switch | Power | | | brightness | Dimmer | Brightness | | -| ambientBrightness | Number | Ambient Brightness | | +| ambientBrightness | Dimmer | Ambient Brightness | | | delayoff | Number:Time | Shutdown Timer | | | colorTemperature | Number | Color Temperature | | | colorMode | Number | Color Mode | | @@ -2759,7 +2997,7 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl |----------------------|----------------------|------------------------------------------|------------| | power | Switch | Power | | | brightness | Dimmer | Brightness | | -| ambientBrightness | Number | Ambient Brightness | | +| ambientBrightness | Dimmer | Ambient Brightness | | | delayoff | Number:Time | Shutdown Timer | | | colorTemperature | Number | Color Temperature | | | colorMode | Number | Color Mode | | @@ -4203,6 +4441,62 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | temperature | Number:Temperature | Temperature | | | childlock | Switch | Child Lock | | +### Mi Air Purifier Pro H (zhimi.airpurifier.vb2) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | Value mapping ["button-toggle"="Toggle","button-toggle-mode"="Toggle Mode"] | +| fault | Number | Air Purifier - Device Fault | Value mapping ["0"="No faults","1"="m1_run","2"="m1_stuck","3"="no_sensor","4"="error_hum","5"="error_temp","6"="timer_error1","7"="timer_error2"] | +| on | Switch | Air Purifier - Power | | +| fan_level | Number | Air Purifier - Fan Level | Value mapping ["1"="Level1","2"="Level2","3"="Level3","0"="Sleep"] | +| mode | Number | Air Purifier - Mode | Value mapping ["0"="Auto","1"="Night","2"="Favourite","3"="Manual"] | +| pm2_5_density | Number | Environment - PM2 5 Density | | +| relative_humidity | Number:Dimensionless | Environment - Relative Humidity | | +| temperature | Number:Temperature | Environment - Temperature | | +| filter_life_level | Number:Dimensionless | Filter - Filter Life Level | | +| filter_used_time | Number:Time | Filter - Filter Used Time | | +| alarm | Switch | Alarm - Alarm | | +| volume | Number:Dimensionless | Alarm - Volume | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="brightest","1"="glimmer","2"="not bright"] | +| on1 | Switch | Indicator Light - Switch Status | | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| button_pressed | String | Button - Button_pressed | | +| filter_max_time | Number:Time | Filter Time - Filter Max Time | | +| filter_hour_used_debug | Number:Time | Filter Time - Filter Hour Used Debug | | +| m1_strong | Number | Motor Speed - M1 Strong | | +| m1_high | Number | Motor Speed - M1 High | | +| m1_med | Number | Motor Speed - M1 Med | | +| m1_med_l | Number | Motor Speed - M1 Med L | | +| m1_low | Number | Motor Speed - M1 Low | | +| m1_silent | Number | Motor Speed - M1 Silent | | +| m1_favorite | Number | Motor Speed - M1 Favorite | | +| motor1_speed | Number | Motor Speed - Motor1 Speed | | +| motor1_set_speed | Number | Motor Speed - Motor1 Set Speed | | +| favorite_level | Number | Motor Speed - Favorite Level | | +| use_time | Number:Time | Use Time - Use Time | | +| purify_volume | Number | Aqi - Purify Volume | | +| average_aqi | Number | Aqi - Average Aqi | | +| average_aqi_cnt | Number | Aqi - Average_aqi Read Times | | +| aqi_zone | String | Aqi - Aqi Zone | | +| sensor_state | Number | Aqi - Sensor State | Value mapping ["0"="waiting","1"="ready"] | +| aqi_goodh | Number | Aqi - Aqi Goodh | | +| aqi_runstate | Number | Aqi - Runstate | Value mapping ["0"="continue","1"="hold","2"="sleep"] | +| aqi_state | Number | Aqi - Aqi State | Value mapping ["0"="AQI_GOOD_L","1"="AQI_GOOD_H","2"="AQI_MID_L","3"="AQI_MID_H","4"="AQI_BAD_L","5"="AQI_BAD_H"] | +| rfid_tag | String | Rfid - Rfid Tag | | +| rfid_factory_id | String | Rfid - Rfid Factory Id | | +| rfid_product_id | String | Rfid - Rfid Product Id | | +| rfid_time | String | Rfid - Rfid Time | | +| rfid_serial_num | String | Rfid - Rfid Serial Num | | +| app_extra | Number | Others - App Extra | | +| main_channel | Number | Others - Main Channel | | +| slave_channel | Number | Others - Slave Channel | | +| cola | String | Others - Cola | | +| buttom_door | String | Others - Buttom Door | | +| reboot_cause | Number | Others - Reboot_cause | Value mapping ["0"="REASON_HW_BOOT","1"="REASON_USER_REBOOT","2"="REASON_UPDATE","3"="REASON_WDT"] | +| manual_level | Number | Others - Manual Level | Value mapping ["1"="level1","2"="level2","3"="level3"] | +| powertime | Number:duration | Others - Powertime | | +| country_code | Number | Others - Country Code | Value mapping ["91"="India","44"="UK","852"="Hong Kong","886"="Taiwan","82"="Korea"] | + ### Mi Standing Fan (zhimi.fan.sa1) Channels | Channel | Type | Description | Comment | @@ -4339,6 +4633,107 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | naturalLevel | Number | Natural Level | | | move | String | Move Direction | | +### Smartmi Standing Fan 3 (zhimi.fan.za5) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| on | Switch | Fan - Power | | +| fan_level | Number | Fan - Fan Level | Value mapping ["1"="Level 1","2"="Level 2","3"="Level 3","4"="Level 4"] | +| horizontal_swing | Switch | Fan - Horizontal Swing | | +| horizontal_angle | Number | Fan - Horizontal Angle | | +| mode | Number | Fan - Mode | Value mapping ["0"="Natural Wind","1"="Straight Wind"] | +| off_delay | Number | Fan - Power Off Delay | | +| anion | Switch | Fan - Anion | | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| brightness | Number:Dimensionless | Indicator Light - Brightness | | +| alarm | Switch | Alarm - Alarm | | +| relative_humidity | Number:Dimensionless | Environment - Relative Humidity | | +| temperature | Number:Temperature | Environment - Temperature | | +| button_press | Number | Custom Service - Button Press | Value mapping ["1"="power","2"="swing","0"="No Button Pressed"] | +| battery_state | Switch | Custom Service - Battery State | | +| speed_now | Number | Custom Service - Speed Now | | +| ac_state | Switch | Custom Service - Ac State | | +| speed_level | Number:Dimensionless | Custom Service - Speed Level | | + +### Mi Smart Space Heater S (zhimi.heater.ma2) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| on | Switch | Heater - Switch Status | | +| fault | Number | Heater - Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | +| target_temperature | Number:Temperature | Heater - Target Temperature | | +| countdown_time | Number:Time | Countdown - Countdown Time | | +| temperature | Number:Temperature | Environment - Temperature | | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| alarm | Switch | Alarm - Alarm | | +| brightness | Number:Dimensionless | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | +| hw_enable | Switch | Private Service - Hw Enable | | +| use_time | Number:Time | Private Service - Use Time | | + +### Mi Smart Baseboard Heater E (zhimi.heater.ma3) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | | +| on | Switch | Heater - Switch Status | | +| fault | Number | Heater - Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | +| target_temperature | Number:Temperature | Heater - Target Temperature | | +| mode | Number | Heater - Mode | Value mapping ["0"="Auto","1"="LL Mode","2"="HH Mode"] | +| countdown_time | Number:Time | Countdown - Countdown Time | | +| temperature | Number:Temperature | Environment - Temperature | | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| alarm | Switch | Alarm - Alarm | | +| brightness | Number:Dimensionless | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | +| use_time | Number:Time | Private Service - Use Time | | + +### Mi Smart Space Heater S (zhimi.heater.mc2) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| on | Switch | Heater - Power | | +| fault | Number | Heater - Device Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | +| target_temperature | Number:Temperature | Heater - Target Temperature | | +| countdown_time | Number:Time | Countdown - Countdown Time | | +| temperature | Number:Temperature | Environment - Temperature | | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| alarm | Switch | Alarm - Alarm | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | +| hw_enable | Switch | Private Service - Hw Enable | | +| use_time | Number:Time | Private Service - Use Time | | +| country_code | Number | Private Service - Country Code | Value mapping ["0"="Unknown","1"="US","82"="KR","44"="EU","81"="JP","7"="RU","86"="CN","852"="HK","886"="TW","33"="FR"] | + +### Smartmi Smart Fan (zhimi.heater.na1) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| on | Switch | Heater - Power | | +| fault | Number | Heater - Device Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | +| heat_level | Number | Heater - Heat Level | Value mapping ["1"="High","2"="Low"] | +| mode | Number | Heater - Mode | Value mapping ["0"="Fan not swing","1"="Fan swing"] | +| alarm | Switch | Alarm - Alarm | | +| countdown_time | Number:Time | Countdown - Countdown Time | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| return_to_middle | Switch | Private Service - Return To Middle | | + +### Smartmi Smart Fan Heater (zhimi.heater.nb1) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| on | Switch | Heater - Power | | +| fault | Number | Heater - Device Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | +| heat_level | Number | Heater - Heat Level | Value mapping ["1"="High","2"="Low"] | +| mode | Number | Heater - Mode | Value mapping ["0"="Fan not swing","1"="Fan swing"] | +| target_temperature | Number:Temperature | Heater - Target Temperature | | +| temperature | Number:Temperature | Environment - Temperature | | +| alarm | Switch | Alarm - Alarm | | +| countdown_time | Number:Time | Countdown - Countdown Time | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | +| physical_controls_locked | Switch | Physical Control Locked - Physical Control Locked | | +| return_to_middle | Switch | Private Service - Return To Middle | | +| country_code | Number | Private Service - Country Code | Value mapping ["0"="Unknown","1"="US","82"="KR","44"="EU","81"="JP","7"="RU","86"="CN","852"="HK","886"="TW","33"="FR"] | +| hw_en | Switch | Private Service - Hw En | | + ### Smartmi Radiant Heater Smart Version (zhimi.heater.za1) Channels | Channel | Type | Description | Comment | @@ -4357,14 +4752,14 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| fault | Number | Heater - Device Fault | | +| fault | Number | Heater - Device Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | | on | Switch | Heater - Power | | | target-temperature | Number:Temperature | Heater - Target Temperature | | | alarm | Switch | Alarm - Alarm | | | countdown-time | Number:Time | Countdown - Countdown Time | | | relative-humidity | Number:Dimensionless | Environment - Relative Humidity | | | temperature | Number:Temperature | Environment - Temperature | | -| brightness | Dimmer | Indicator Light - Brightness | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | | physical-controls-locked | Switch | Physical Control Locked - Physical Controls Locked | | | use-time | Number:Time | Private-Service - Use Time | | @@ -4372,14 +4767,14 @@ e.g. `openhab:send actionCommand 'upd_timer["1498595904821", "on"]'` would enabl | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| fault | Number | Heater - Device Fault | | | on | Switch | Heater - Power | | +| fault | Number | Heater - Device Fault | Value mapping ["0"="No Error","1"="NTC Connect Error","2"="High Temperature Alarm","3"="EEPROM Error","4"="Multi Errors"] | | target-temperature | Number:Temperature | Heater - Target Temperature | | | alarm | Switch | Alarm - Alarm | | | countdown-time | Number:Time | Countdown - Countdown Time | | | relative-humidity | Number:Dimensionless | Environment - Relative Humidity | | | temperature | Number:Temperature | Environment - Temperature | | -| brightness | Dimmer | Indicator Light - Brightness | | +| brightness | Number | Indicator Light - Brightness | Value mapping ["0"="Bright","1"="Dark","2"="Extinguished"] | | physical-controls-locked | Switch | Physical Control Locked - Physical Controls Locked | | | use-time | Number:Time | Private-Service - Use Time | | | country-code | Number | Private-Service - Country-Code | Value mapping ["0"="Unknown","1"="US","82"="KR","44"="EU","81"="JP","7"="RU","86"="CN","852"="HK","886"="TW","33"="FR"] | @@ -4543,8 +4938,20 @@ Image map "Cleaning Map" (gVacLast) {channel="miio:vacuum:034F0E45:cleaning#map" Note: cleaning map is only available with cloud access. -Additionally depending on the capabilities of your robot vacuum other channels may be enabled at runtime +There are several advanced channels, which may be useful in rules (e.g. for individual room cleaning etc) +In case your vacuum does not support one of these commands, it will show "unsupported_method" for string channels or no value for numeric channels. +| Type | Channel | Description | +|---------|-----------------------------------|----------------------------| +| Number | status#segment_status | Segment Status | +| Number | status#map_status | Map Box Status | +| Number | status#led_status | Led Box Status | +| String | info#carpet_mode | Carpet Mode details | +| String | info#fw_features | Firmware Features | +| String | info#room_mapping | Room Mapping details | +| String | info#multi_maps_list | Maps Listing details | + +Additionally depending on the capabilities of your robot vacuum other channels may be enabled at runtime | Type | Channel | Description | |---------|-----------------------------------|----------------------------| @@ -4558,6 +4965,28 @@ Additionally depending on the capabilities of your robot vacuum other channels m +### Qingping Air Monitor Lite (cgllc.airm.cgdn1) item file lines + +note: Autogenerated example. Replace the id (airm) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_airm "Qingping Air Monitor Lite" +String actions "Actions" (G_airm) {channel="miio:basic:airm:actions"} +Number:Dimensionless relative_humidity "Environment - Relative Humidity" (G_airm) {channel="miio:basic:airm:relative_humidity"} +Number:Density pm2_5_density "Environment - PM2 5 Density" (G_airm) {channel="miio:basic:airm:pm2_5_density"} +Number:Density pm10_density "Environment - PM10 Density" (G_airm) {channel="miio:basic:airm:pm10_density"} +Number:Temperature temperature "Environment - Temperature" (G_airm) {channel="miio:basic:airm:temperature"} +Number:Density co2_density "Environment - CO2 Density" (G_airm) {channel="miio:basic:airm:co2_density"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_airm) {channel="miio:basic:airm:battery_level"} +Number charging_state "Battery - Charging State" (G_airm) {channel="miio:basic:airm:charging_state"} +Number:ElectricPotential voltage "Battery - Voltage" (G_airm) {channel="miio:basic:airm:voltage"} +String mac "Mac - Mac" (G_airm) {channel="miio:basic:airm:mac"} +Number:Time monitoring_frequency "Settings - Monitoring Frequency" (G_airm) {channel="miio:basic:airm:monitoring_frequency"} +Number:Time screen_off "Settings - Screen Off" (G_airm) {channel="miio:basic:airm:screen_off"} +Number:Time device_off "Settings - Device Off" (G_airm) {channel="miio:basic:airm:device_off"} +String tempature_unit "Settings - Tempature Unit" (G_airm) {channel="miio:basic:airm:tempature_unit"} +``` + ### Mi Multifunction Air Monitor (cgllc.airmonitor.b1) item file lines note: Autogenerated example. Replace the id (airmonitor) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. @@ -4594,14 +5023,14 @@ note: Autogenerated example. Replace the id (plug) in the channel with your own. Group G_plug "Mi Smart Power Plug 2 (Wi-Fi and Bluetooth Gateway)" Switch on "Power" (G_plug) {channel="miio:basic:plug:on"} Number:Temperature temperature "Temperature" (G_plug) {channel="miio:basic:plug:temperature"} -Number:Duration working_time "Working Time" (G_plug) {channel="miio:basic:plug:working-time"} +Number:Time working_time "Working Time" (G_plug) {channel="miio:basic:plug:working-time"} Switch on1 "Indicator Light - Switch Status" (G_plug) {channel="miio:basic:plug:on1"} Number:Energy power_consumption "Daily Power Consumption" (G_plug) {channel="miio:basic:plug:power-consumption"} Number:Current electric_current "Power Consumption - Electric Current" (G_plug) {channel="miio:basic:plug:electric-current"} Number:ElectricPotential voltage "Power Consumption - Voltage" (G_plug) {channel="miio:basic:plug:voltage"} Number:Power electric_power "Current Power Consumption - Electric Power" (G_plug) {channel="miio:basic:plug:electric-power"} -Number:Duration on_duration "Imilab Timer - On Duration" (G_plug) {channel="miio:basic:plug:on-duration"} -Number:Duration off_duration "Imilab Timer - Off Duration" (G_plug) {channel="miio:basic:plug:off-duration"} +Number:Time on_duration "Imilab Timer - On Duration" (G_plug) {channel="miio:basic:plug:on-duration"} +Number:Time off_duration "Imilab Timer - Off Duration" (G_plug) {channel="miio:basic:plug:off-duration"} Number:Time countdown "Imilab Timer - Countdown" (G_plug) {channel="miio:basic:plug:countdown"} Switch task_switch "Imilab Timer - Task Switch" (G_plug) {channel="miio:basic:plug:task-switch"} Switch countdown_info "Imilab Timer - Countdown Info" (G_plug) {channel="miio:basic:plug:countdown-info"} @@ -4710,6 +5139,52 @@ String SerialNumber "Device Information-Device Serial Number" (G_plug) {channel= Switch On "Switch-Switch Status" (G_plug) {channel="miio:basic:plug:On"} ``` +### Mi Smart Antibacterial Humidifier (deerma.humidifier.jsq) item file lines + +note: Autogenerated example. Replace the id (humidifier) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_humidifier "Mi Smart Antibacterial Humidifier" +Switch power "Power" (G_humidifier) {channel="miio:basic:humidifier:power"} +Number mode "Mode" (G_humidifier) {channel="miio:basic:humidifier:mode"} +Number:Dimensionless humidity "Humidity" (G_humidifier) {channel="miio:basic:humidifier:humidity"} +Number:Dimensionless humidity_set "Humidity Setting" (G_humidifier) {channel="miio:basic:humidifier:humidity_set"} +Switch led "LED indicator Light" (G_humidifier) {channel="miio:basic:humidifier:led"} +Switch sound "Notification Sounds" (G_humidifier) {channel="miio:basic:humidifier:sound"} +Number watertankstatus "Watertank Status" (G_humidifier) {channel="miio:basic:humidifier:watertankstatus"} +``` + +### Mi S Smart humidifer (deerma.humidifier.jsq1) item file lines + +note: Autogenerated example. Replace the id (humidifier) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_humidifier "Mi S Smart humidifer " +Switch power "Power" (G_humidifier) {channel="miio:basic:humidifier:power"} +Number mode "Mode" (G_humidifier) {channel="miio:basic:humidifier:mode"} +Number:Dimensionless humidity "Humidity" (G_humidifier) {channel="miio:basic:humidifier:humidity"} +Number:Dimensionless humidity_set "Humidity Setting" (G_humidifier) {channel="miio:basic:humidifier:humidity_set"} +Switch led "LED indicator Light" (G_humidifier) {channel="miio:basic:humidifier:led"} +Switch sound "Notification Sounds" (G_humidifier) {channel="miio:basic:humidifier:sound"} +Number watertankstatus "Watertank Status" (G_humidifier) {channel="miio:basic:humidifier:watertankstatus"} +Switch wet_and_protect "Wet and Protect" (G_humidifier) {channel="miio:basic:humidifier:wet_and_protect"} +``` + +### Mi Smart Humidifier (deerma.humidifier.mjjsq) item file lines + +note: Autogenerated example. Replace the id (humidifier) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_humidifier "Mi Smart Humidifier" +Switch power "Power" (G_humidifier) {channel="miio:basic:humidifier:power"} +Number mode "Mode" (G_humidifier) {channel="miio:basic:humidifier:mode"} +Number:Dimensionless humidity "Humidity" (G_humidifier) {channel="miio:basic:humidifier:humidity"} +Number:Dimensionless humidity_set "Humidity Setting" (G_humidifier) {channel="miio:basic:humidifier:humidity_set"} +Switch led "LED indicator Light" (G_humidifier) {channel="miio:basic:humidifier:led"} +Switch sound "Notification Sounds" (G_humidifier) {channel="miio:basic:humidifier:sound"} +Number watertankstatus "Watertank Status" (G_humidifier) {channel="miio:basic:humidifier:watertankstatus"} +``` + ### Mi Fresh Air Ventilator A1-150 (dmaker.airfresh.a1) item file lines note: Autogenerated example. Replace the id (airfresh) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. @@ -4987,6 +5462,186 @@ Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="m Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total-clean-area"} ``` +### Trouver Robot LDS Vacuum-Mop Finder (dreame.vacuum.p2036) item file lines + +note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_vacuum "Trouver Robot LDS Vacuum-Mop Finder" +String vacuumaction "Vacuum Action" (G_vacuum) {channel="miio:basic:vacuum:vacuumaction"} +Number status "Robot Cleaner - Status" (G_vacuum) {channel="miio:basic:vacuum:status"} +Number fault "Robot Cleaner - Device Fault" (G_vacuum) {channel="miio:basic:vacuum:fault"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_vacuum) {channel="miio:basic:vacuum:battery-level"} +Number charging_state "Battery - Charging State" (G_vacuum) {channel="miio:basic:vacuum:charging-state"} +String resetConsumable "Consumables Reset" (G_vacuum) {channel="miio:basic:vacuum:resetConsumable"} +Number:Time brush_left_time "Main Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush-left-time"} +Number:Dimensionless brush_life_level "Main Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush-life-level"} +Number:Time brush_left_time1 "Side Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush-left-time1"} +Number:Dimensionless brush_life_level1 "Side Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush-life-level1"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_vacuum) {channel="miio:basic:vacuum:filter-life-level"} +Number:Time filter_left_time "Filter - Filter Left Time" (G_vacuum) {channel="miio:basic:vacuum:filter-left-time"} +Number work_mode "Vacuum Extend - Work Mode" (G_vacuum) {channel="miio:basic:vacuum:work-mode"} +Number:Time cleaning_time "Vacuum Extend - Cleaning Time" (G_vacuum) {channel="miio:basic:vacuum:cleaning-time"} +Number:Area cleaning_area "Vacuum Extend - Cleaning Area" (G_vacuum) {channel="miio:basic:vacuum:cleaning-area"} +Number cleaning_mode "Vacuum Extend - Cleaning Mode" (G_vacuum) {channel="miio:basic:vacuum:cleaning-mode"} +Number mop_mode "Vacuum Extend - Mop Mode" (G_vacuum) {channel="miio:basic:vacuum:mop-mode"} +Number waterbox_status "Vacuum Extend - Waterbox Status" (G_vacuum) {channel="miio:basic:vacuum:waterbox-status"} +Number task_status "Vacuum Extend - Task Status" (G_vacuum) {channel="miio:basic:vacuum:task-status"} +Number break_point_restart "Vacuum Extend - Break Point Restart" (G_vacuum) {channel="miio:basic:vacuum:break-point-restart"} +Number carpet_press "Vacuum Extend - Carpet Press" (G_vacuum) {channel="miio:basic:vacuum:carpet-press"} +String serial_number1 "Vacuum Extend - Serial Number" (G_vacuum) {channel="miio:basic:vacuum:serial-number1"} +Number:Time clean_rags_tip "Vacuum Extend - Clean Rags Tip" (G_vacuum) {channel="miio:basic:vacuum:clean-rags-tip"} +Number:Time keep_sweeper_time "Vacuum Extend - Keep Sweeper Time" (G_vacuum) {channel="miio:basic:vacuum:keep-sweeper-time"} +String faults "Vacuum Extend - Faults" (G_vacuum) {channel="miio:basic:vacuum:faults"} +Switch enable "Do Not Disturb - Enable" (G_vacuum) {channel="miio:basic:vacuum:enable"} +String start_time "Do Not Disturb - Start Time" (G_vacuum) {channel="miio:basic:vacuum:start-time"} +String end_time "Do Not Disturb - End Time" (G_vacuum) {channel="miio:basic:vacuum:end-time"} +Number:Dimensionless volume "Audio - Volume" (G_vacuum) {channel="miio:basic:vacuum:volume"} +String voice_packet_id "Audio - Voice Packet Id" (G_vacuum) {channel="miio:basic:vacuum:voice-packet-id"} +String voice_change_state "Audio - Voice Change State" (G_vacuum) {channel="miio:basic:vacuum:voice-change-state"} +String time_zone "Time - Time Zone" (G_vacuum) {channel="miio:basic:vacuum:time-zone"} +String timer_clean "Time - Timer Clean" (G_vacuum) {channel="miio:basic:vacuum:timer-clean"} +Number first_clean_time "Clean Logs - First Clean Time" (G_vacuum) {channel="miio:basic:vacuum:first-clean-time"} +Number:Time total_clean_time "Clean Logs - Total Clean Time" (G_vacuum) {channel="miio:basic:vacuum:total-clean-time"} +Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="miio:basic:vacuum:total-clean-times"} +Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total-clean-area"} +``` + +### Mi Robot Vacuum-Mop 2 Pro+ (dreame.vacuum.p2041o) item file lines + +note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_vacuum "Mi Robot Vacuum-Mop 2 Pro+" +String actions "Actions" (G_vacuum) {channel="miio:basic:vacuum:actions"} +Number status "Robot Cleaner - Status" (G_vacuum) {channel="miio:basic:vacuum:status"} +Number fault "Robot Cleaner - Device Fault" (G_vacuum) {channel="miio:basic:vacuum:fault"} +Number mode "Robot Cleaner - Mode" (G_vacuum) {channel="miio:basic:vacuum:mode"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_vacuum) {channel="miio:basic:vacuum:battery_level"} +Number charging_state "Battery - Charging State" (G_vacuum) {channel="miio:basic:vacuum:charging_state"} +Number:Time brush_left_time "Main Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time"} +Number:Dimensionless brush_life_level "Main Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level"} +Number:Time brush_left_time1 "Side Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time1"} +Number:Dimensionless brush_life_level1 "Side Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level1"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_vacuum) {channel="miio:basic:vacuum:filter_life_level"} +Number:Time filter_left_time "Filter - Filter Left Time" (G_vacuum) {channel="miio:basic:vacuum:filter_left_time"} +Number work_mode "Vacuum Extend - Work Mode" (G_vacuum) {channel="miio:basic:vacuum:work_mode"} +Number:Time cleaning_time "Vacuum Extend - Cleaning Time" (G_vacuum) {channel="miio:basic:vacuum:cleaning_time"} +Number:Area cleaning_area "Vacuum Extend - Cleaning Area" (G_vacuum) {channel="miio:basic:vacuum:cleaning-area"} +Number cleaning_mode "Vacuum Extend - Cleaning Mode" (G_vacuum) {channel="miio:basic:vacuum:cleaning_mode"} +Number mop_mode "Vacuum Extend - Mop Mode" (G_vacuum) {channel="miio:basic:vacuum:mop_mode"} +Number waterbox_status "Vacuum Extend - Waterbox Status" (G_vacuum) {channel="miio:basic:vacuum:waterbox_status"} +Number task_status "Vacuum Extend - Task Status" (G_vacuum) {channel="miio:basic:vacuum:task_status"} +Number break_point_restart "Vacuum Extend - Break Point Restart" (G_vacuum) {channel="miio:basic:vacuum:break_point_restart"} +Number carpet_press "Vacuum Extend - Carpet Press" (G_vacuum) {channel="miio:basic:vacuum:carpet_press"} +String serial_number "Vacuum Extend - Serial Number" (G_vacuum) {channel="miio:basic:vacuum:serial_number"} +Number:Time keep_sweeper_time "Vacuum Extend - Keep Sweeper Time" (G_vacuum) {channel="miio:basic:vacuum:keep_sweeper_time"} +String faults "Vacuum Extend - Faults" (G_vacuum) {channel="miio:basic:vacuum:faults"} +Switch enable "Do Not Disturb - Enable" (G_vacuum) {channel="miio:basic:vacuum:enable"} +String start_time "Do Not Disturb - Start Time" (G_vacuum) {channel="miio:basic:vacuum:start_time"} +String end_time "Do Not Disturb - End Time" (G_vacuum) {channel="miio:basic:vacuum:end_time"} +Number:Dimensionless volume "Audio - Volume" (G_vacuum) {channel="miio:basic:vacuum:volume"} +String voice_packet_id "Audio - Voice Packet Id" (G_vacuum) {channel="miio:basic:vacuum:voice_packet_id"} +String voice_change_state "Audio - Voice Change State" (G_vacuum) {channel="miio:basic:vacuum:voice_change_state"} +String time_zone "Time - Time Zone" (G_vacuum) {channel="miio:basic:vacuum:time_zone"} +String timer_clean "Time - Timer Clean" (G_vacuum) {channel="miio:basic:vacuum:timer_clean"} +Number first_clean_time "Clean Logs - First Clean Time" (G_vacuum) {channel="miio:basic:vacuum:first_clean_time"} +Number:Time total_clean_time "Clean Logs - Total Clean Time" (G_vacuum) {channel="miio:basic:vacuum:total_clean_time"} +Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="miio:basic:vacuum:total_clean_times"} +Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total_clean_area"} +Number save_map_status "Vslam Extend - Save Map Status" (G_vacuum) {channel="miio:basic:vacuum:save_map_status"} +``` + +### MOVA Z500 Robot Vacuum and Mop Cleaner (dreame.vacuum.p2156o) item file lines + +note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_vacuum "MOVA Z500 Robot Vacuum and Mop Cleaner" +String actions "Actions" (G_vacuum) {channel="miio:basic:vacuum:actions"} +Number status "Robot Cleaner - Status" (G_vacuum) {channel="miio:basic:vacuum:status"} +Number fault "Robot Cleaner - Device Fault" (G_vacuum) {channel="miio:basic:vacuum:fault"} +Number mode "Robot Cleaner - Mode" (G_vacuum) {channel="miio:basic:vacuum:mode"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_vacuum) {channel="miio:basic:vacuum:battery_level"} +Number charging_state "Battery - Charging State" (G_vacuum) {channel="miio:basic:vacuum:charging_state"} +Number:Time brush_left_time "Main Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time"} +Number:Dimensionless brush_life_level "Main Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level"} +Number:Time brush_left_time1 "Side Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time1"} +Number:Dimensionless brush_life_level1 "Side Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level1"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_vacuum) {channel="miio:basic:vacuum:filter_life_level"} +Number:Time filter_left_time "Filter - Filter Left Time" (G_vacuum) {channel="miio:basic:vacuum:filter_left_time"} +Number work_mode "Vacuum Extend - Work Mode" (G_vacuum) {channel="miio:basic:vacuum:work_mode"} +Number:Time cleaning_time "Vacuum Extend - Cleaning Time" (G_vacuum) {channel="miio:basic:vacuum:cleaning_time"} +Number:Area cleaning_area "Vacuum Extend - Cleaning Area" (G_vacuum) {channel="miio:basic:vacuum:cleaning-area"} +Number cleaning_mode "Vacuum Extend - Cleaning Mode" (G_vacuum) {channel="miio:basic:vacuum:cleaning_mode"} +Number mop_mode "Vacuum Extend - Mop Mode" (G_vacuum) {channel="miio:basic:vacuum:mop_mode"} +Number waterbox_status "Vacuum Extend - Waterbox Status" (G_vacuum) {channel="miio:basic:vacuum:waterbox_status"} +Number task_status "Vacuum Extend - Task Status" (G_vacuum) {channel="miio:basic:vacuum:task_status"} +Number break_point_restart "Vacuum Extend - Break Point Restart" (G_vacuum) {channel="miio:basic:vacuum:break_point_restart"} +Number carpet_press "Vacuum Extend - Carpet Press" (G_vacuum) {channel="miio:basic:vacuum:carpet_press"} +String serial_number "Vacuum Extend - Serial Number" (G_vacuum) {channel="miio:basic:vacuum:serial_number"} +Number:Time keep_sweeper_time "Vacuum Extend - Keep Sweeper Time" (G_vacuum) {channel="miio:basic:vacuum:keep_sweeper_time"} +String faults "Vacuum Extend - Faults" (G_vacuum) {channel="miio:basic:vacuum:faults"} +Switch enable "Do Not Disturb - Enable" (G_vacuum) {channel="miio:basic:vacuum:enable"} +String start_time "Do Not Disturb - Start Time" (G_vacuum) {channel="miio:basic:vacuum:start_time"} +String end_time "Do Not Disturb - End Time" (G_vacuum) {channel="miio:basic:vacuum:end_time"} +Number:Dimensionless volume "Audio - Volume" (G_vacuum) {channel="miio:basic:vacuum:volume"} +String voice_packet_id "Audio - Voice Packet Id" (G_vacuum) {channel="miio:basic:vacuum:voice_packet_id"} +String voice_change_state "Audio - Voice Change State" (G_vacuum) {channel="miio:basic:vacuum:voice_change_state"} +String time_zone "Time - Time Zone" (G_vacuum) {channel="miio:basic:vacuum:time_zone"} +String timer_clean "Time - Timer Clean" (G_vacuum) {channel="miio:basic:vacuum:timer_clean"} +Number first_clean_time "Clean Logs - First Clean Time" (G_vacuum) {channel="miio:basic:vacuum:first_clean_time"} +Number:Time total_clean_time "Clean Logs - Total Clean Time" (G_vacuum) {channel="miio:basic:vacuum:total_clean_time"} +Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="miio:basic:vacuum:total_clean_times"} +Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total_clean_area"} +Number save_map_status "Vslam Extend - Save Map Status" (G_vacuum) {channel="miio:basic:vacuum:save_map_status"} +``` + +### MOVA L600 Robot Vacuum and Mop Cleaner (dreame.vacuum.p2157) item file lines + +note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_vacuum "MOVA L600 Robot Vacuum and Mop Cleaner" +String vacuumaction "Vacuum Action" (G_vacuum) {channel="miio:basic:vacuum:vacuumaction"} +Number status "Robot Cleaner - Status" (G_vacuum) {channel="miio:basic:vacuum:status"} +Number fault "Robot Cleaner - Device Fault" (G_vacuum) {channel="miio:basic:vacuum:fault"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_vacuum) {channel="miio:basic:vacuum:battery-level"} +Number charging_state "Battery - Charging State" (G_vacuum) {channel="miio:basic:vacuum:charging-state"} +String resetConsumable "Consumables Reset" (G_vacuum) {channel="miio:basic:vacuum:resetConsumable"} +Number:Time brush_left_time "Main Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush-left-time"} +Number:Dimensionless brush_life_level "Main Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush-life-level"} +Number:Time brush_left_time1 "Side Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush-left-time1"} +Number:Dimensionless brush_life_level1 "Side Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush-life-level1"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_vacuum) {channel="miio:basic:vacuum:filter-life-level"} +Number:Time filter_left_time "Filter - Filter Left Time" (G_vacuum) {channel="miio:basic:vacuum:filter-left-time"} +Number work_mode "Vacuum Extend - Work Mode" (G_vacuum) {channel="miio:basic:vacuum:work-mode"} +Number:Time cleaning_time "Vacuum Extend - Cleaning Time" (G_vacuum) {channel="miio:basic:vacuum:cleaning-time"} +Number:Area cleaning_area "Vacuum Extend - Cleaning Area" (G_vacuum) {channel="miio:basic:vacuum:cleaning-area"} +Number cleaning_mode "Vacuum Extend - Cleaning Mode" (G_vacuum) {channel="miio:basic:vacuum:cleaning-mode"} +Number mop_mode "Vacuum Extend - Mop Mode" (G_vacuum) {channel="miio:basic:vacuum:mop-mode"} +Number waterbox_status "Vacuum Extend - Waterbox Status" (G_vacuum) {channel="miio:basic:vacuum:waterbox-status"} +Number task_status "Vacuum Extend - Task Status" (G_vacuum) {channel="miio:basic:vacuum:task-status"} +Number break_point_restart "Vacuum Extend - Break Point Restart" (G_vacuum) {channel="miio:basic:vacuum:break-point-restart"} +Number carpet_press "Vacuum Extend - Carpet Press" (G_vacuum) {channel="miio:basic:vacuum:carpet-press"} +String serial_number1 "Vacuum Extend - Serial Number" (G_vacuum) {channel="miio:basic:vacuum:serial-number1"} +Number:Time clean_rags_tip "Vacuum Extend - Clean Rags Tip" (G_vacuum) {channel="miio:basic:vacuum:clean-rags-tip"} +Number:Time keep_sweeper_time "Vacuum Extend - Keep Sweeper Time" (G_vacuum) {channel="miio:basic:vacuum:keep-sweeper-time"} +String faults "Vacuum Extend - Faults" (G_vacuum) {channel="miio:basic:vacuum:faults"} +Switch enable "Do Not Disturb - Enable" (G_vacuum) {channel="miio:basic:vacuum:enable"} +String start_time "Do Not Disturb - Start Time" (G_vacuum) {channel="miio:basic:vacuum:start-time"} +String end_time "Do Not Disturb - End Time" (G_vacuum) {channel="miio:basic:vacuum:end-time"} +Number:Dimensionless volume "Audio - Volume" (G_vacuum) {channel="miio:basic:vacuum:volume"} +String voice_packet_id "Audio - Voice Packet Id" (G_vacuum) {channel="miio:basic:vacuum:voice-packet-id"} +String voice_change_state "Audio - Voice Change State" (G_vacuum) {channel="miio:basic:vacuum:voice-change-state"} +String time_zone "Time - Time Zone" (G_vacuum) {channel="miio:basic:vacuum:time-zone"} +String timer_clean "Time - Timer Clean" (G_vacuum) {channel="miio:basic:vacuum:timer-clean"} +Number first_clean_time "Clean Logs - First Clean Time" (G_vacuum) {channel="miio:basic:vacuum:first-clean-time"} +Number:Time total_clean_time "Clean Logs - Total Clean Time" (G_vacuum) {channel="miio:basic:vacuum:total-clean-time"} +Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="miio:basic:vacuum:total-clean-times"} +Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total-clean-area"} +``` + ### HUIZUO ARIES For Bedroom (huayi.light.ari013) item file lines note: Autogenerated example. Replace the id (light) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. @@ -6456,7 +7111,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6483,7 +7138,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6510,7 +7165,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6537,7 +7192,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6564,7 +7219,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6591,7 +7246,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6618,7 +7273,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6645,7 +7300,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6672,7 +7327,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6699,7 +7354,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6726,7 +7381,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6753,7 +7408,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6780,7 +7435,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6807,7 +7462,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6834,7 +7489,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6861,7 +7516,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6888,7 +7543,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6915,7 +7570,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6942,7 +7597,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -6969,7 +7624,7 @@ Number:Energy electricity "Power consumption accumulation in kWh" (G_airconditio Number elec_count "Electricity - Count" (G_aircondition) {channel="miio:basic:aircondition:elec-count"} String clean "Maintenance - Clean" (G_aircondition) {channel="miio:basic:aircondition:clean"} String examine "Maintenance - Examine" (G_aircondition) {channel="miio:basic:aircondition:examine"} -Number:Duration running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} +Number:Time running_duration "Maintenance - Running Duration" (G_aircondition) {channel="miio:basic:aircondition:running-duration"} Number:Dimentionless fan_percent "Fan Speed %" (G_aircondition) {channel="miio:basic:aircondition:fan-percent"} String timer "Enhance - Timer" (G_aircondition) {channel="miio:basic:aircondition:timer"} ``` @@ -7155,7 +7810,7 @@ note: Autogenerated example. Replace the id (light) in the channel with your own Group G_light "Yeelight LED Ceiling Light" Switch power "Power" (G_light) {channel="miio:basic:light:power"} Dimmer brightness "Brightness" (G_light) {channel="miio:basic:light:brightness"} -Number ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} +Dimmer ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} Number:Time delayoff "Shutdown Timer" (G_light) {channel="miio:basic:light:delayoff"} Number colorTemperature "Color Temperature" (G_light) {channel="miio:basic:light:colorTemperature"} Number colorMode "Color Mode" (G_light) {channel="miio:basic:light:colorMode"} @@ -7272,7 +7927,7 @@ note: Autogenerated example. Replace the id (light) in the channel with your own Group G_light "Yeelight Crystal Pendant Lamp" Switch power "Power" (G_light) {channel="miio:basic:light:power"} Dimmer brightness "Brightness" (G_light) {channel="miio:basic:light:brightness"} -Number ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} +Dimmer ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} Number:Time delayoff "Shutdown Timer" (G_light) {channel="miio:basic:light:delayoff"} Number colorTemperature "Color Temperature" (G_light) {channel="miio:basic:light:colorTemperature"} Number colorMode "Color Mode" (G_light) {channel="miio:basic:light:colorMode"} @@ -8945,6 +9600,65 @@ Number:Temperature temperature "Temperature" (G_airpurifier) {channel="miio:basi Switch childlock "Child Lock" (G_airpurifier) {channel="miio:basic:airpurifier:childlock"} ``` +### Mi Air Purifier Pro H (zhimi.airpurifier.vb2) item file lines + +note: Autogenerated example. Replace the id (airpurifier) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_airpurifier "Mi Air Purifier Pro H" +String actions "Actions" (G_airpurifier) {channel="miio:basic:airpurifier:actions"} +Number fault "Air Purifier - Device Fault" (G_airpurifier) {channel="miio:basic:airpurifier:fault"} +Switch on "Air Purifier - Power" (G_airpurifier) {channel="miio:basic:airpurifier:on"} +Number fan_level "Air Purifier - Fan Level" (G_airpurifier) {channel="miio:basic:airpurifier:fan_level"} +Number mode "Air Purifier - Mode" (G_airpurifier) {channel="miio:basic:airpurifier:mode"} +Number pm2_5_density "Environment - PM2 5 Density" (G_airpurifier) {channel="miio:basic:airpurifier:pm2_5_density"} +Number:Dimensionless relative_humidity "Environment - Relative Humidity" (G_airpurifier) {channel="miio:basic:airpurifier:relative_humidity"} +Number:Temperature temperature "Environment - Temperature" (G_airpurifier) {channel="miio:basic:airpurifier:temperature"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_airpurifier) {channel="miio:basic:airpurifier:filter_life_level"} +Number:Time filter_used_time "Filter - Filter Used Time" (G_airpurifier) {channel="miio:basic:airpurifier:filter_used_time"} +Switch alarm "Alarm - Alarm" (G_airpurifier) {channel="miio:basic:airpurifier:alarm"} +Number:Dimensionless volume "Alarm - Volume" (G_airpurifier) {channel="miio:basic:airpurifier:volume"} +Number brightness "Indicator Light - Brightness" (G_airpurifier) {channel="miio:basic:airpurifier:brightness"} +Switch on1 "Indicator Light - Switch Status" (G_airpurifier) {channel="miio:basic:airpurifier:on1"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_airpurifier) {channel="miio:basic:airpurifier:physical_controls_locked"} +String button_pressed "Button - Button_pressed" (G_airpurifier) {channel="miio:basic:airpurifier:button_pressed"} +Number:Time filter_max_time "Filter Time - Filter Max Time" (G_airpurifier) {channel="miio:basic:airpurifier:filter_max_time"} +Number:Time filter_hour_used_debug "Filter Time - Filter Hour Used Debug" (G_airpurifier) {channel="miio:basic:airpurifier:filter_hour_used_debug"} +Number m1_strong "Motor Speed - M1 Strong" (G_airpurifier) {channel="miio:basic:airpurifier:m1_strong"} +Number m1_high "Motor Speed - M1 High" (G_airpurifier) {channel="miio:basic:airpurifier:m1_high"} +Number m1_med "Motor Speed - M1 Med" (G_airpurifier) {channel="miio:basic:airpurifier:m1_med"} +Number m1_med_l "Motor Speed - M1 Med L" (G_airpurifier) {channel="miio:basic:airpurifier:m1_med_l"} +Number m1_low "Motor Speed - M1 Low" (G_airpurifier) {channel="miio:basic:airpurifier:m1_low"} +Number m1_silent "Motor Speed - M1 Silent" (G_airpurifier) {channel="miio:basic:airpurifier:m1_silent"} +Number m1_favorite "Motor Speed - M1 Favorite" (G_airpurifier) {channel="miio:basic:airpurifier:m1_favorite"} +Number motor1_speed "Motor Speed - Motor1 Speed" (G_airpurifier) {channel="miio:basic:airpurifier:motor1_speed"} +Number motor1_set_speed "Motor Speed - Motor1 Set Speed" (G_airpurifier) {channel="miio:basic:airpurifier:motor1_set_speed"} +Number favorite_level "Motor Speed - Favorite Level" (G_airpurifier) {channel="miio:basic:airpurifier:favorite_level"} +Number:Time use_time "Use Time - Use Time" (G_airpurifier) {channel="miio:basic:airpurifier:use_time"} +Number purify_volume "Aqi - Purify Volume" (G_airpurifier) {channel="miio:basic:airpurifier:purify_volume"} +Number average_aqi "Aqi - Average Aqi" (G_airpurifier) {channel="miio:basic:airpurifier:average_aqi"} +Number average_aqi_cnt "Aqi - Average_aqi Read Times" (G_airpurifier) {channel="miio:basic:airpurifier:average_aqi_cnt"} +String aqi_zone "Aqi - Aqi Zone" (G_airpurifier) {channel="miio:basic:airpurifier:aqi_zone"} +Number sensor_state "Aqi - Sensor State" (G_airpurifier) {channel="miio:basic:airpurifier:sensor_state"} +Number aqi_goodh "Aqi - Aqi Goodh" (G_airpurifier) {channel="miio:basic:airpurifier:aqi_goodh"} +Number aqi_runstate "Aqi - Runstate" (G_airpurifier) {channel="miio:basic:airpurifier:aqi_runstate"} +Number aqi_state "Aqi - Aqi State" (G_airpurifier) {channel="miio:basic:airpurifier:aqi_state"} +String rfid_tag "Rfid - Rfid Tag" (G_airpurifier) {channel="miio:basic:airpurifier:rfid_tag"} +String rfid_factory_id "Rfid - Rfid Factory Id" (G_airpurifier) {channel="miio:basic:airpurifier:rfid_factory_id"} +String rfid_product_id "Rfid - Rfid Product Id" (G_airpurifier) {channel="miio:basic:airpurifier:rfid_product_id"} +String rfid_time "Rfid - Rfid Time" (G_airpurifier) {channel="miio:basic:airpurifier:rfid_time"} +String rfid_serial_num "Rfid - Rfid Serial Num" (G_airpurifier) {channel="miio:basic:airpurifier:rfid_serial_num"} +Number app_extra "Others - App Extra" (G_airpurifier) {channel="miio:basic:airpurifier:app_extra"} +Number main_channel "Others - Main Channel" (G_airpurifier) {channel="miio:basic:airpurifier:main_channel"} +Number slave_channel "Others - Slave Channel" (G_airpurifier) {channel="miio:basic:airpurifier:slave_channel"} +String cola "Others - Cola" (G_airpurifier) {channel="miio:basic:airpurifier:cola"} +String buttom_door "Others - Buttom Door" (G_airpurifier) {channel="miio:basic:airpurifier:buttom_door"} +Number reboot_cause "Others - Reboot_cause" (G_airpurifier) {channel="miio:basic:airpurifier:reboot_cause"} +Number manual_level "Others - Manual Level" (G_airpurifier) {channel="miio:basic:airpurifier:manual_level"} +Number:duration powertime "Others - Powertime" (G_airpurifier) {channel="miio:basic:airpurifier:powertime"} +Number country_code "Others - Country Code" (G_airpurifier) {channel="miio:basic:airpurifier:country_code"} +``` + ### Mi Standing Fan (zhimi.fan.sa1) item file lines note: Autogenerated example. Replace the id (fan) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. @@ -9102,6 +9816,125 @@ Number naturalLevel "Natural Level" (G_fan) {channel="miio:basic:fan:naturalLeve String move "Move Direction" (G_fan) {channel="miio:basic:fan:move"} ``` +### Smartmi Standing Fan 3 (zhimi.fan.za5) item file lines + +note: Autogenerated example. Replace the id (fan) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_fan "Smartmi Standing Fan 3 " +Switch on "Fan - Power" (G_fan) {channel="miio:basic:fan:on"} +Number fan_level "Fan - Fan Level" (G_fan) {channel="miio:basic:fan:fan_level"} +Switch horizontal_swing "Fan - Horizontal Swing" (G_fan) {channel="miio:basic:fan:horizontal_swing"} +Number horizontal_angle "Fan - Horizontal Angle" (G_fan) {channel="miio:basic:fan:horizontal_angle"} +Number mode "Fan - Mode" (G_fan) {channel="miio:basic:fan:mode"} +Number off_delay "Fan - Power Off Delay" (G_fan) {channel="miio:basic:fan:off_delay"} +Switch anion "Fan - Anion" (G_fan) {channel="miio:basic:fan:anion"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_fan) {channel="miio:basic:fan:physical_controls_locked"} +Number:Dimensionless brightness "Indicator Light - Brightness" (G_fan) {channel="miio:basic:fan:brightness"} +Switch alarm "Alarm - Alarm" (G_fan) {channel="miio:basic:fan:alarm"} +Number:Dimensionless relative_humidity "Environment - Relative Humidity" (G_fan) {channel="miio:basic:fan:relative_humidity"} +Number:Temperature temperature "Environment - Temperature" (G_fan) {channel="miio:basic:fan:temperature"} +Number button_press "Custom Service - Button Press" (G_fan) {channel="miio:basic:fan:button_press"} +Switch battery_state "Custom Service - Battery State" (G_fan) {channel="miio:basic:fan:battery_state"} +Number speed_now "Custom Service - Speed Now" (G_fan) {channel="miio:basic:fan:speed_now"} +Switch ac_state "Custom Service - Ac State" (G_fan) {channel="miio:basic:fan:ac_state"} +Number:Dimensionless speed_level "Custom Service - Speed Level" (G_fan) {channel="miio:basic:fan:speed_level"} +``` + +### Mi Smart Space Heater S (zhimi.heater.ma2) item file lines + +note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_heater "Mi Smart Space Heater S" +Switch on "Heater - Switch Status" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Fault" (G_heater) {channel="miio:basic:heater:fault"} +Number:Temperature target_temperature "Heater - Target Temperature" (G_heater) {channel="miio:basic:heater:target_temperature"} +Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown_time"} +Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_heater) {channel="miio:basic:heater:physical_controls_locked"} +Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} +Number:Dimensionless brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Switch hw_enable "Private Service - Hw Enable" (G_heater) {channel="miio:basic:heater:hw_enable"} +Number:Time use_time "Private Service - Use Time" (G_heater) {channel="miio:basic:heater:use_time"} +``` + +### Mi Smart Baseboard Heater E (zhimi.heater.ma3) item file lines + +note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_heater "Mi Smart Baseboard Heater E" +String actions "Actions" (G_heater) {channel="miio:basic:heater:actions"} +Switch on "Heater - Switch Status" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Fault" (G_heater) {channel="miio:basic:heater:fault"} +Number:Temperature target_temperature "Heater - Target Temperature" (G_heater) {channel="miio:basic:heater:target_temperature"} +Number mode "Heater - Mode" (G_heater) {channel="miio:basic:heater:mode"} +Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown_time"} +Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_heater) {channel="miio:basic:heater:physical_controls_locked"} +Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} +Number:Dimensionless brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Number:Time use_time "Private Service - Use Time" (G_heater) {channel="miio:basic:heater:use_time"} +``` + +### Mi Smart Space Heater S (zhimi.heater.mc2) item file lines + +note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_heater "Mi Smart Space Heater S" +Switch on "Heater - Power" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Device Fault" (G_heater) {channel="miio:basic:heater:fault"} +Number:Temperature target_temperature "Heater - Target Temperature" (G_heater) {channel="miio:basic:heater:target_temperature"} +Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown_time"} +Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_heater) {channel="miio:basic:heater:physical_controls_locked"} +Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} +Number brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Switch hw_enable "Private Service - Hw Enable" (G_heater) {channel="miio:basic:heater:hw_enable"} +Number:Time use_time "Private Service - Use Time" (G_heater) {channel="miio:basic:heater:use_time"} +Number country_code "Private Service - Country Code" (G_heater) {channel="miio:basic:heater:country_code"} +``` + +### Smartmi Smart Fan (zhimi.heater.na1) item file lines + +note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_heater "Smartmi Smart Fan" +Switch on "Heater - Power" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Device Fault" (G_heater) {channel="miio:basic:heater:fault"} +Number heat_level "Heater - Heat Level" (G_heater) {channel="miio:basic:heater:heat_level"} +Number mode "Heater - Mode" (G_heater) {channel="miio:basic:heater:mode"} +Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} +Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown_time"} +Number brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_heater) {channel="miio:basic:heater:physical_controls_locked"} +Switch return_to_middle "Private Service - Return To Middle" (G_heater) {channel="miio:basic:heater:return_to_middle"} +``` + +### Smartmi Smart Fan Heater (zhimi.heater.nb1) item file lines + +note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_heater "Smartmi Smart Fan Heater" +Switch on "Heater - Power" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Device Fault" (G_heater) {channel="miio:basic:heater:fault"} +Number heat_level "Heater - Heat Level" (G_heater) {channel="miio:basic:heater:heat_level"} +Number mode "Heater - Mode" (G_heater) {channel="miio:basic:heater:mode"} +Number:Temperature target_temperature "Heater - Target Temperature" (G_heater) {channel="miio:basic:heater:target_temperature"} +Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} +Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} +Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown_time"} +Number brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Switch physical_controls_locked "Physical Control Locked - Physical Control Locked" (G_heater) {channel="miio:basic:heater:physical_controls_locked"} +Switch return_to_middle "Private Service - Return To Middle" (G_heater) {channel="miio:basic:heater:return_to_middle"} +Number country_code "Private Service - Country Code" (G_heater) {channel="miio:basic:heater:country_code"} +Switch hw_en "Private Service - Hw En" (G_heater) {channel="miio:basic:heater:hw_en"} +``` + ### Smartmi Radiant Heater Smart Version (zhimi.heater.za1) item file lines note: Autogenerated example. Replace the id (heater) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. @@ -9132,7 +9965,7 @@ Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown-time"} Number:Dimensionless relative_humidity "Environment - Relative Humidity" (G_heater) {channel="miio:basic:heater:relative-humidity"} Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} -Dimmer brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Number brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} Switch physical_controls_locked "Physical Control Locked - Physical Controls Locked" (G_heater) {channel="miio:basic:heater:physical-controls-locked"} Number:Time use_time "Private-Service - Use Time" (G_heater) {channel="miio:basic:heater:use-time"} ``` @@ -9143,14 +9976,14 @@ note: Autogenerated example. Replace the id (heater) in the channel with your ow ``` Group G_heater "Smartmi Smart Convector Heater 1S" -Number fault "Heater - Device Fault" (G_heater) {channel="miio:basic:heater:fault"} Switch on "Heater - Power" (G_heater) {channel="miio:basic:heater:on"} +Number fault "Heater - Device Fault" (G_heater) {channel="miio:basic:heater:fault"} Number:Temperature target_temperature "Heater - Target Temperature" (G_heater) {channel="miio:basic:heater:target-temperature"} Switch alarm "Alarm - Alarm" (G_heater) {channel="miio:basic:heater:alarm"} Number:Time countdown_time "Countdown - Countdown Time" (G_heater) {channel="miio:basic:heater:countdown-time"} Number:Dimensionless relative_humidity "Environment - Relative Humidity" (G_heater) {channel="miio:basic:heater:relative-humidity"} Number:Temperature temperature "Environment - Temperature" (G_heater) {channel="miio:basic:heater:temperature"} -Dimmer brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} +Number brightness "Indicator Light - Brightness" (G_heater) {channel="miio:basic:heater:brightness"} Switch physical_controls_locked "Physical Control Locked - Physical Controls Locked" (G_heater) {channel="miio:basic:heater:physical-controls-locked"} Number:Time use_time "Private-Service - Use Time" (G_heater) {channel="miio:basic:heater:use-time"} Number country_code "Private-Service - Country-Code" (G_heater) {channel="miio:basic:heater:country-code"} diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoCommand.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoCommand.java index 447b51d1c5dd6..f16df318f268d 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoCommand.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoCommand.java @@ -86,6 +86,15 @@ public enum MiIoCommand { REMOTE_END("app_rc_end"), REMOTE_MOVE("app_rc_move"), + GET_MAP_STATUS("get_map_status"), + GET_SEGMENT_STATUS("get_segment_status"), + GET_LED_STATUS("get_led_status"), + GET_CARPET_MODE("get_carpet_mode"), + GET_FW_FEATURES("get_fw_features"), + GET_CUSTOMIZED_CLEAN_MODE("get_customize_clean_mode"), + GET_MULTI_MAP_LIST("get_multi_maps_list"), + GET_ROOM_MAPPING("get_room_mapping"), + UNKNOWN(""); private final String command; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java index b3c224629f294..16f871fc5f3b6 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java @@ -25,6 +25,7 @@ @NonNullByDefault public enum MiIoDevices { AUX_AIRCONDITION_V1("aux.aircondition.v1", "AUX Smart Air Conditioner", THING_TYPE_UNSUPPORTED), + CGLLC_AIRM_CGDN1("cgllc.airm.cgdn1", "Qingping Air Monitor Lite", THING_TYPE_BASIC), CGLLC_AIRMONITOR_B1("cgllc.airmonitor.b1", "Mi Multifunction Air Monitor", THING_TYPE_BASIC), CGLLC_AIRMONITOR_S1("cgllc.airmonitor.s1", "Qingping Air Monitor", THING_TYPE_BASIC), CHUANGMI_IR_V2("chuangmi.ir.v2", "Mi Universal Remote", THING_TYPE_UNSUPPORTED), @@ -45,6 +46,8 @@ public enum MiIoDevices { CHUNMI_COOKER_PRESS1("chunmi.cooker.press1", "Mi IH Pressure Rice Cooker", THING_TYPE_UNSUPPORTED), CHUNMI_COOKER_PRESS2("chunmi.cooker.press2", "Mi IH Pressure Rice Cooker", THING_TYPE_UNSUPPORTED), CUCO_PLUG_CP1("cuco.plug.cp1", "Gosund Smart Plug", THING_TYPE_BASIC), + DEERMA_HUMIDIFIER_JSQ("deerma.humidifier.jsq", "Mi Smart Antibacterial Humidifier", THING_TYPE_BASIC), + DEERMA_HUMIDIFIER_JSQ1("deerma.humidifier.jsq1", "Mi S Smart humidifer ", THING_TYPE_BASIC), DEERMA_HUMIDIFIER_MJJSQ("deerma.humidifier.mjjsq", "Mi Smart Humidifier", THING_TYPE_BASIC), DMAKER_AIRFRESH_A1("dmaker.airfresh.a1", "Mi Fresh Air Ventilator A1-150", THING_TYPE_BASIC), DMAKER_AIRFRESH_T2017("dmaker.airfresh.t2017", "Mi Fresh Air Ventilator", THING_TYPE_BASIC), @@ -56,6 +59,10 @@ public enum MiIoDevices { DREAME_VACUUM_MC1808("dreame.vacuum.mc1808", "Mi Robot Vacuum Mop 1C STYTJ01ZHM", THING_TYPE_BASIC), DREAME_VACUUM_P2008("dreame.vacuum.p2008", "Dreame Robot Vacuum-Mop F9", THING_TYPE_BASIC), DREAME_VACUUM_P2009("dreame.vacuum.p2009", "Dreame Robot Vacuum D9 ", THING_TYPE_BASIC), + DREAME_VACUUM_P2036("dreame.vacuum.p2036", "Trouver Robot LDS Vacuum-Mop Finder", THING_TYPE_BASIC), + DREAME_VACUUM_P2041O("dreame.vacuum.p2041o", "Mi Robot Vacuum-Mop 2 Pro+", THING_TYPE_BASIC), + DREAME_VACUUM_P2156O("dreame.vacuum.p2156o", "MOVA Z500 Robot Vacuum and Mop Cleaner", THING_TYPE_BASIC), + DREAME_VACUUM_P2157("dreame.vacuum.p2157", "MOVA L600 Robot Vacuum and Mop Cleaner", THING_TYPE_BASIC), HUAYI_LIGHT_ARI013("huayi.light.ari013", "HUIZUO ARIES For Bedroom", THING_TYPE_BASIC), HUAYI_LIGHT_ARIES("huayi.light.aries", "HUIZUO ARIES For Living Room", THING_TYPE_BASIC), HUAYI_LIGHT_FANWY("huayi.light.fanwy", "HUIZUO Fan Light", THING_TYPE_BASIC), @@ -338,6 +345,7 @@ public enum MiIoDevices { ZHIMI_AIRPURIFIER_V5("zhimi.airpurifier.v5", "Mi Air Purifier v5", THING_TYPE_BASIC), ZHIMI_AIRPURIFIER_V6("zhimi.airpurifier.v6", "Mi Air Purifier Pro v6", THING_TYPE_BASIC), ZHIMI_AIRPURIFIER_V7("zhimi.airpurifier.v7", "Mi Air Purifier Pro v7", THING_TYPE_BASIC), + ZHIMI_AIRPURIFIER_VB2("zhimi.airpurifier.vb2", "Mi Air Purifier Pro H", THING_TYPE_BASIC), ZHIMI_AIRPURIFIER_VIRTUAL("zhimi.airpurifier.virtual", "Mi Air Purifier virtual", THING_TYPE_UNSUPPORTED), ZHIMI_AIRPURIFIER_VTL_M1("zhimi.airpurifier.vtl_m1", "Mi Air Purifier 2(Virtual)", THING_TYPE_UNSUPPORTED), ZHIMI_FAN_SA1("zhimi.fan.sa1", "Mi Standing Fan", THING_TYPE_BASIC), @@ -347,6 +355,12 @@ public enum MiIoDevices { ZHIMI_FAN_ZA1("zhimi.fan.za1", "Smartmi Inverter Pedestal Fan", THING_TYPE_BASIC), ZHIMI_FAN_ZA3("zhimi.fan.za3", "Smartmi Standing Fan 2", THING_TYPE_BASIC), ZHIMI_FAN_ZA4("zhimi.fan.za4", "Smartmi Standing Fan 2S", THING_TYPE_BASIC), + ZHIMI_FAN_ZA5("zhimi.fan.za5", "Smartmi Standing Fan 3 ", THING_TYPE_BASIC), + ZHIMI_HEATER_MA2("zhimi.heater.ma2", "Mi Smart Space Heater S", THING_TYPE_BASIC), + ZHIMI_HEATER_MA3("zhimi.heater.ma3", "Mi Smart Baseboard Heater E", THING_TYPE_BASIC), + ZHIMI_HEATER_MC2("zhimi.heater.mc2", "Mi Smart Space Heater S", THING_TYPE_BASIC), + ZHIMI_HEATER_NA1("zhimi.heater.na1", "Smartmi Smart Fan", THING_TYPE_BASIC), + ZHIMI_HEATER_NB1("zhimi.heater.nb1", "Smartmi Smart Fan Heater", THING_TYPE_BASIC), ZHIMI_HEATER_ZA1("zhimi.heater.za1", "Smartmi Radiant Heater Smart Version", THING_TYPE_BASIC), ZHIMI_HEATER_ZA2("zhimi.heater.za2", "Smartmi Smart Convector Heater 1S", THING_TYPE_BASIC), ZHIMI_HEATER_ZB1("zhimi.heater.zb1", "Smartmi Smart Convector Heater 1S", THING_TYPE_BASIC), diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java index 129ce0a6b8277..08fa2c001702f 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java @@ -15,6 +15,8 @@ import static org.openhab.binding.miio.internal.MiIoBindingConstants.*; import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -35,7 +37,10 @@ import org.openhab.core.thing.type.ChannelTypeRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link MiIoHandlerFactory} is responsible for creating things and thing @@ -49,17 +54,20 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory { private static final String THING_HANDLER_THREADPOOL_NAME = "thingHandler"; protected final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(THING_HANDLER_THREADPOOL_NAME); - private MiIoDatabaseWatchService miIoDatabaseWatchService; private CloudConnector cloudConnector; private ChannelTypeRegistry channelTypeRegistry; private BasicChannelTypeProvider basicChannelTypeProvider; + private @Nullable Future scheduledTask; + private final Logger logger = LoggerFactory.getLogger(MiIoHandlerFactory.class); @Activate public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry, @Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector, @Reference BasicChannelTypeProvider basicChannelTypeProvider, Map properties) { this.miIoDatabaseWatchService = miIoDatabaseWatchService; + this.channelTypeRegistry = channelTypeRegistry; + this.basicChannelTypeProvider = basicChannelTypeProvider; this.cloudConnector = cloudConnector; @Nullable String username = (String) properties.get("username"); @@ -68,9 +76,23 @@ public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry, @Nullable String country = (String) properties.get("country"); cloudConnector.setCredentials(username, password, country); - scheduler.submit(() -> cloudConnector.isConnected()); - this.channelTypeRegistry = channelTypeRegistry; - this.basicChannelTypeProvider = basicChannelTypeProvider; + try { + if (!scheduler.isShutdown()) { + scheduledTask = scheduler.submit(() -> cloudConnector.isConnected()); + } else { + logger.debug("Unexpected: ScheduledExecutorService is shutdown."); + } + } catch (RejectedExecutionException e) { + logger.debug("Unexpected: ScheduledExecutorService task rejected.", e); + } + } + + @Deactivate + private void dispose() { + final Future scheduledTask = this.scheduledTask; + if (scheduledTask != null && !scheduledTask.isDone()) { + scheduledTask.cancel(true); + } } @Override diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java index 5a07f8f746bb6..9b931a91c2a29 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java @@ -47,6 +47,7 @@ public enum MiIoQuantiyTypes { AMPERE(Units.AMPERE), MILLI_AMPERE(MILLI(Units.AMPERE), "mA"), VOLT(Units.VOLT), + MILLI_VOLT(MILLI(Units.VOLT), "mV"), WATT(Units.WATT), LITRE(Units.LITRE, "liter"), LUX(Units.LUX), @@ -55,7 +56,7 @@ public enum MiIoQuantiyTypes { SQUARE_METRE(SIUnits.SQUARE_METRE, "square_meter", "squaremeter"), PERCENT(Units.PERCENT, "percentage"), KGM3(Units.KILOGRAM_PER_CUBICMETRE, "kilogram_per_cubicmeter"), - UGM3(Units.MICROGRAM_PER_CUBICMETRE, "microgram_per_cubicmeter"), + UGM3(Units.MICROGRAM_PER_CUBICMETRE, "microgram_per_cubicmeter", "μg/m3"), M3(SIUnits.CUBIC_METRE, "cubic_meter", "cubic_metre"), LITER(Units.LITRE, "L", "litre"), PPM(Units.PARTS_PER_MILLION, "parts_per_million"); diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java index 9cc5c418a464f..00d141f628b4a 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java @@ -98,10 +98,9 @@ public static String obfuscateToken(String tokenString) { public static JsonObject convertFileToJSON(URL fileName) throws JsonIOException, JsonSyntaxException, JsonParseException, IOException, URISyntaxException, NoSuchFileException { JsonObject jsonObject = new JsonObject(); - JsonParser parser = new JsonParser(); try (InputStream inputStream = fileName.openStream(); InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - JsonElement jsonElement = parser.parse(reader); + JsonElement jsonElement = JsonParser.parseReader(reader); jsonObject = jsonElement.getAsJsonObject(); return jsonObject; } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/CommandParameterType.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/CommandParameterType.java index c2a26b77012d5..7a379a581dda9 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/CommandParameterType.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/CommandParameterType.java @@ -27,6 +27,7 @@ public enum CommandParameterType { ONOFFPARA("onoffpara"), ONOFFBOOL("onoffbool"), ONOFFBOOLSTRING("onoffboolstring"), + ONOFFNUMBER("onoffnumber"), STRING("string"), CUSTOMSTRING("customstring"), NUMBER("number"), diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudLoginDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudLoginDTO.java index 859e78eb9bf17..712a533a5a0f4 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudLoginDTO.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudLoginDTO.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.miio.internal.cloud; -import org.jetbrains.annotations.NotNull; - import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; @@ -64,23 +62,23 @@ public class CloudLoginDTO { @Expose private Object captchaUrl; - public @NotNull String getSsecurity() { + public String getSsecurity() { return ssecurity != null ? ssecurity : ""; } - public @NotNull String getUserId() { + public String getUserId() { return userId != null ? userId : ""; } - public @NotNull String getcUserId() { + public String getcUserId() { return cUserId != null ? cUserId : ""; } - public @NotNull String getPassToken() { + public String getPassToken() { return passToken != null ? passToken : ""; } - public @NotNull String getLocation() { + public String getLocation() { return location != null ? location : ""; } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/MiCloudConnector.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/MiCloudConnector.java index cd076a9d5dde1..3ecabee39c9f1 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/MiCloudConnector.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/MiCloudConnector.java @@ -74,7 +74,6 @@ public class MiCloudConnector { private static final TimeZone TZ = TimeZone.getDefault(); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("OOOO"); private static final Gson GSON = new GsonBuilder().serializeNulls().create(); - private static final JsonParser PARSER = new JsonParser(); private final String clientId; @@ -156,7 +155,7 @@ public String getMapUrl(String vacuumMap, String country) throws MiCloudExceptio logger.trace("response: {}", mapResponse); String errorMsg = ""; try { - JsonElement response = PARSER.parse(mapResponse); + JsonElement response = JsonParser.parseString(mapResponse); if (response.isJsonObject()) { logger.debug("Received JSON message {}", response); if (response.getAsJsonObject().has("result") @@ -210,7 +209,7 @@ public List getDevices(String country) { final String response = getDeviceString(country); List devicesList = new ArrayList<>(); try { - final JsonElement resp = PARSER.parse(response); + final JsonElement resp = JsonParser.parseString(response); if (resp.isJsonObject()) { final JsonObject jor = resp.getAsJsonObject(); if (jor.has("result")) { @@ -413,7 +412,7 @@ private String loginStep1() throws InterruptedException, TimeoutException, Execu logger.trace("Xiaomi Login step 1 content response= {}", content); logger.trace("Xiaomi Login step 1 response = {}", responseStep1); try { - JsonElement resp = new JsonParser().parse(parseJson(content)); + JsonElement resp = JsonParser.parseString(parseJson(content)); if (resp.isJsonObject() && resp.getAsJsonObject().has("_sign")) { String sign = resp.getAsJsonObject().get("_sign").getAsString(); logger.trace("Xiaomi Login step 1 sign = {}", sign); @@ -457,7 +456,7 @@ private String loginStep2(String sign) throws MiIoCryptoException, InterruptedEx logger.trace("Xiaomi login step 2 response = {}", responseStep2); logger.trace("Xiaomi login step 2 content = {}", content2); - JsonElement resp2 = new JsonParser().parse(parseJson(content2)); + JsonElement resp2 = JsonParser.parseString(parseJson(content2)); CloudLoginDTO jsonResp = GSON.fromJson(resp2, CloudLoginDTO.class); if (jsonResp == null) { throw new MiCloudException("Error getting logon details from step 2: " + content2); diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java index bdd6106fbd278..b38208451c01c 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java @@ -58,7 +58,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; /** * The {@link MiIoAbstractHandler} is responsible for handling commands, which are @@ -76,7 +75,6 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi protected MiIoDevices miDevice = MiIoDevices.UNKNOWN; protected boolean isIdentified; - protected final JsonParser parser = new JsonParser(); protected byte[] token = new byte[0]; protected @Nullable MiIoBindingConfiguration configuration; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java index bb7731f477ef2..cefd4469be068 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java @@ -73,6 +73,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; @@ -196,6 +197,8 @@ public void handleCommand(ChannelUID channelUID, Command receivedCommand) { value = new JsonPrimitive(boolCommand); } else if (paramType == CommandParameterType.ONOFFBOOLSTRING) { value = new JsonPrimitive(command == OnOffType.ON ? "true" : "false"); + } else if (paramType == CommandParameterType.ONOFFNUMBER) { + value = new JsonPrimitive(command == OnOffType.ON ? 1 : 0); } } else if (command instanceof DecimalType) { value = new JsonPrimitive(((DecimalType) command).toBigDecimal()); @@ -503,7 +506,8 @@ private boolean buildChannelStructure(String deviceName) { private void updatePropsFromJsonArray(MiIoSendCommand response) { JsonArray res = response.getResult().getAsJsonArray(); - JsonArray para = parser.parse(response.getCommandString()).getAsJsonObject().get("params").getAsJsonArray(); + JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params") + .getAsJsonArray(); if (res.size() != para.size()) { logger.debug("Unexpected size different. Request size {}, response size {}. (Req: {}, Resp:{})", para.size(), res.size(), para, res); @@ -570,8 +574,13 @@ private void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param updateState(basicChannel.getChannel(), new StringType(val.getAsString())); break; case "switch": - updateState(basicChannel.getChannel(), val.getAsString().toLowerCase().equals("on") - || val.getAsString().toLowerCase().equals("true") ? OnOffType.ON : OnOffType.OFF); + if (val.getAsJsonPrimitive().isNumber()) { + updateState(basicChannel.getChannel(), val.getAsInt() > 0 ? OnOffType.ON : OnOffType.OFF); + } else { + String strVal = val.getAsString().toLowerCase(); + updateState(basicChannel.getChannel(), + strVal.equals("on") || strVal.equals("true") ? OnOffType.ON : OnOffType.OFF); + } break; case "color": Color rgb = new Color(val.getAsInt()); diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java index 3b3eba803aa5d..30fe0fbe84422 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java @@ -22,10 +22,14 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Collections; import java.util.Date; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.imageio.ImageIO; @@ -89,6 +93,12 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler { private static final Gson GSON = new GsonBuilder().serializeNulls().create(); private final ChannelUID mapChannelUid; + private static final Set FEATURES_CHANNELS = Collections.unmodifiableSet(Stream + .of(RobotCababilities.SEGMENT_STATUS, RobotCababilities.MAP_STATUS, RobotCababilities.LED_STATUS, + RobotCababilities.CARPET_MODE, RobotCababilities.FW_FEATURES, RobotCababilities.ROOM_MAPPING, + RobotCababilities.MULTI_MAP_LIST, RobotCababilities.CUSTOMIZE_CLEAN_MODE) + .collect(Collectors.toSet())); + private ExpiringCache status; private ExpiringCache consumables; private ExpiringCache dnd; @@ -289,6 +299,7 @@ private boolean updateVacuumStatus(JsonObject statusData) { safeUpdateState(CHANNEL_IN_CLEANING, statusInfo.getInCleaning()); safeUpdateState(CHANNEL_MAP_PRESENT, statusInfo.getMapPresent()); if (statusInfo.getState() != null) { + stateId = statusInfo.getState(); StatusType state = StatusType.getType(statusInfo.getState()); updateState(CHANNEL_STATE, new StringType(state.getDescription())); updateState(CHANNEL_STATE_ID, new DecimalType(statusInfo.getState())); @@ -464,6 +475,11 @@ protected synchronized void updateData() { map.getValue(); } } + for (RobotCababilities cmd : FEATURES_CHANNELS) { + if (isLinked(cmd.getChannel())) { + sendCommand(cmd.getCommand()); + } + } } catch (Exception e) { logger.debug("Error while updating '{}': '{}", getThing().getUID().toString(), e.getLocalizedMessage()); } @@ -530,11 +546,54 @@ public void onMessageReceived(MiIoSendCommand response) { } } break; + case GET_MAP_STATUS: + case GET_SEGMENT_STATUS: + case GET_LED_STATUS: + updateNumericChannel(response); + break; + case GET_CARPET_MODE: + case GET_FW_FEATURES: + case GET_CUSTOMIZED_CLEAN_MODE: + case GET_MULTI_MAP_LIST: + case GET_ROOM_MAPPING: + for (RobotCababilities cmd : FEATURES_CHANNELS) { + if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) { + updateState(cmd.getChannel(), new StringType(response.getResult().toString())); + break; + } + } + break; default: break; } } + private void updateNumericChannel(MiIoSendCommand response) { + RobotCababilities capabilityChannel = null; + for (RobotCababilities cmd : FEATURES_CHANNELS) { + if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) { + capabilityChannel = cmd; + break; + } + } + if (capabilityChannel != null) { + if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().get(0).isJsonPrimitive()) { + try { + Integer stat = response.getResult().getAsJsonArray().get(0).getAsInt(); + updateState(capabilityChannel.getChannel(), new DecimalType(stat)); + return; + } catch (ClassCastException | IllegalStateException e) { + logger.debug("Could not update numeric channel {} with '{}': {}", capabilityChannel.getChannel(), + response.getResult(), e.getMessage()); + } + } else { + logger.debug("Could not update numeric channel {} with '{}': Not in expected format", + capabilityChannel.getChannel(), response.getResult()); + } + updateState(capabilityChannel.getChannel(), UnDefType.UNDEF); + } + } + private void setCapabilities(JsonObject statusResponse) { for (RobotCababilities capability : RobotCababilities.values()) { if (statusResponse.has(capability.getStatusFieldName())) { diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RobotCababilities.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RobotCababilities.java index 2d8d5dc339a62..d30eca08c4d32 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RobotCababilities.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RobotCababilities.java @@ -23,23 +23,33 @@ @NonNullByDefault public enum RobotCababilities { - WATERBOX_STATUS("water_box_status", "status#water_box_status", "miio:water_box_status"), - LOCKSTATUS("lock_status", "status#lock_status", "miio:lock_status"), - WATERBOX_MODE("water_box_mode", "status#water_box_mode", "miio:water_box_mode"), - WATERBOX_CARRIAGE("water_box_carriage_status", "status#water_box_carriage_status", - "miio:water_box_carriage_status"), - MOP_FORBIDDEN("mop_forbidden_enable", "status#mop_forbidden_enable", "miio:mop_forbidden_enable"), - LOCATING("is_locating", "status#is_locating", "miio:is_locating"), - SEGMENT_CLEAN("", "actions#segment", "miio:segment"); + WATERBOX_STATUS("water_box_status", "status#water_box_status", "miio:water_box_status", ""), + LOCKSTATUS("lock_status", "status#lock_status", "miio:lock_status", ""), + WATERBOX_MODE("water_box_mode", "status#water_box_mode", "miio:water_box_mode", ""), + WATERBOX_CARRIAGE("water_box_carriage_status", "status#water_box_carriage_status", "miio:water_box_carriage_status", + ""), + MOP_FORBIDDEN("mop_forbidden_enable", "status#mop_forbidden_enable", "miio:mop_forbidden_enable", ""), + LOCATING("is_locating", "status#is_locating", "miio:is_locating", ""), + SEGMENT_STATUS("", "status#segment_status", "miio:segment_status", "get_segment_status"), + MAP_STATUS("", "status#map_status", "miio:map_status", "get_map_status"), + LED_STATUS("", "status#led_status", "miio:led_status", "get_led_status"), + CARPET_MODE("", "info#carpet_mode", "miio:carpet_mode", "get_carpet_mode"), + FW_FEATURES("", "info#fw_features", "miio:fw_features", "get_fw_features"), + ROOM_MAPPING("", "info#room_mapping", "miio:room_mapping", "get_room_mapping"), + MULTI_MAP_LIST("", "info#multi_maps_list", "miio:multi_maps_list", "get_multi_maps_list"), + CUSTOMIZE_CLEAN_MODE("", "info#customize_clean_mode", "miio:customize_clean_mode", "get_customize_clean_mode"), + SEGMENT_CLEAN("", "actions#segment", "miio:segment", ""); private final String statusFieldName; private final String channel; private final String channelType; + private final String command; - RobotCababilities(String statusKey, String channel, String channelType) { + RobotCababilities(String statusKey, String channel, String channelType, String command) { this.statusFieldName = statusKey; this.channel = channel; this.channelType = channelType; + this.command = command; } public String getStatusFieldName() { @@ -54,9 +64,14 @@ public ChannelTypeUID getChannelType() { return new ChannelTypeUID(channelType); } + public String getCommand() { + return command; + } + @Override public String toString() { - return String.format("Capability %s: status field name: '%s', channel: '%s', channeltype: '%s'.", this.name(), - statusFieldName, channel, channelType); + return String.format("Capability %s: status field name: '%s', channel: '%s', channeltype: '%s'%s%s.", + this.name(), statusFieldName, channel, channelType, command.isBlank() ? "" : ", custom command: ", + command); } } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java index 22cc0747ec94a..f4ebe2ce2739b 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java @@ -73,7 +73,6 @@ public class MiIoAsyncCommunication { private AtomicInteger id = new AtomicInteger(-1); private int timeDelta; private int timeStamp; - private final JsonParser parser; private @Nullable MessageSenderThread senderThread; private boolean connected; private ThingStatusDetail status = ThingStatusDetail.NONE; @@ -94,7 +93,6 @@ public MiIoAsyncCommunication(String ip, byte[] token, byte[] did, int id, int t this.timeout = timeout; this.cloudConnector = cloudConnector; setId(id); - parser = new JsonParser(); startReceiver(); } @@ -150,7 +148,7 @@ public int queueCommand(String command, String params, String cloudServer) } fullCommand.addProperty("id", cmdId); fullCommand.addProperty("method", command); - fullCommand.add("params", parser.parse(params)); + fullCommand.add("params", JsonParser.parseString(params)); MiIoSendCommand sendCmd = new MiIoSendCommand(cmdId, MiIoCommand.getCommand(command), fullCommand, cloudServer); concurrentLinkedQueue.add(sendCmd); @@ -188,7 +186,7 @@ MiIoSendCommand sendMiIoSendCommand(MiIoSendCommand miIoSendCommand) { // hack due to avoid invalid json errors from some misbehaving device firmwares decryptedResponse = decryptedResponse.replace(",,", ","); JsonElement response; - response = parser.parse(decryptedResponse); + response = JsonParser.parseString(decryptedResponse); if (!response.isJsonObject()) { errorMsg = "Received message is not a JSON object "; } else { diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml index 4950a52f0d4aa..3eaad8d934352 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml @@ -6,22 +6,19 @@ Binding for Xiaomi Mi IO devices like Mi Robot Vacuum - + Xiaomi cloud username. Typically your email - false - + - false - + Xiaomi server country(s) (e.g. sg,de). Separate multiple servers with comma. Leave empty for all. See binding readme for country to server mapping - false - + disabled Allow for discovery via the cloud. This may be used for devices that are not on the same network as @@ -31,7 +28,6 @@ - false diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/vacuumThing.xml b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/vacuumThing.xml index caee93a44ecf4..c9e5e713eedaa 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/vacuumThing.xml +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/vacuumThing.xml @@ -15,6 +15,7 @@ + @@ -50,6 +51,9 @@ + + + @@ -95,6 +99,16 @@ + + + + + + + + + + @@ -152,6 +166,21 @@ + + Number + + + + + Number + + + + + Number + + + @@ -358,4 +387,29 @@ + + + + String + + + + + String + + + + + String + + + + + String + + + + String + + diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/cgllc.airm.cgdn1-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/cgllc.airm.cgdn1-miot.json new file mode 100644 index 0000000000000..ae9bc7fa3e8f7 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/cgllc.airm.cgdn1-miot.json @@ -0,0 +1,451 @@ +{ + "deviceMapping": { + "id": [ + "cgllc.airm.cgdn1" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "", + "friendlyName": "Actions", + "channel": "actions", + "type": "String", + "stateDescription": { + "options": [ + { + "value": "settings-set-start-time", + "label": "Set Start Time" + }, + { + "value": "settings-set-end-time", + "label": "Set End Time" + }, + { + "value": "settings-set-frequency", + "label": "Set Frequency" + }, + { + "value": "settings-set-screen-off", + "label": "Set Screen Off" + }, + { + "value": "settings-set-device-off", + "label": "Set Device Off" + }, + { + "value": "settings-set-temp-unit", + "label": "Set Temp Unit" + } + ] + }, + "actions": [ + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 2.0 + ], + "siid": 9, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-start-time" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 3.0 + ], + "siid": 9, + "aiid": 3, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-end-time" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 4.0 + ], + "siid": 9, + "aiid": 4, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-frequency" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 5.0 + ], + "siid": 9, + "aiid": 5, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-screen-off" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 6.0 + ], + "siid": 9, + "aiid": 6, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-device-off" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 7.0 + ], + "siid": 9, + "aiid": 7, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "settings-set-temp-unit" + } + ] + } + } + ], + "readmeComment": "Value mapping [\"settings-set-start-time\"\u003d\"Set Start Time\",\"settings-set-end-time\"\u003d\"Set End Time\",\"settings-set-frequency\"\u003d\"Set Frequency\",\"settings-set-screen-off\"\u003d\"Set Screen Off\",\"settings-set-device-off\"\u003d\"Set Device Off\",\"settings-set-temp-unit\"\u003d\"Set Temp Unit\"]" + }, + { + "property": "relative-humidity", + "siid": 3, + "piid": 1, + "friendlyName": "Environment - Relative Humidity", + "channel": "relative_humidity", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "pm2.5-density", + "siid": 3, + "piid": 4, + "friendlyName": "Environment - PM2 5 Density", + "channel": "pm2_5_density", + "type": "Number:Density", + "unit": "μg/m3", + "stateDescription": { + "minimum": 0, + "maximum": 1000, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "pm10-density", + "siid": 3, + "piid": 5, + "friendlyName": "Environment - PM10 Density", + "channel": "pm10_density", + "type": "Number:Density", + "unit": "μg/m3", + "stateDescription": { + "minimum": 0, + "maximum": 1000, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "temperature", + "siid": 3, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "co2-density", + "siid": 3, + "piid": 8, + "friendlyName": "Environment - CO2 Density", + "channel": "co2_density", + "type": "Number:Density", + "unit": "ppm", + "stateDescription": { + "minimum": 0, + "maximum": 9999, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "battery-level", + "siid": 4, + "piid": 1, + "friendlyName": "Battery - Battery Level", + "channel": "battery_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "charging-state", + "siid": 4, + "piid": 2, + "friendlyName": "Battery - Charging State", + "channel": "charging_state", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "Charging" + }, + { + "value": "2", + "label": "Not charging" + }, + { + "value": "3", + "label": "Not chargeable" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"Charging\",\"2\"\u003d\"Not charging\",\"3\"\u003d\"Not chargeable\"]" + }, + { + "property": "voltage", + "siid": 4, + "piid": 3, + "friendlyName": "Battery - Voltage", + "channel": "voltage", + "type": "Number:ElectricPotential", + "unit": "mV", + "stateDescription": { + "minimum": 0, + "maximum": 65535, + "step": 1, + "pattern": "%.2f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "mac", + "siid": 8, + "piid": 1, + "friendlyName": "Mac - Mac", + "channel": "mac", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "monitoring-frequency", + "siid": 9, + "piid": 4, + "friendlyName": "Settings - Monitoring Frequency", + "channel": "monitoring_frequency", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Second" + }, + { + "value": "60", + "label": "Second" + }, + { + "value": "300", + "label": "Second" + }, + { + "value": "600", + "label": "Second" + }, + { + "value": "0", + "label": "Null" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"Second\",\"60\"\u003d\"Second\",\"300\"\u003d\"Second\",\"600\"\u003d\"Second\",\"0\"\u003d\"Null\"]" + }, + { + "property": "screen-off", + "siid": 9, + "piid": 5, + "friendlyName": "Settings - Screen Off", + "channel": "screen_off", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "options": [ + { + "value": "15", + "label": "Second" + }, + { + "value": "30", + "label": "Second" + }, + { + "value": "60", + "label": "Second" + }, + { + "value": "300", + "label": "Second" + }, + { + "value": "0", + "label": "Null" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"15\"\u003d\"Second\",\"30\"\u003d\"Second\",\"60\"\u003d\"Second\",\"300\"\u003d\"Second\",\"0\"\u003d\"Null\"]" + }, + { + "property": "device-off", + "siid": 9, + "piid": 6, + "friendlyName": "Settings - Device Off", + "channel": "device_off", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "options": [ + { + "value": "15", + "label": "Minute" + }, + { + "value": "30", + "label": "Minute" + }, + { + "value": "60", + "label": "Minute" + }, + { + "value": "0", + "label": "Null" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"15\"\u003d\"Minute\",\"30\"\u003d\"Minute\",\"60\"\u003d\"Minute\",\"0\"\u003d\"Null\"]" + }, + { + "property": "tempature-unit", + "siid": 9, + "piid": 7, + "friendlyName": "Settings - Tempature Unit", + "channel": "tempature_unit", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + } + ], + "readmeComment": "Identified manual actions for execution\u003cbr /\u003e`action{\"did\":\"settings-set-start-time\",\"siid\":9,\"aiid\":2,\"in\":[2.0]}`\u003cbr /\u003e`action{\"did\":\"settings-set-end-time\",\"siid\":9,\"aiid\":3,\"in\":[3.0]}`\u003cbr /\u003e`action{\"did\":\"settings-set-frequency\",\"siid\":9,\"aiid\":4,\"in\":[4.0]}`\u003cbr /\u003e`action{\"did\":\"settings-set-screen-off\",\"siid\":9,\"aiid\":5,\"in\":[5.0]}`\u003cbr /\u003e`action{\"did\":\"settings-set-device-off\",\"siid\":9,\"aiid\":6,\"in\":[6.0]}`\u003cbr /\u003e`action{\"did\":\"settings-set-temp-unit\",\"siid\":9,\"aiid\":7,\"in\":[7.0]}`\u003cbr /\u003ePlease test and feedback if they are working to they can be linked to a channel." + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json index 61b9dec71b05f..4827ef6cd6df4 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json @@ -54,7 +54,7 @@ "piid": 7, "friendlyName": "Working Time", "channel": "working-time", - "type": "Number:Duration", + "type": "Number:Time", "unit": "minutes", "stateDescription": { "minimum": 0, @@ -184,7 +184,7 @@ "piid": 1, "friendlyName": "Imilab Timer - On Duration", "channel": "on-duration", - "type": "Number:Duration", + "type": "Number:Time", "unit": "seconds", "stateDescription": { "minimum": 0, @@ -207,7 +207,7 @@ "piid": 2, "friendlyName": "Imilab Timer - Off Duration", "channel": "off-duration", - "type": "Number:Duration", + "type": "Number:Time", "unit": "seconds", "stateDescription": { "minimum": 0, diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3.json b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3.json index 1316189f2bab4..fbc7759e4d011 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3.json @@ -1,7 +1,9 @@ { "deviceMapping": { "id": [ - "chuangmi.plug.v3old" + "chuangmi.plug.v3", + "chuangmi.plug.hmi206", + "chuangmi.plug.hmi208" ], "channels": [ { @@ -13,14 +15,15 @@ "refresh": true, "actions": [ { - "command": "set_*", - "parameterType": "ONOFFPARA" + "command": "set_power", + "parameterType": "ONOFF" } ], "category": "switch", "tags": [ "Switch" - ] + ], + "readmeComment": "If this channel does not respond to on/off upgrade firmware" }, { "property": "usb_on", diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3fw.json b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3fw.json deleted file mode 100644 index 1999d01a2bcf6..0000000000000 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.v3fw.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "deviceMapping": { - "id": [ - "chuangmi.plug.v3", - "chuangmi.plug.v3fw", - "chuangmi.plug.hmi206", - "chuangmi.plug.hmi208" - ], - "channels": [ - { - "property": "power", - "friendlyName": "Power", - "channel": "power", - "channelType": "power", - "type": "Switch", - "refresh": true, - "actions": [ - { - "command": "set_power", - "parameterType": "ONOFF" - } - ], - "category": "switch", - "tags": [ - "Switch" - ], - "readmeComment": "If this channel does not respond to on/off replace the model with chuangmi.plug.v3old in the config or upgrade firmware" - }, - { - "property": "usb_on", - "friendlyName": "USB", - "channel": "usb", - "channelType": "usb", - "type": "Switch", - "refresh": true, - "ChannelGroup": "actions", - "actions": [ - { - "command": "set_usb_*", - "parameterType": "ONOFFPARA" - } - ] - }, - { - "property": "temperature", - "friendlyName": "Temperature", - "channel": "temperature", - "type": "Number:Temperature", - "unit": "CELCIUS", - "stateDescription": { - "pattern": "%.1f %unit%", - "readOnly": true - }, - "refresh": true, - "actions": [], - "category": "temperature", - "tags": [ - "Measurement", - "Temperature" - ] - }, - { - "property": "wifi_led", - "friendlyName": "Wifi LED", - "channel": "led", - "channelType": "led", - "type": "Switch", - "refresh": true, - "ChannelGroup": "actions", - "actions": [ - { - "command": "set_wifi_led", - "parameterType": "ONOFF" - } - ] - } - ] - } -} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.jsq1.json b/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.jsq1.json new file mode 100644 index 0000000000000..2a8a2a53149a6 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.jsq1.json @@ -0,0 +1,158 @@ +{ + "deviceMapping": { + "id": [ + "deerma.humidifier.jsq1" + ], + "maxProperties": 1, + "channels": [ + { + "property": "OnOff_State", + "friendlyName": "Power", + "channel": "power", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "Set_OnOff", + "parameterType": "ONOFFNUMBER" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "Humidifier_Gear", + "friendlyName": "Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Low" + }, + { + "value": "2", + "label": "Medium" + }, + { + "value": "3", + "label": "High" + }, + { + "value": "4", + "label": "Humidity " + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "Set_HumidifierGears", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "Humidity_Value", + "friendlyName": "Humidity", + "channel": "humidity", + "type": "Number:Dimensionless", + "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "humidity", + "tags": [ + "Measurement", + "Humidity" + ] + }, + { + "property": "HumiSet_Value", + "friendlyName": "Humidity Setting", + "channel": "humidity_set", + "type": "Number:Dimensionless", + "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": false + }, + "refresh": true, + "actions": [ + { + "command": "Set_HumiValue", + "parameterType": "NUMBER" + } + ], + "category": "humidity", + "tags": [ + "SetPoint", + "Humidity" + ] + }, + { + "property": "Led_State", + "friendlyName": "LED indicator Light", + "channel": "led", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "SetLedState", + "parameterType": "ONOFFNUMBER" + } + ] + }, + { + "property": "TipSound_State", + "friendlyName": "Notification Sounds", + "channel": "sound", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "SetTipSound_Status", + "parameterType": "ONOFFNUMBER" + } + ] + }, + { + "property": "watertankstatus", + "friendlyName": "Watertank Status", + "channel": "watertankstatus", + "type": "Number", + "stateDescription": { + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "wet_and_protect", + "friendlyName": "Wet and Protect", + "channel": "wet_and_protect", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "Set_wet_and_protect", + "parameterType": "ONOFFNUMBER" + } + ] + } + ] + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.mjjsq.json b/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.mjjsq.json new file mode 100644 index 0000000000000..d3ce6308fc0ef --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/deerma.humidifier.mjjsq.json @@ -0,0 +1,146 @@ +{ + "deviceMapping": { + "id": [ + "deerma.humidifier.mjjsq", + "deerma.humidifier.jsq" + ], + "maxProperties": 1, + "channels": [ + { + "property": "OnOff_State", + "friendlyName": "Power", + "channel": "power", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "Set_OnOff", + "parameterType": "ONOFFNUMBER" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "Humidifier_Gear", + "friendlyName": "Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Low" + }, + { + "value": "2", + "label": "Medium" + }, + { + "value": "3", + "label": "High" + }, + { + "value": "4", + "label": "Humidity " + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "Set_HumidifierGears", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "Humidity_Value", + "friendlyName": "Humidity", + "channel": "humidity", + "type": "Number:Dimensionless", + "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "humidity", + "tags": [ + "Measurement", + "Humidity" + ] + }, + { + "property": "HumiSet_Value", + "friendlyName": "Humidity Setting", + "channel": "humidity_set", + "type": "Number:Dimensionless", + "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": false + }, + "refresh": true, + "actions": [ + { + "command": "Set_HumiValue", + "parameterType": "NUMBER" + } + ], + "category": "humidity", + "tags": [ + "SetPoint", + "Humidity" + ] + }, + { + "property": "Led_State", + "friendlyName": "LED indicator Light", + "channel": "led", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "SetLedState", + "parameterType": "ONOFFNUMBER" + } + ] + }, + { + "property": "TipSound_State", + "friendlyName": "Notification Sounds", + "channel": "sound", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "SetTipSound_Status", + "parameterType": "ONOFFNUMBER" + } + ] + }, + { + "property": "watertankstatus", + "friendlyName": "Watertank Status", + "channel": "watertankstatus", + "type": "Number", + "stateDescription": { + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + } + ] + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2009-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2009-miot.json index c0c5d71dcc86e..3a62d30f89444 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2009-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2009-miot.json @@ -1,7 +1,9 @@ { "deviceMapping": { "id": [ - "dreame.vacuum.p2009" + "dreame.vacuum.p2009", + "dreame.vacuum.p2036", + "dreame.vacuum.p2157" ], "propertyMethod": "get_properties", "maxProperties": 1, @@ -12,7 +14,6 @@ "channel": "vacuumaction", "type": "String", "stateDescription": { - "readOnly": true, "options": [ { "value": "sweep", @@ -374,7 +375,7 @@ "minimum": 0, "maximum": 50, "step": 1, - "pattern": "%.1f", + "pattern": "%.0f", "readOnly": true }, "refresh": true, @@ -516,14 +517,33 @@ "channel": "task-status", "type": "Number", "stateDescription": { - "minimum": 0, - "maximum": 20, - "step": 1, - "pattern": "%.1f", - "readOnly": true + "readOnly": true, + "options": [ + { + "value": "0", + "label": "Notask" + }, + { + "value": "1", + "label": "AutoClean" + }, + { + "value": "2", + "label": "CustomClean" + }, + { + "value": "3", + "label": "SelectAreanClean" + }, + { + "value": "4", + "label": "SpotArea" + } + ] }, "refresh": true, - "actions": [] + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"Notask\",\"1\"\u003d\"AutoClean\",\"2\"\u003d\"CustomClean\",\"3\"\u003d\"SelectAreanClean\",\"4\"\u003d\"SpotArea\"]" }, { "property": "break-point-restart", diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2156o-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2156o-miot.json new file mode 100644 index 0000000000000..c23afd413107a --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2156o-miot.json @@ -0,0 +1,1025 @@ +{ + "deviceMapping": { + "id": [ + "dreame.vacuum.p2156o", + "dreame.vacuum.p2041o" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "", + "friendlyName": "Actions", + "channel": "actions", + "type": "String", + "stateDescription": { + "options": [ + { + "value": "vacuum-start-sweep", + "label": "Start Sweep" + }, + { + "value": "vacuum-stop-sweeping", + "label": "Stop Sweeping" + }, + { + "value": "battery-start-charge", + "label": "Start Charge" + }, + { + "value": "brush-cleaner-reset-brush-life", + "label": "Brush Cleaner Reset Brush Life" + }, + { + "value": "brush-cleaner-reset-brush-life", + "label": "Brush Cleaner Reset Brush Life" + }, + { + "value": "filter-reset-filter-life", + "label": "Filter Reset Filter Life" + }, + { + "value": "vacuum-extend-start-clean", + "label": "Vacuum Extend Start Clean" + }, + { + "value": "vacuum-extend-stop-clean", + "label": "Vacuum Extend Stop Clean" + }, + { + "value": "map-map-req", + "label": "Map Map Req" + }, + { + "value": "map-update-map", + "label": "Map Update Map" + }, + { + "value": "audio-position", + "label": "Audio Position" + }, + { + "value": "audio-play-sound", + "label": "Audio Play Sound" + }, + { + "value": "time-delete-timer", + "label": "Time Delete Timer" + } + ] + }, + "actions": [ + { + "command": "action", + "parameterType": "EMPTY", + "siid": 2, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-start-sweep" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 2, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-stop-sweeping" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 3, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "battery-start-charge" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 9, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "brush-cleaner-reset-brush-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 10, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "brush-cleaner-reset-brush-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 11, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "filter-reset-filter-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 10.0 + ], + "siid": 4, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-extend-start-clean" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 4, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-extend-stop-clean" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 2.0 + ], + "siid": 6, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "map-map-req" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 4.0 + ], + "siid": 6, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "map-update-map" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 7, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "audio-position" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 7, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "audio-play-sound" + } + ] + } + }, + { + "command": "action", + "parameterType": "NUMBER", + "parameters": [ + 3.0 + ], + "siid": 8, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "time-delete-timer" + } + ] + } + } + ], + "readmeComment": "Value mapping [\"vacuum-start-sweep\"\u003d\"Start Sweep\",\"vacuum-stop-sweeping\"\u003d\"Stop Sweeping\",\"battery-start-charge\"\u003d\"Start Charge\",\"brush-cleaner-reset-brush-life\"\u003d\"Brush Cleaner Reset Brush Life\",\"brush-cleaner-reset-brush-life\"\u003d\"Brush Cleaner Reset Brush Life\",\"filter-reset-filter-life\"\u003d\"Filter Reset Filter Life\",\"vacuum-extend-start-clean\"\u003d\"Vacuum Extend Start Clean\",\"vacuum-extend-stop-clean\"\u003d\"Vacuum Extend Stop Clean\",\"map-map-req\"\u003d\"Map Map Req\",\"map-update-map\"\u003d\"Map Update Map\",\"audio-position\"\u003d\"Audio Position\",\"audio-play-sound\"\u003d\"Audio Play Sound\",\"time-delete-timer\"\u003d\"Time Delete Timer\"]" + }, + { + "property": "status", + "siid": 2, + "piid": 1, + "friendlyName": "Robot Cleaner - Status", + "channel": "status", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "Sweeping" + }, + { + "value": "2", + "label": "Idle" + }, + { + "value": "3", + "label": "Paused" + }, + { + "value": "4", + "label": "Error" + }, + { + "value": "5", + "label": "Go Charging" + }, + { + "value": "6", + "label": "Charging" + }, + { + "value": "7", + "label": "Mopping" + } + ] + }, + "refresh": true, + "actions": [], + "category": "status", + "tags": [ + "Status" + ], + "readmeComment": "Value mapping [\"1\"\u003d\"Sweeping\",\"2\"\u003d\"Idle\",\"3\"\u003d\"Paused\",\"4\"\u003d\"Error\",\"5\"\u003d\"Go Charging\",\"6\"\u003d\"Charging\",\"7\"\u003d\"Mopping\"]" + }, + { + "property": "fault", + "siid": 2, + "piid": 2, + "friendlyName": "Robot Cleaner - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "mode", + "siid": 2, + "piid": 3, + "friendlyName": "Robot Cleaner - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Silent" + }, + { + "value": "1", + "label": "Basic" + }, + { + "value": "2", + "label": "Strong" + }, + { + "value": "3", + "label": "Full Speed" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Silent\",\"1\"\u003d\"Basic\",\"2\"\u003d\"Strong\",\"3\"\u003d\"Full Speed\"]" + }, + { + "property": "battery-level", + "siid": 3, + "piid": 1, + "friendlyName": "Battery - Battery Level", + "channel": "battery_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "charging-state", + "siid": 3, + "piid": 2, + "friendlyName": "Battery - Charging State", + "channel": "charging_state", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "Charging" + }, + { + "value": "2", + "label": "Not Charging" + }, + { + "value": "5", + "label": "Go Charging" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"Charging\",\"2\"\u003d\"Not Charging\",\"5\"\u003d\"Go Charging\"]" + }, + { + "property": "brush-left-time", + "siid": 9, + "piid": 1, + "friendlyName": "Main Cleaning Brush - Brush Left Time", + "channel": "brush_left_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 300, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-life-level", + "siid": 9, + "piid": 2, + "friendlyName": "Main Cleaning Brush - Brush Life Level", + "channel": "brush_life_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-left-time1", + "siid": 10, + "piid": 1, + "friendlyName": "Side Cleaning Brush - Brush Left Time", + "channel": "brush_left_time1", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 200, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-life-level1", + "siid": 10, + "piid": 2, + "friendlyName": "Side Cleaning Brush - Brush Life Level", + "channel": "brush_life_level1", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-life-level", + "siid": 11, + "piid": 1, + "friendlyName": "Filter - Filter Life Level", + "channel": "filter_life_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-left-time", + "siid": 11, + "piid": 2, + "friendlyName": "Filter - Filter Left Time", + "channel": "filter_left_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 150, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "work-mode", + "siid": 4, + "piid": 1, + "friendlyName": "Vacuum Extend - Work Mode", + "channel": "work_mode", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 50, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-time", + "siid": 4, + "piid": 2, + "friendlyName": "Vacuum Extend - Cleaning Time", + "channel": "cleaning_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": 0, + "maximum": 32767, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-area", + "siid": 4, + "piid": 3, + "friendlyName": "Vacuum Extend - Cleaning Area", + "channel": "cleaning-area", + "type": "Number:Area", + "unit": "square_meter", + "stateDescription": { + "minimum": 0, + "maximum": 32767, + "step": 1, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-mode", + "siid": 4, + "piid": 4, + "friendlyName": "Vacuum Extend - Cleaning Mode", + "channel": "cleaning_mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "mode 0" + }, + { + "value": "1", + "label": "mode 1" + }, + { + "value": "2", + "label": "mode 2" + }, + { + "value": "3", + "label": "mode 3" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"mode 0\",\"1\"\u003d\"mode 1\",\"2\"\u003d\"mode 2\",\"3\"\u003d\"mode 3\"]" + }, + { + "property": "mop-mode", + "siid": 4, + "piid": 5, + "friendlyName": "Vacuum Extend - Mop Mode", + "channel": "mop_mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "low water" + }, + { + "value": "2", + "label": "medium water" + }, + { + "value": "3", + "label": "high water" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"low water\",\"2\"\u003d\"medium water\",\"3\"\u003d\"high water\"]" + }, + { + "property": "waterbox-status", + "siid": 4, + "piid": 6, + "friendlyName": "Vacuum Extend - Waterbox Status", + "channel": "waterbox_status", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "Status 0" + }, + { + "value": "1", + "label": "Status 1" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"Status 0\",\"1\"\u003d\"Status 1\"]" + }, + { + "property": "task-status", + "siid": 4, + "piid": 7, + "friendlyName": "Vacuum Extend - Task Status", + "channel": "task_status", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "Notask" + }, + { + "value": "1", + "label": "AutoClean" + }, + { + "value": "2", + "label": "CustomClean" + }, + { + "value": "3", + "label": "SelectAreanClean" + }, + { + "value": "4", + "label": "SpotArea" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"Notask\",\"1\"\u003d\"AutoClean\",\"2\"\u003d\"CustomClean\",\"3\"\u003d\"SelectAreanClean\",\"4\"\u003d\"SpotArea\"]" + }, + { + "property": "break-point-restart", + "siid": 4, + "piid": 11, + "friendlyName": "Vacuum Extend - Break Point Restart", + "channel": "break_point_restart", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Off\",\"1\"\u003d\"On\"]" + }, + { + "property": "carpet-press", + "siid": 4, + "piid": 12, + "friendlyName": "Vacuum Extend - Carpet Press", + "channel": "carpet_press", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Off\",\"1\"\u003d\"On\"]" + }, + { + "property": "serial-number", + "siid": 4, + "piid": 14, + "friendlyName": "Vacuum Extend - Serial Number", + "channel": "serial_number", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "keep-sweeper-time", + "siid": 4, + "piid": 17, + "friendlyName": "Vacuum Extend - Keep Sweeper Time", + "channel": "keep_sweeper_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": -1, + "maximum": 1000000, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "faults", + "siid": 4, + "piid": 18, + "friendlyName": "Vacuum Extend - Faults", + "channel": "faults", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "enable", + "siid": 5, + "piid": 1, + "friendlyName": "Do Not Disturb - Enable", + "channel": "enable", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "start-time", + "siid": 5, + "piid": 2, + "friendlyName": "Do Not Disturb - Start Time", + "channel": "start_time", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "end-time", + "siid": 5, + "piid": 3, + "friendlyName": "Do Not Disturb - End Time", + "channel": "end_time", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "volume", + "siid": 7, + "piid": 1, + "friendlyName": "Audio - Volume", + "channel": "volume", + "type": "Number:Dimensionless", + "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "voice-packet-id", + "siid": 7, + "piid": 2, + "friendlyName": "Audio - Voice Packet Id", + "channel": "voice_packet_id", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "voice-change-state", + "siid": 7, + "piid": 3, + "friendlyName": "Audio - Voice Change State", + "channel": "voice_change_state", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "time-zone", + "siid": 8, + "piid": 1, + "friendlyName": "Time - Time Zone", + "channel": "time_zone", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "timer-clean", + "siid": 8, + "piid": 2, + "friendlyName": "Time - Timer Clean", + "channel": "timer_clean", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "first-clean-time", + "siid": 12, + "piid": 1, + "friendlyName": "Clean Logs - First Clean Time", + "channel": "first_clean_time", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-time", + "siid": 12, + "piid": 2, + "friendlyName": "Clean Logs - Total Clean Time", + "channel": "total_clean_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-times", + "siid": 12, + "piid": 3, + "friendlyName": "Clean Logs - Total Clean Times", + "channel": "total_clean_times", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-area", + "siid": 12, + "piid": 4, + "friendlyName": "Clean Logs - Total Clean Area", + "channel": "total_clean_area", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "save-map-status", + "siid": 13, + "piid": 1, + "friendlyName": "Vslam Extend - Save Map Status", + "channel": "save_map_status", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Off\",\"1\"\u003d\"On\"]" + } + ], + "readmeComment": "Identified manual actions for execution\u003cbr /\u003e`action{\"did\":\"vacuum-start-sweep\",\"siid\":2,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"vacuum-stop-sweeping\",\"siid\":2,\"aiid\":2,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"battery-start-charge\",\"siid\":3,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"brush-cleaner-reset-brush-life\",\"siid\":9,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"brush-cleaner-reset-brush-life\",\"siid\":10,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"filter-reset-filter-life\",\"siid\":11,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"vacuum-extend-start-clean\",\"siid\":4,\"aiid\":1,\"in\":[10.0]}`\u003cbr /\u003e`action{\"did\":\"vacuum-extend-stop-clean\",\"siid\":4,\"aiid\":2,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"map-map-req\",\"siid\":6,\"aiid\":1,\"in\":[2.0]}`\u003cbr /\u003e`action{\"did\":\"map-update-map\",\"siid\":6,\"aiid\":2,\"in\":[4.0]}`\u003cbr /\u003e`action{\"did\":\"audio-position\",\"siid\":7,\"aiid\":1,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"audio-play-sound\",\"siid\":7,\"aiid\":2,\"in\":[]}`\u003cbr /\u003e`action{\"did\":\"time-delete-timer\",\"siid\":8,\"aiid\":1,\"in\":[3.0]}`\u003cbr /\u003ePlease test and feedback if they are working to they can be linked to a channel.", + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/viomi.vacuum.v8.json b/bundles/org.openhab.binding.miio/src/main/resources/database/viomi.vacuum.v8.json index 5eb3298cab590..6aa4387dd9962 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/viomi.vacuum.v8.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/viomi.vacuum.v8.json @@ -60,7 +60,7 @@ "actions": [] }, { - "property": "battery_life", + "property": "battary_life", "friendlyName": "Battery", "channel": "battery_life", "type": "Number", diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/xiaomi.aircondition.mc1-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/xiaomi.aircondition.mc1-miot.json index 082e75a507306..e5fa1d519e047 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/xiaomi.aircondition.mc1-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/xiaomi.aircondition.mc1-miot.json @@ -405,7 +405,7 @@ "piid": 5, "friendlyName": "Maintenance - Running Duration", "channel": "running-duration", - "type": "Number:Duration", + "type": "Number:Time", "unit": "seconds", "stateDescription": { "minimum": 0, diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.ceiling4.json b/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.ceiling4.json index a589bc11aa336..488353fddf61f 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.ceiling4.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.ceiling4.json @@ -55,14 +55,23 @@ "property": "bg_bright", "friendlyName": "Ambient Brightness", "channel": "ambientBrightness", - "channelType": "ambientBrightness", - "type": "Number", + "type": "Dimmer", "refresh": true, "ChannelGroup": "actions", "actions": [ { "command": "bg_set_bright", - "parameterType": "NUMBER" + "parameterType": "NUMBER", + "condition": { + "name": "BrightnessExisting" + } + }, + { + "command": "set_power", + "parameterType": "ONOFF", + "condition": { + "name": "BrightnessOnOff" + } } ] }, diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.airpurifier.vb2-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.airpurifier.vb2-miot.json new file mode 100644 index 0000000000000..4e20dcae188de --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.airpurifier.vb2-miot.json @@ -0,0 +1,1099 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.airpurifier.vb2" + ], + "propertyMethod": "get_properties", + "maxProperties": 3, + "channels": [ + { + "property": "", + "friendlyName": "Actions", + "channel": "actions", + "type": "String", + "stateDescription": { + "options": [ + { + "value": "button-toggle", + "label": "Toggle" + }, + { + "value": "button-toggle-mode", + "label": "Toggle Mode" + } + ] + }, + "actions": [ + { + "command": "action", + "parameterType": "EMPTY", + "siid": 8, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "button-toggle" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 8, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "button-toggle-mode" + } + ] + } + } + ], + "readmeComment": "Value mapping [\"button-toggle\"\u003d\"Toggle\",\"button-toggle-mode\"\u003d\"Toggle Mode\"]" + }, + { + "property": "fault", + "siid": 2, + "piid": 1, + "friendlyName": "Air Purifier - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No faults" + }, + { + "value": "1", + "label": "m1_run" + }, + { + "value": "2", + "label": "m1_stuck" + }, + { + "value": "3", + "label": "no_sensor" + }, + { + "value": "4", + "label": "error_hum" + }, + { + "value": "5", + "label": "error_temp" + }, + { + "value": "6", + "label": "timer_error1" + }, + { + "value": "7", + "label": "timer_error2" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No faults\",\"1\"\u003d\"m1_run\",\"2\"\u003d\"m1_stuck\",\"3\"\u003d\"no_sensor\",\"4\"\u003d\"error_hum\",\"5\"\u003d\"error_temp\",\"6\"\u003d\"timer_error1\",\"7\"\u003d\"timer_error2\"]" + }, + { + "property": "on", + "siid": 2, + "piid": 2, + "friendlyName": "Air Purifier - Power", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "fan-level", + "siid": 2, + "piid": 4, + "friendlyName": "Air Purifier - Fan Level", + "channel": "fan_level", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Level1" + }, + { + "value": "2", + "label": "Level2" + }, + { + "value": "3", + "label": "Level3" + }, + { + "value": "0", + "label": "Sleep" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"Level1\",\"2\"\u003d\"Level2\",\"3\"\u003d\"Level3\",\"0\"\u003d\"Sleep\"]" + }, + { + "property": "mode", + "siid": 2, + "piid": 5, + "friendlyName": "Air Purifier - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Auto" + }, + { + "value": "1", + "label": "Night" + }, + { + "value": "2", + "label": "Favourite" + }, + { + "value": "3", + "label": "Manual" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Auto\",\"1\"\u003d\"Night\",\"2\"\u003d\"Favourite\",\"3\"\u003d\"Manual\"]" + }, + { + "property": "pm2.5-density", + "siid": 3, + "piid": 6, + "friendlyName": "Environment - PM2 5 Density", + "channel": "pm2_5_density", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 600, + "step": 1, + "pattern": "%.1f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "relative-humidity", + "siid": 3, + "piid": 7, + "friendlyName": "Environment - Relative Humidity", + "channel": "relative_humidity", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "temperature", + "siid": 3, + "piid": 8, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "CELCIUS", + "stateDescription": { + "minimum": -40, + "maximum": 125, + "pattern": "%.1f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-life-level", + "siid": 4, + "piid": 3, + "friendlyName": "Filter - Filter Life Level", + "channel": "filter_life_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-used-time", + "siid": 4, + "piid": 5, + "friendlyName": "Filter - Filter Used Time", + "channel": "filter_used_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 18000, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "alarm", + "siid": 5, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "volume", + "siid": 5, + "piid": 2, + "friendlyName": "Alarm - Volume", + "channel": "volume", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "brightness", + "siid": 6, + "piid": 1, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "brightest" + }, + { + "value": "1", + "label": "glimmer" + }, + { + "value": "2", + "label": "not bright" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"brightest\",\"1\"\u003d\"glimmer\",\"2\"\u003d\"not bright\"]" + }, + { + "property": "on1", + "siid": 6, + "piid": 6, + "friendlyName": "Indicator Light - Switch Status", + "channel": "on1", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "physical-controls-locked", + "siid": 7, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "button-pressed", + "siid": 8, + "piid": 1, + "friendlyName": "Button - Button_pressed", + "channel": "button_pressed", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-max-time", + "siid": 9, + "piid": 1, + "friendlyName": "Filter Time - Filter Max Time", + "channel": "filter_max_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 2000, + "maximum": 8000, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "filter-hour-used-debug", + "siid": 9, + "piid": 2, + "friendlyName": "Filter Time - Filter Hour Used Debug", + "channel": "filter_hour_used_debug", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 18000, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-strong", + "siid": 10, + "piid": 1, + "friendlyName": "Motor Speed - M1 Strong", + "channel": "m1_strong", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-high", + "siid": 10, + "piid": 2, + "friendlyName": "Motor Speed - M1 High", + "channel": "m1_high", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-med", + "siid": 10, + "piid": 3, + "friendlyName": "Motor Speed - M1 Med", + "channel": "m1_med", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-med-l", + "siid": 10, + "piid": 4, + "friendlyName": "Motor Speed - M1 Med L", + "channel": "m1_med_l", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-low", + "siid": 10, + "piid": 5, + "friendlyName": "Motor Speed - M1 Low", + "channel": "m1_low", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-silent", + "siid": 10, + "piid": 6, + "friendlyName": "Motor Speed - M1 Silent", + "channel": "m1_silent", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "m1-favorite", + "siid": 10, + "piid": 7, + "friendlyName": "Motor Speed - M1 Favorite", + "channel": "m1_favorite", + "type": "Number", + "stateDescription": { + "minimum": 300, + "maximum": 2300, + "step": 10, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "motor1-speed", + "siid": 10, + "piid": 8, + "friendlyName": "Motor Speed - Motor1 Speed", + "channel": "motor1_speed", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 10000, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "motor1-set-speed", + "siid": 10, + "piid": 9, + "friendlyName": "Motor Speed - Motor1 Set Speed", + "channel": "motor1_set_speed", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 10000, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "favorite-level", + "siid": 10, + "piid": 10, + "friendlyName": "Motor Speed - Favorite Level", + "channel": "favorite_level", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 9, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "use-time", + "siid": 12, + "piid": 1, + "friendlyName": "Use Time - Use Time", + "channel": "use_time", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "purify-volume", + "siid": 13, + "piid": 1, + "friendlyName": "Aqi - Purify Volume", + "channel": "purify_volume", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "average-aqi", + "siid": 13, + "piid": 2, + "friendlyName": "Aqi - Average Aqi", + "channel": "average_aqi", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 600, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "average-aqi-cnt", + "siid": 13, + "piid": 3, + "friendlyName": "Aqi - Average_aqi Read Times", + "channel": "average_aqi_cnt", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "aqi-zone", + "siid": 13, + "piid": 4, + "friendlyName": "Aqi - Aqi Zone", + "channel": "aqi_zone", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "sensor-state", + "siid": 13, + "piid": 5, + "friendlyName": "Aqi - Sensor State", + "channel": "sensor_state", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "waiting" + }, + { + "value": "1", + "label": "ready" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"waiting\",\"1\"\u003d\"ready\"]" + }, + { + "property": "aqi-goodh", + "siid": 13, + "piid": 6, + "friendlyName": "Aqi - Aqi Goodh", + "channel": "aqi_goodh", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 255, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "aqi-runstate", + "siid": 13, + "piid": 7, + "friendlyName": "Aqi - Runstate", + "channel": "aqi_runstate", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "continue" + }, + { + "value": "1", + "label": "hold" + }, + { + "value": "2", + "label": "sleep" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"continue\",\"1\"\u003d\"hold\",\"2\"\u003d\"sleep\"]" + }, + { + "property": "aqi-state", + "siid": 13, + "piid": 8, + "friendlyName": "Aqi - Aqi State", + "channel": "aqi_state", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "AQI_GOOD_L" + }, + { + "value": "1", + "label": "AQI_GOOD_H" + }, + { + "value": "2", + "label": "AQI_MID_L" + }, + { + "value": "3", + "label": "AQI_MID_H" + }, + { + "value": "4", + "label": "AQI_BAD_L" + }, + { + "value": "5", + "label": "AQI_BAD_H" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"AQI_GOOD_L\",\"1\"\u003d\"AQI_GOOD_H\",\"2\"\u003d\"AQI_MID_L\",\"3\"\u003d\"AQI_MID_H\",\"4\"\u003d\"AQI_BAD_L\",\"5\"\u003d\"AQI_BAD_H\"]" + }, + { + "property": "rfid-tag", + "siid": 14, + "piid": 1, + "friendlyName": "Rfid - Rfid Tag", + "channel": "rfid_tag", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "rfid-factory-id", + "siid": 14, + "piid": 2, + "friendlyName": "Rfid - Rfid Factory Id", + "channel": "rfid_factory_id", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "rfid-product-id", + "siid": 14, + "piid": 3, + "friendlyName": "Rfid - Rfid Product Id", + "channel": "rfid_product_id", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "rfid-time", + "siid": 14, + "piid": 4, + "friendlyName": "Rfid - Rfid Time", + "channel": "rfid_time", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "rfid-serial-num", + "siid": 14, + "piid": 5, + "friendlyName": "Rfid - Rfid Serial Num", + "channel": "rfid_serial_num", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "app-extra", + "siid": 15, + "piid": 1, + "friendlyName": "Others - App Extra", + "channel": "app_extra", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "main-channel", + "siid": 15, + "piid": 2, + "friendlyName": "Others - Main Channel", + "channel": "main_channel", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "slave-channel", + "siid": 15, + "piid": 3, + "friendlyName": "Others - Slave Channel", + "channel": "slave_channel", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cola", + "siid": 15, + "piid": 4, + "friendlyName": "Others - Cola", + "channel": "cola", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "buttom-door", + "siid": 15, + "piid": 5, + "friendlyName": "Others - Buttom Door", + "channel": "buttom_door", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "reboot-cause", + "siid": 15, + "piid": 6, + "friendlyName": "Others - Reboot_cause", + "channel": "reboot_cause", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "REASON_HW_BOOT" + }, + { + "value": "1", + "label": "REASON_USER_REBOOT" + }, + { + "value": "2", + "label": "REASON_UPDATE" + }, + { + "value": "3", + "label": "REASON_WDT" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"REASON_HW_BOOT\",\"1\"\u003d\"REASON_USER_REBOOT\",\"2\"\u003d\"REASON_UPDATE\",\"3\"\u003d\"REASON_WDT\"]" + }, + { + "property": "manual-level", + "siid": 15, + "piid": 7, + "friendlyName": "Others - Manual Level", + "channel": "manual_level", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "level1" + }, + { + "value": "2", + "label": "level2" + }, + { + "value": "3", + "label": "level3" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"level1\",\"2\"\u003d\"level2\",\"3\"\u003d\"level3\"]" + }, + { + "property": "powertime", + "siid": 15, + "piid": 8, + "friendlyName": "Others - Powertime", + "channel": "powertime", + "type": "Number:duration", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "country-code", + "siid": 15, + "piid": 9, + "friendlyName": "Others - Country Code", + "channel": "country_code", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "91", + "label": "India" + }, + { + "value": "44", + "label": "UK" + }, + { + "value": "852", + "label": "Hong Kong" + }, + { + "value": "886", + "label": "Taiwan" + }, + { + "value": "82", + "label": "Korea" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"91\"\u003d\"India\",\"44\"\u003d\"UK\",\"852\"\u003d\"Hong Kong\",\"886\"\u003d\"Taiwan\",\"82\"\u003d\"Korea\"]" + } + ], + "experimental": false + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za5-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za5-miot.json new file mode 100644 index 0000000000000..a5e92ab2d8670 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za5-miot.json @@ -0,0 +1,361 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.fan.za5" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "on", + "siid": 2, + "piid": 1, + "friendlyName": "Fan - Power", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fan-level", + "siid": 2, + "piid": 2, + "friendlyName": "Fan - Fan Level", + "channel": "fan_level", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Level 1" + }, + { + "value": "2", + "label": "Level 2" + }, + { + "value": "3", + "label": "Level 3" + }, + { + "value": "4", + "label": "Level 4" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"Level 1\",\"2\"\u003d\"Level 2\",\"3\"\u003d\"Level 3\",\"4\"\u003d\"Level 4\"]" + }, + { + "property": "horizontal-swing", + "siid": 2, + "piid": 3, + "friendlyName": "Fan - Horizontal Swing", + "channel": "horizontal_swing", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "horizontal-angle", + "siid": 2, + "piid": 5, + "friendlyName": "Fan - Horizontal Angle", + "channel": "horizontal_angle", + "type": "Number", + "stateDescription": { + "minimum": 30, + "maximum": 120, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "mode", + "siid": 2, + "piid": 7, + "friendlyName": "Fan - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Natural Wind" + }, + { + "value": "1", + "label": "Straight Wind" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Natural Wind\",\"1\"\u003d\"Straight Wind\"]" + }, + { + "property": "off-delay", + "siid": 2, + "piid": 10, + "friendlyName": "Fan - Power Off Delay", + "channel": "off_delay", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 36000, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "anion", + "siid": 2, + "piid": 11, + "friendlyName": "Fan - Anion", + "channel": "anion", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "physical-controls-locked", + "siid": 3, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "brightness", + "siid": 4, + "piid": 3, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ] + }, + { + "property": "alarm", + "siid": 5, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "relative-humidity", + "siid": 7, + "piid": 1, + "friendlyName": "Environment - Relative Humidity", + "channel": "relative_humidity", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "tags": [ + "Measurement", + "Humidity" + ] + }, + { + "property": "temperature", + "siid": 7, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "temperature", + "tags": [ + "Setpoint", + "Temperature" + ] + }, + { + "property": "button-press", + "siid": 6, + "piid": 1, + "friendlyName": "Custom Service - Button Press", + "channel": "button_press", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "power" + }, + { + "value": "2", + "label": "swing" + }, + { + "value": "0", + "label": "No Button Pressed" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"power\",\"2\"\u003d\"swing\",\"0\"\u003d\"No Button Pressed\"]" + }, + { + "property": "battery-state", + "siid": 6, + "piid": 2, + "friendlyName": "Custom Service - Battery State", + "channel": "battery_state", + "type": "Switch", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "speed-now", + "siid": 6, + "piid": 4, + "friendlyName": "Custom Service - Speed Now", + "channel": "speed_now", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 3000, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "ac-state", + "siid": 6, + "piid": 5, + "friendlyName": "Custom Service - Ac State", + "channel": "ac_state", + "type": "Switch", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "speed-level", + "siid": 6, + "piid": 8, + "friendlyName": "Custom Service - Speed Level", + "channel": "speed_level", + "type": "Number:Dimensionless", + "unit": "percent", + "stateDescription": { + "minimum": 1, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + } + ], + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma2-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma2-miot.json new file mode 100644 index 0000000000000..5976b2e2a7399 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma2-miot.json @@ -0,0 +1,241 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.heater.ma2" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "on", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Switch Status", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fault", + "siid": 2, + "piid": 2, + "friendlyName": "Heater - Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC Connect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC Connect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, + { + "property": "target-temperature", + "siid": 2, + "piid": 5, + "friendlyName": "Heater - Target Temperature", + "channel": "target_temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": 18, + "maximum": 28, + "step": 1, + "pattern": "%.1f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "temperature", + "tags": [ + "Setpoint", + "Temperature" + ] + }, + { + "property": "countdown-time", + "siid": 3, + "piid": 1, + "friendlyName": "Countdown - Countdown Time", + "channel": "countdown_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 12, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "temperature", + "siid": 4, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "temperature", + "tags": [ + "Measurement", + "Temperature" + ] + }, + { + "property": "physical-controls-locked", + "siid": 5, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "alarm", + "siid": 6, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "alarm" + }, + { + "property": "brightness", + "siid": 7, + "piid": 3, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" + }, + { + "property": "hw-enable", + "siid": 8, + "piid": 8, + "friendlyName": "Private Service - Hw Enable", + "channel": "hw_enable", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "use-time", + "siid": 8, + "piid": 9, + "friendlyName": "Private Service - Use Time", + "channel": "use_time", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "time" + } + ], + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma3-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma3-miot.json new file mode 100644 index 0000000000000..ad79fffd912bd --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.ma3-miot.json @@ -0,0 +1,288 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.heater.ma3" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "", + "friendlyName": "Actions", + "channel": "actions", + "type": "String", + "stateDescription": { + "options": [ + { + "value": "private-service-toggle-switch", + "label": "Toggle Private Service" + } + ] + }, + "actions": [ + { + "command": "action", + "parameterType": "EMPTY", + "siid": 8, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "private-service-toggle-switch" + } + ] + } + } + ] + }, + { + "property": "on", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Switch Status", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fault", + "siid": 2, + "piid": 2, + "friendlyName": "Heater - Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC Connect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC Connect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, + { + "property": "target-temperature", + "siid": 2, + "piid": 5, + "friendlyName": "Heater - Target Temperature", + "channel": "target_temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": 16, + "maximum": 28, + "step": 1, + "pattern": "%.1f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "temperature", + "tags": [ + "Setpoint", + "Temperature" + ] + }, + { + "property": "mode", + "siid": 2, + "piid": 6, + "friendlyName": "Heater - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Auto" + }, + { + "value": "1", + "label": "LL Mode" + }, + { + "value": "2", + "label": "HH Mode" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Auto\",\"1\"\u003d\"LL Mode\",\"2\"\u003d\"HH Mode\"]" + }, + { + "property": "countdown-time", + "siid": 3, + "piid": 1, + "friendlyName": "Countdown - Countdown Time", + "channel": "countdown_time", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 43200, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "temperature", + "siid": 4, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "temperature", + "tags": [ + "Measurement", + "Temperature" + ] + }, + { + "property": "physical-controls-locked", + "siid": 5, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "alarm", + "siid": 6, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "alarm" + }, + { + "property": "brightness", + "siid": 7, + "piid": 3, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" + }, + { + "property": "use-time", + "siid": 8, + "piid": 9, + "friendlyName": "Private Service - Use Time", + "channel": "use_time", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + } + ], + "readmeComment": "Identified manual actions for execution\u003cbr /\u003e`action{\"did\":\"private-service-toggle-switch\",\"siid\":8,\"aiid\":1,\"in\":[]}`\u003cbr /\u003ePlease test and feedback if they are working to they can be linked to a channel.", + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.mc2-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.mc2-miot.json new file mode 100644 index 0000000000000..4982172b49b4d --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.mc2-miot.json @@ -0,0 +1,299 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.heater.mc2" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "on", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Power", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fault", + "siid": 2, + "piid": 2, + "friendlyName": "Heater - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC Connect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC Connect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, + { + "property": "target-temperature", + "siid": 2, + "piid": 5, + "friendlyName": "Heater - Target Temperature", + "channel": "target_temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": 18, + "maximum": 28, + "step": 1, + "pattern": "%.1f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "temperature", + "tags": [ + "Setpoint", + "Temperature" + ] + }, + { + "property": "countdown-time", + "siid": 3, + "piid": 1, + "friendlyName": "Countdown - Countdown Time", + "channel": "countdown_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 12, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "temperature", + "siid": 4, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [], + "category": "temperature", + "tags": [ + "Measurement", + "Temperature" + ] + }, + { + "property": "physical-controls-locked", + "siid": 5, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "alarm", + "siid": 6, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "alarm" + }, + { + "property": "brightness", + "siid": 7, + "piid": 3, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" + }, + { + "property": "hw-enable", + "siid": 8, + "piid": 8, + "friendlyName": "Private Service - Hw Enable", + "channel": "hw_enable", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "use-time", + "siid": 8, + "piid": 9, + "friendlyName": "Private Service - Use Time", + "channel": "use_time", + "type": "Number:Time", + "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "country-code", + "siid": 8, + "piid": 10, + "friendlyName": "Private Service - Country Code", + "channel": "country_code", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Unknown" + }, + { + "value": "1", + "label": "US" + }, + { + "value": "82", + "label": "KR" + }, + { + "value": "44", + "label": "EU" + }, + { + "value": "81", + "label": "JP" + }, + { + "value": "7", + "label": "RU" + }, + { + "value": "86", + "label": "CN" + }, + { + "value": "852", + "label": "HK" + }, + { + "value": "886", + "label": "TW" + }, + { + "value": "33", + "label": "FR" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Unknown\",\"1\"\u003d\"US\",\"82\"\u003d\"KR\",\"44\"\u003d\"EU\",\"81\"\u003d\"JP\",\"7\"\u003d\"RU\",\"86\"\u003d\"CN\",\"852\"\u003d\"HK\",\"886\"\u003d\"TW\",\"33\"\u003d\"FR\"]" + } + ], + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.na1-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.na1-miot.json new file mode 100644 index 0000000000000..3383fdae5b9a3 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.na1-miot.json @@ -0,0 +1,228 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.heater.na1" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "on", + "siid": 2, + "piid": 2, + "friendlyName": "Heater - Power", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fault", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC Connect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC Connect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, + { + "property": "heat-level", + "siid": 2, + "piid": 3, + "friendlyName": "Heater - Heat Level", + "channel": "heat_level", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "High" + }, + { + "value": "2", + "label": "Low" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"High\",\"2\"\u003d\"Low\"]" + }, + { + "property": "mode", + "siid": 2, + "piid": 4, + "friendlyName": "Heater - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Fan not swing" + }, + { + "value": "1", + "label": "Fan swing" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Fan not swing\",\"1\"\u003d\"Fan swing\"]" + }, + { + "property": "alarm", + "siid": 3, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "alarm" + }, + { + "property": "countdown-time", + "siid": 4, + "piid": 1, + "friendlyName": "Countdown - Countdown Time", + "channel": "countdown_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 12, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "brightness", + "siid": 6, + "piid": 1, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" + }, + { + "property": "physical-controls-locked", + "siid": 7, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "return-to-middle", + "siid": 8, + "piid": 3, + "friendlyName": "Private Service - Return To Middle", + "channel": "return_to_middle", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + } + ], + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.nb1-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.nb1-miot.json new file mode 100644 index 0000000000000..61248fa404d82 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.nb1-miot.json @@ -0,0 +1,342 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.heater.nb1" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "on", + "siid": 2, + "piid": 2, + "friendlyName": "Heater - Power", + "channel": "on", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "switch", + "tags": [ + "Switch" + ] + }, + { + "property": "fault", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC Connect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC Connect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, + { + "property": "heat-level", + "siid": 2, + "piid": 3, + "friendlyName": "Heater - Heat Level", + "channel": "heat_level", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "High" + }, + { + "value": "2", + "label": "Low" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"High\",\"2\"\u003d\"Low\"]" + }, + { + "property": "mode", + "siid": 2, + "piid": 4, + "friendlyName": "Heater - Mode", + "channel": "mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Fan not swing" + }, + { + "value": "1", + "label": "Fan swing" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Fan not swing\",\"1\"\u003d\"Fan swing\"]" + }, + { + "property": "target-temperature", + "siid": 2, + "piid": 5, + "friendlyName": "Heater - Target Temperature", + "channel": "target_temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": 16, + "maximum": 30, + "step": 1, + "pattern": "%.1f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "temperature", + "siid": 9, + "piid": 7, + "friendlyName": "Environment - Temperature", + "channel": "temperature", + "type": "Number:Temperature", + "unit": "celsius", + "stateDescription": { + "minimum": -30, + "maximum": 100, + "pattern": "%.1f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "alarm", + "siid": 3, + "piid": 1, + "friendlyName": "Alarm - Alarm", + "channel": "alarm", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ], + "category": "alarm" + }, + { + "property": "countdown-time", + "siid": 4, + "piid": 1, + "friendlyName": "Countdown - Countdown Time", + "channel": "countdown_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 12, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "brightness", + "siid": 6, + "piid": 1, + "friendlyName": "Indicator Light - Brightness", + "channel": "brightness", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "category": "light", + "tags": [ + "Control", + "Light" + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" + }, + { + "property": "physical-controls-locked", + "siid": 7, + "piid": 1, + "friendlyName": "Physical Control Locked - Physical Control Locked", + "channel": "physical_controls_locked", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "return-to-middle", + "siid": 8, + "piid": 3, + "friendlyName": "Private Service - Return To Middle", + "channel": "return_to_middle", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "country-code", + "siid": 8, + "piid": 4, + "friendlyName": "Private Service - Country Code", + "channel": "country_code", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Unknown" + }, + { + "value": "1", + "label": "US" + }, + { + "value": "82", + "label": "KR" + }, + { + "value": "44", + "label": "EU" + }, + { + "value": "81", + "label": "JP" + }, + { + "value": "7", + "label": "RU" + }, + { + "value": "86", + "label": "CN" + }, + { + "value": "852", + "label": "HK" + }, + { + "value": "886", + "label": "TW" + }, + { + "value": "33", + "label": "FR" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Unknown\",\"1\"\u003d\"US\",\"82\"\u003d\"KR\",\"44\"\u003d\"EU\",\"81\"\u003d\"JP\",\"7\"\u003d\"RU\",\"86\"\u003d\"CN\",\"852\"\u003d\"HK\",\"886\"\u003d\"TW\",\"33\"\u003d\"FR\"]" + }, + { + "property": "hw-en", + "siid": 8, + "piid": 5, + "friendlyName": "Private Service - Hw En", + "channel": "hw_en", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + } + ], + "experimental": true + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za1.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za1.json index 6c60317f43681..4dc9fb14e418d 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za1.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za1.json @@ -10,7 +10,6 @@ "property": "power", "friendlyName": "Power", "channel": "power", - "channelType": "power", "type": "Switch", "refresh": true, "actions": [ @@ -69,7 +68,6 @@ "property": "buzzer", "friendlyName": "Buzzer Status", "channel": "buzzer", - "channelType": "buzzer", "type": "Switch", "refresh": true, "actions": [ @@ -96,7 +94,6 @@ "property": "child_lock", "friendlyName": "Child Lock", "channel": "childlock", - "channelType": "childlock", "type": "Switch", "refresh": true, "actions": [ @@ -141,7 +138,6 @@ "property": "use_time", "friendlyName": "Run Time", "channel": "usedhours", - "channelType": "usedhours", "type": "Number:Time", "unit": "hours", "refresh": true, diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za2-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za2-miot.json index 46f853218ab18..8344f0a8ea9dd 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za2-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.za2-miot.json @@ -12,10 +12,35 @@ "piid": 1, "friendlyName": "Heater - Device Fault", "channel": "fault", - "channelType": "miot_uint32", "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC\tConnect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, "refresh": true, - "actions": [] + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC\tConnect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" }, { "property": "on", @@ -23,7 +48,6 @@ "piid": 2, "friendlyName": "Heater - Power", "channel": "on", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -43,9 +67,14 @@ "piid": 6, "friendlyName": "Heater - Target Temperature", "channel": "target-temperature", - "channelType": "ZhimiHeaterZa2_target-temperature", "type": "Number:Temperature", "unit": "CELCIUS", + "stateDescription": { + "minimum": 16, + "maximum": 28, + "step": 1, + "pattern": "%.1f %unit%" + }, "refresh": true, "actions": [ { @@ -65,7 +94,6 @@ "piid": 1, "friendlyName": "Alarm - Alarm", "channel": "alarm", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -82,9 +110,14 @@ "piid": 1, "friendlyName": "Countdown - Countdown Time", "channel": "countdown-time", - "channelType": "time", "type": "Number:Time", "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 8, + "step": 1, + "pattern": "%.0f %unit%" + }, "refresh": true, "actions": [ { @@ -101,6 +134,13 @@ "channel": "relative-humidity", "type": "Number:Dimensionless", "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, "refresh": true, "actions": [], "tags": [ @@ -117,6 +157,8 @@ "type": "Number:Temperature", "unit": "CELCIUS", "stateDescription": { + "minimum": -30, + "maximum": 100, "pattern": "%.1f %unit%", "readOnly": true }, @@ -134,7 +176,23 @@ "piid": 1, "friendlyName": "Indicator Light - Brightness", "channel": "brightness", - "type": "Dimmer", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, "refresh": true, "actions": [ { @@ -146,7 +204,8 @@ "tags": [ "Control", "Light" - ] + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" }, { "property": "physical-controls-locked", @@ -154,7 +213,6 @@ "piid": 1, "friendlyName": "Physical Control Locked - Physical Controls Locked", "channel": "physical-controls-locked", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -170,9 +228,15 @@ "piid": 7, "friendlyName": "Private-Service - Use Time", "channel": "use-time", - "channelType": "time", "type": "Number:Time", "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": -1, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, "refresh": true, "actions": [], "category": "time" diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.zb1-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.zb1-miot.json index e9d63466afc07..1513658d28d32 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.zb1-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.heater.zb1-miot.json @@ -6,24 +6,12 @@ "propertyMethod": "get_properties", "maxProperties": 1, "channels": [ - { - "property": "fault", - "siid": 2, - "piid": 1, - "friendlyName": "Heater - Device Fault", - "channel": "fault", - "channelType": "miot_uint32", - "type": "Number", - "refresh": true, - "actions": [] - }, { "property": "on", "siid": 2, "piid": 2, "friendlyName": "Heater - Power", "channel": "on", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -37,15 +25,56 @@ "Switch" ] }, + { + "property": "fault", + "siid": 2, + "piid": 1, + "friendlyName": "Heater - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "No Error" + }, + { + "value": "1", + "label": "NTC\tConnect Error" + }, + { + "value": "2", + "label": "High Temperature Alarm" + }, + { + "value": "3", + "label": "EEPROM Error" + }, + { + "value": "4", + "label": "Multi Errors" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"No Error\",\"1\"\u003d\"NTC\tConnect Error\",\"2\"\u003d\"High Temperature Alarm\",\"3\"\u003d\"EEPROM Error\",\"4\"\u003d\"Multi Errors\"]" + }, { "property": "target-temperature", "siid": 2, "piid": 6, "friendlyName": "Heater - Target Temperature", "channel": "target-temperature", - "channelType": "ZhimiHeaterZa2_target-temperature", "type": "Number:Temperature", - "unit": "CELCIUS", + "unit": "celsius", + "stateDescription": { + "minimum": 16, + "maximum": 28, + "step": 1, + "pattern": "%.1f %unit%" + }, "refresh": true, "actions": [ { @@ -65,7 +94,6 @@ "piid": 1, "friendlyName": "Alarm - Alarm", "channel": "alarm", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -82,9 +110,14 @@ "piid": 1, "friendlyName": "Countdown - Countdown Time", "channel": "countdown-time", - "channelType": "time", "type": "Number:Time", "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 8, + "step": 1, + "pattern": "%.0f %unit%" + }, "refresh": true, "actions": [ { @@ -101,6 +134,13 @@ "channel": "relative-humidity", "type": "Number:Dimensionless", "unit": "PERCENT", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, "refresh": true, "actions": [], "tags": [ @@ -117,6 +157,8 @@ "type": "Number:Temperature", "unit": "CELCIUS", "stateDescription": { + "minimum": -30, + "maximum": 100, "pattern": "%.1f %unit%", "readOnly": true }, @@ -134,7 +176,23 @@ "piid": 1, "friendlyName": "Indicator Light - Brightness", "channel": "brightness", - "type": "Dimmer", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Bright" + }, + { + "value": "1", + "label": "Dark" + }, + { + "value": "2", + "label": "Extinguished" + } + ] + }, "refresh": true, "actions": [ { @@ -146,7 +204,8 @@ "tags": [ "Control", "Light" - ] + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Bright\",\"1\"\u003d\"Dark\",\"2\"\u003d\"Extinguished\"]" }, { "property": "physical-controls-locked", @@ -154,7 +213,6 @@ "piid": 1, "friendlyName": "Physical Control Locked - Physical Controls Locked", "channel": "physical-controls-locked", - "channelType": "miot_bool", "type": "Switch", "refresh": true, "actions": [ @@ -170,12 +228,17 @@ "piid": 7, "friendlyName": "Private-Service - Use Time", "channel": "use-time", - "channelType": "time", "type": "Number:Time", "unit": "seconds", + "stateDescription": { + "minimum": 0, + "maximum": 2147483647, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, "refresh": true, - "actions": [], - "category": "time" + "actions": [] }, { "property": "country-code", @@ -183,7 +246,6 @@ "piid": 8, "friendlyName": "Private-Service - Country-Code", "channel": "country-code", - "channelType": "ZhimiHeaterZb1_country-code", "type": "Number", "stateDescription": { "readOnly": true, diff --git a/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json b/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json index 572e2d6ca7979..d1fa2c964c7c7 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json @@ -1,4 +1,5 @@ { + "090615.aircondition.lnk": "PTX central air conditioning controller", "090615.curtain.1mcu01": "One meter intelligent curtain", "090615.curtain.jldj03": "PTX Rolling curtain", "090615.curtain.jxdj02": " PTX mechanical roller motor", @@ -16,9 +17,14 @@ "090615.curtain.zsdj82": "ZS intelligent curtain motor", "090615.light.demo2": "PTX intelligent heating and cooling lamp belt (mesh)", "090615.light.mlig01": "PTX intelligent downlight (mesh)", + "090615.light.pipadd": "Crackle smart light band (mesh)", + "090615.light.pipatd": "Crackle smart anti glare lamp (mesh)", "090615.plug.pipa86": "Crackle intelligent switch two three plug", "090615.plug.plus01": "Intelligent 86 socket(WIFI)", "090615.plug.plus02": "Intelligent Mobile Plug (WIFI)", + "090615.switch.hmb012": "H intelligent single key switch (mesh)", + "090615.switch.hmb022": "H intelligent double key switch (mesh)", + "090615.switch.hmb032": "H intelligent three key switch (mesh)", "090615.switch.mesw1": "PTX Mesh intelligent one switch", "090615.switch.mesw2": "PTX Mesh intelligent two switch", "090615.switch.mesw3": "PTX Mesh intelligent three switch", @@ -33,6 +39,7 @@ "090615.switch.ptxtc1": "PTX intelligent touch switch on (mesh)", "090615.switch.ptxtc2": "PTX intelligent touch two on (mesh)", "090615.switch.ptxtc3": "PTX intelligent touch switch three on (mesh)", + "090615.switch.qksw32": "PTX card access switch (WiFi)", "090615.switch.smb1s1": "Sumi B1 Bluetooth mesh switch (single key)", "090615.switch.smb1s2": "Sumi B1 Bluetooth mesh switch (double key)", "090615.switch.smb1s3": "Sumi B1 Bluetooth mesh switch (three key)", @@ -108,6 +115,9 @@ "aok98.curtain.aok68": "AOK AM68 Smart Curtain", "aok98.curtain.qlam50": "qinglinkQ302 Smart Curtain", "app.light.wynd1": "Nnuodu Intelligent Living Room Lamp", + "arnoo.curtain.awcc10": "智能平开窗帘电机", + "arnoo.curtain.awcc11": "智能平开窗帘电机", + "arnoo.humidifier.aahz10": "智能加湿器", "arnoo.light.cct01": "WiFi 60W 调光调色灯", "arnoo.light.dim01": "WiFi 60W 调光灯", "arnoo.light.rgbw01": "WiFi 60W 调光调颜色灯", @@ -131,6 +141,7 @@ "babai.curtain.bb82mj": "Babuy Smart Curtain", "babai.curtain.kyx850": "KYX Smart Curtain", "babai.curtain.lb100a": "LANBOO Smart Curtain", + "babai.curtain.m515e": "Zemismart smart curtain motor", "babai.curtain.mtx850": "MTX Smart Curtain", "babai.curtain.nh5810": "NanHoo Smart Curtain", "babai.curtain.yilc3": "Yi-LOCK Smart Curtain C3", @@ -157,6 +168,11 @@ "beihao.airer.l9": "Lisheng clothes-horse", "beihao.airer.l9xf": "JIajueshi clothes-horse", "beihao.airer.lyj08": "Ai Smart Clothes Dryer", + "bemfa.fan.fan": "风扇", + "bemfa.light.be01": "巴法", + "bemfa.light.bemfa": "巴法云", + "bemfa.plug.be002": "巴法插座", + "bemfa.sensor_ht.dht11": "传感器", "bgdz.light.test3": "light", "bgdz.plug.chazuo": "outlet", "binthe.curtain.bcm": "BINTHEN Smart Curtain", @@ -167,6 +183,7 @@ "bj352.waterpuri.k10": "352Water purifier K10", "bj352.waterpuri.s100cm": "352WaterPurifierS100", "bjdm.airfresh.test01": "LOMEDIQI Intelligent automatic air purifier", + "bkrobo.bed.t1": "Blupeace Air Massage Mattress Topper", "blink.airmonitor.bs0001": "BlinkerAirDetector", "blink.light.bl0001": "BlinkerSmartLight", "blink.plug.bp0001": "BlinkerSmartPlug", @@ -208,17 +225,26 @@ "bull.switch.s312b": "G27_智能开关_三开_中键", "bull.switch.s312c": "G27_智能开关_三开_下键", "bymiot.aircondition.ir2": "未来居空调控制器(红外版)", + "bymiot.aircondition.v1": "未来居中央空调控制器", "bymiot.curtain.v2": "未来居电动窗帘", "bymiot.gateway.v1": "企业有线智能网关", "bymiot.gateway.v2": "企业有线智能网关2.0版", "bymiot.motion.v1": "未来居人体移动传感器", + "bymiot.sensor_occupy.v3": "未来居强电插卡取电", + "bymiot.switch.1keyv1": "未来居单键强电开关", + "bymiot.switch.2keyv1": "未来居双键强电开关", + "bymiot.switch.3keyv1": "未来居三键强电开关", + "bymiot.switch.4keyv1": "未来居四键强电开关", + "bymiot.switch.6keyv1": "未来居六键强电开关", "bzhome.curtain.sz050": "窗帘电机", "bzhome.plug.sz070": "插座转换器", "bzhome.switch.sz010": "开关", + "candor.wine_cool.a": "Candor Wine Cooler(80L/275L)", "cchip.light.l1": "Air Surface ceiling light", "cchome.motion.v1": "人体感应器", "cchome.switch.86l1v1": "Smart Switch(Single)", "cchome.switch.86l2v1": "Smart Switch(Double)", + "cchome.switch.86l3v1": "Smart Switch(Three)", "cchome.tow_w.wyj001": "Bathroom Rack", "cchome.wopener.tcq001": "Motor Controller", "cgllc.airm.cgdn1": "Qingping Air Monitor Lite", @@ -252,6 +278,7 @@ "chuangmi.camera.025b02": "IMILAB Security Camera N Series", "chuangmi.camera.029a02": "Mi 360° Home Security Camera 2K", "chuangmi.camera.036a02": "IMILAB Home Security Camera Y2", + "chuangmi.camera.038a02": "IMILAB C21", "chuangmi.camera.27a02": "IMILAB C10", "chuangmi.camera.ip026c": "Mi 360° Home Security Camera 1080p Essential", "chuangmi.camera.ip029a": "Mi 360° Home Security Camera 2K", @@ -268,6 +295,7 @@ "chuangmi.camera.ipc019e": "IMI Home Security Camera A1", "chuangmi.camera.ipc020": "IMILAB Security Camera N Series", "chuangmi.camera.ipc021": "Mi 360° Home Security Camera 2K Pro", + "chuangmi.camera.ipc022": "Mi 360° Home Security AI Camera", "chuangmi.camera.v2": "Mi Home Security Camera 360°", "chuangmi.camera.v3": "IMI 1080P Home Security Camera", "chuangmi.camera.v4": "IMI Home Security Camera mini", @@ -340,6 +368,7 @@ "chunmi.waterpuri.800f3": "Mi Water Purifier H800G", "chunmi.ysj.tsa1": "Mi Countertop Filtered Water Dispenser", "cleargrass.sensor_ht.dk1": "Mi Temperature and Humidity Monitor", + "clk.light.basket": "WuZuoTiLan", "cmcc.aircondition.x12": "中国移动空调伴侣CMCC-X12", "cmcc.plug.x11": "中国移动智能插座CMCC-X11", "coc.airpurifier.tk": "离子塔/静音无耗材空气净化器-骑士", @@ -476,6 +505,10 @@ "dicook.cooker.wfz4003": "intelligent cooking robot", "difeis.curtain.d1": "Difeis D1 Motor", "difeis.ven_fan.dql04h": "Difeisi Air Cool", + "dikair.light.wy0a01": "Dan Fei Nuo Smart Light", + "dikair.light.wy0a02": "Xuan Yi Smart Light", + "dikair.light.wy0a03": "Yan Tai Scene Smart Llight", + "dikair.light.wy0a04": "Yi Tang Smart Light", "dmaker.airfresh.a1": "Mi Fresh Air Ventilator A1-150", "dmaker.airfresh.dm2018": "DreamMaker Fresh Air Ventilator", "dmaker.airfresh.t2017": "Mi Fresh Air Ventilator", @@ -515,16 +548,21 @@ "dreame.vacuum.mc1808": "Mi Robot Vacuum Mop", "dreame.vacuum.p2008": "Dreame Robot Vacuum-Mop F9", "dreame.vacuum.p2009": "Dreame Robot Vacuum D9 ", + "dreame.vacuum.p2029": "Dreame Bot L10 Pro", "dreame.vacuum.p2036": "Trouver Robot LDS Vacuum-Mop Finder", "dreame.vacuum.p2041": "Mi Robot Vacuum-Mop 1T", "dreame.vacuum.p2041o": "Mi Robot Vacuum-Mop 2 Pro+", + "dreame.vacuum.p2156o": "MOVA Z500 Robot Vacuum and Mop Cleaner", + "dreame.vacuum.p2157": "MOVA L600 Robot Vacuum and Mop Cleaner", "dsm.lock.h3": "DESSMANN smart lock-Di H3", "dsm.lock.q3": "Q3", "dsm.lock.r5": "DESSMANN Facial recognition smart lock-Di R5", "dtr.magic_touch.211mgr": "P5B Intelligent Neck Massager", "dtr.magic_touch.p6b": "PGG Neck Massager P6", + "dtr.magic_touch.p7": "PGG Neck Massager P7", "dun.cateye.nknk500": "DUN Smart Doorbell", "duoqin.safe.pbfv01": "Privacy box for finger vein identification", + "dwdz.switch.sw0a01": "Scene mesh breaker DBS", "ecloud.airc.eq": "智能空调", "ecloud.airfresh.eq": "新风", "ecloud.curtain.eq": "窗帘", @@ -585,6 +623,7 @@ "ezhome.switch.zhyapp03": "touch-panel", "ezhome.switch.zhyapp06": "Reset-Switch", "ezhome.tv.yy1002": "云接入电视", + "fawad.airrtc.40011": "FOWAD Floorheating Thermostat", "fawad.airrtc.fwd20011": "FOWAD thermostat controller", "fbs.airmonitor.pth02": "AIR QUALITY TESTER", "feibit.aircondition.zrc": "IR blaster", @@ -676,6 +715,7 @@ "gmn.light.wy0a02": "Gomanni Scene two color downlight", "gmn.light.wy0w01": "Gomanni Scene two color ceiling lamp", "gmn.light.wyfan1": "Gomanni G1 series bedroom fan lamp", + "gmrii.curtain.gt30": "GMIRI Curtain GT30TV/P", "gmwl.light.cibdo0410ga": "gmwlznd", "golden.curtain.c01": "curtain", "golden.kettle.wd01ei": "drinking", @@ -775,6 +815,7 @@ "huayi.light.peg093": "HUIZUO PEGASUS For Bedroom", "huayi.light.pis123": "HUIZUO PISCES For Bedroom", "huayi.light.pisces": "HUIZUO PISCES For Living Room", + "huayi.light.rgb205": "HUIZUO Light Board", "huayi.light.tau023": "HUIZUO TAURUS For Bedroom", "huayi.light.taurus": "HUIZUO TAURUS For Living Room", "huayi.light.vir063": "HUIZUO VIRGO For Bedroom", @@ -792,10 +833,12 @@ "huazhu.airc.v2": "空调控制器", "huazhu.curtain.v2": "窗帘电机", "huazhu.switch.1keyv2": "单键开关", + "huazhu.tv.ir1": "电视控制器", "huihe.switch.plug": "smart pulg", "huohe.lock.m1": "M1Lock", "huoman.litter_box.co1": "CO1", "hutlon.lock.v0001": "Huitailong ultra-thin screen fingerprint intelligent lock U1", + "hwzn.light.wy0a01": "Xihui smart ceiling lamp", "hxiot.light.ha1": "智能灯控(1路单亮度版)", "hxrcj.hood.e5": "烟机", "hxrcj.oven.e5": "蒸箱", @@ -809,6 +852,11 @@ "idelan.aircondition.v2": "Jinxing Smart Air Conditioner", "ihealth.bp.bpm1": "iHealth Sphygmomanometer", "ihealth.bpm.kd5907": "Andon Smart Sphygmomanometer", + "ihome.aircondition.eaac": "空调", + "ihome.curtain.escc": "窗帘", + "ihome.light.estk": "智能照明", + "ihome.plug.essp": "插座", + "ihome.switch.switch": "开关", "ijomoo.toilet.zs320": "Smart Toilet Lid-ZS320T", "ikea.light.led1536g5": "IKEA E14 white spectrum", "ikea.light.led1537r6": "IKEA GU10 white spectrum", @@ -817,7 +865,20 @@ "ikea.light.led1623g12": "IKEA E27 warm white", "ikea.light.led1649c5": "IKEA E14 warm white", "ikea.light.led1650r5": "IKEA GU10 warm white", + "ikecin.aircondition.ir1": "空调", + "ikecin.airfresh.95": "智能新风控制", + "ikecin.airrtc.29": "智能温控器", + "ikecin.airrtc.76": "智能温控器", + "ikecin.airrtc.84": "智能温控器", + "ikecin.airrtc.87": "智能温控器", + "ikecin.airrtc.90": "智能温控器", + "ikecin.airrtc.93": "智能温控器", + "ikecin.airrtc.96": "智能温控器", + "ikecin.airrtc.99": "智能温控器", "ikecin.plug.6": "智能开关", + "ikecin.stb.ir3": "机顶盒", + "ikecin.switch.ir10": "开关", + "ikecin.tv.ir2": "电视", "ikonke.light.kkbulb": "bulb", "ikonke.light.kklight": "智能炫彩灯", "ikonke.plug.k2": "k2 smart plug", @@ -847,13 +908,20 @@ "isa.camera.virtual": "小方智能摄像机组", "isa.magnet.dw2hl": "Mi Door and Window Sensor 2", "isa.router.mr01hl": "HuaLai Xiao Fang Mesh router gateway", + "isa.switch.kg03hl": "Mi Smart Triple One Way Wall Switch with Display", "isleep.blanket.hs2001": "LETSLEEP Water Heating Mattress HS2001", "iwarm.aircondition.gt": "小沃精灵", "iwarm.waterheater.l1p24": "小沃壁挂炉", "janshi.magic_touch.g2": "G2 Spinal comfort neck massager", + "jieman.magic_touch.js01": "Jishu Intelligent cervical spine massage instrument", "jieman.magic_touch.js78": "Jishu Intelligent Cervical Massage", + "jieman.magic_touch.kdl0": "Kangduoli Smart Massager", "jieman.magic_touch.ms9": "Intelligent cervical massage instrument", + "jieman.magic_touch.newf": "New for Smart Massager", + "jieman.magic_touch.tfs2": "Tefeishi Smart Massager", + "jieman.magic_touch.tsf1": "Tefeishi Smart Massager", "jieman.magic_touch.ys01": "Yashen Intelligent cervical massage instrument", + "jieman.magic_touch.ys02": "Yashen Smart Massager", "jihisi.light.wy0a01": "JIHISI Intelligent lamp", "jilian.aircondition.a0": "mini-aircondition", "jilian.curtain.fm1": "Geeklink_Curtain", @@ -929,6 +997,7 @@ "kejia.airer.krq": "COURAGE Smart Airer", "kejia.airer.krqpro": "Courage Smart AirerPRO", "kejia.airer.mzn": "MI Smart·Electric Airer", + "kejia.airer.mznpro": "Mi Smart AirerPRO", "kejia.airer.th001": "Smart drying rack", "kiwik.aircondition.irac": "空调红外遥控器", "kiwik.curtain.kaw": "窗帘电机", @@ -995,6 +1064,7 @@ "lcrmcr.safe.ms30mp": "CRMCR Annuo Smart Safe PRO", "lcrmcr.safe.ms55kn": "CRMCR Kanuo Smart Safe", "lcrmcr.safe.ms80b": "Carberry Face Recognition Safe", + "ldsn.curtain.awcc10": "智能窗帘电机", "ldsn.light.2bpd18": "抱朴(DIM PP-有线版)", "ldsn.light.2hld20": "花轮(DIM11头-遥控版)", "ldsn.light.2hsd15": "怀素(DIM单头-触控\u0026遥控版)", @@ -1005,12 +1075,16 @@ "ldsn.light.2yfd19": "一方(DIM )", "ldsn.light.2ygd05": "摇光(DIM大版-遥控版)", "ldsn.light.3ygd05": "摇光(DIM大版-遥控版)", + "ldsn.light.hsxd01": "怀素吸顶灯", + "ldsn.light.hsxd02": "怀素吸顶灯", + "ldsn.light.ygxd01": "摇光吸顶灯", "leishi.bhf_light.yuba02": "NVC Smart Bath-Heater", "leishi.light.eps112": "NVC LED Smart Ceiling Lamp", "leishi.light.eps113": "DongDong Babysbreath LED Ceiling Lamp", "leishi.light.eps115": "NVC Nordic Style Light", "leishi.light.eps116": "NVC Pendant lamp", "leishi.light.eps117": "NVC Spotlight", + "leishi.light.eps118": "NVC LED Smart Ceiling Lamp", "leishi.light.esp114": "NVC DieYing LED ceiling lamp", "leishi.light.fan01": "NVC Fan Light", "leishi.light.nest": "NVC LED Nest Ceiling Lamp", @@ -1020,6 +1094,7 @@ "leishi.light.wy0a02": "NVC Smart down lamp", "leishi.light.wy0a04": "NVC Smart bulb ", "leishi.light.wy0a05": "NVC Smart panel light ", + "leishi.light.wy0a06": "NVC Intelligent Lighting", "leishi.light.wy0b01": "NVC Smart Desk lamp", "leishi.light.wy0c01": "NVC Smart Ceiling Lamp(ZhiRui)", "leishi.light.wy0c02": "NVC Smart Ceiling Lamp(ZhiZhen)", @@ -1037,6 +1112,9 @@ "lemesh.light.wy0c07": "Scene mesh color temperature lamp TM series", "lemesh.light.wy0c08": "Scene mesh color temperature lamp", "lemesh.switch.sw0a01": "Scene mesh breaker", + "lemesh.switch.sw1a02": "One-click Smart Switch Mesh version", + "lemesh.switch.sw2a02": "Two-button Smart Switch(Mesh)", + "lemesh.switch.sw3a02": "Three-button Smart Switch(Mesh)", "leshi.curtain.v0001": "Scene Curtain WIFI X", "leshi.light.wy0b01": "Scenario WIFI dual color light", "leshi.light.wyfan": "Scene intelligent fan lamp WiFi", @@ -1156,6 +1234,7 @@ "lumi.curtain.hagl05": "Xiaomiyoupin Curtain Controller (Wi-Fi)", "lumi.curtain.hagl08": "Aqara Curtain Controller A1", "lumi.curtain.hmcn01": "Mi Smart Motorized Curtain", + "lumi.curtain.hocn01": "好博推窗器", "lumi.curtain.l07": "智能窗帘电机C2", "lumi.curtain.naq2": "智能窗帘", "lumi.curtain.nes1": "智能窗帘电机(Zigbee开合帘版)", @@ -1328,6 +1407,7 @@ "lumi.switch.v11": "墙壁开关(零火线双键版)", "lumi.vibration.aq1": "Aqara Vibration Sensor", "lumi.weather.v1": "Aqara Temperature and Humidity Sensor", + "lumi.wopener.hocn02": "好博开窗器", "lwkj.bed.5110": "智能床", "lwkj.curtain.2a10": "智能窗帘", "lwkj.light.2010": "彩色灯", @@ -1335,6 +1415,7 @@ "lwkj.light.2210": "单色灯", "lwkj.switch.2410": "智能插座", "lxk.curtain.clmb": "窗帘面板", + "lxk.curtain.cs06": "自动开合帘CS06", "lxk.curtain.dj01w": "窗帘电机", "lxk.hood.n909i": "烟机", "lxk.hood.n913i": "烟机", @@ -1353,6 +1434,7 @@ "madv.cateye.dlowlplus": "Dling Smart Video Doorbell Plus", "madv.cateye.dlowlse": "Dling Smart Video Doorbell C3", "madv.cateye.dlowlse2": "Dling Smart Video Doorbell C5", + "madv.cateye.mi2gt": "Mi Smart Video Doorbell 2", "madv.cateye.miowl": "Mi Smart Video Doorbell", "madv.cateye.miowlv2": "Mi Smart Video Doorbell 2", "madv.cateye.miowlv2l": "Mi Smart Video Doorbell 2 Lite", @@ -1366,6 +1448,8 @@ "maxway.light.mx101": "maxway_light", "mbzn.curtain.m20051": "智能窗帘", "mengye.plug.rmtsw": "电脑远程开关机卡", + "mhaq.aircondition.miair": "三菱重工海尔·智能空调", + "mhaq.airfresh.miwind": "三菱重工海尔·恒温新风机", "mhiot.light.me27w": "WiFi智能灯头", "mhiot.plug.mp01w": "WiFi智能插座", "mhiot.plug.ms86w": "WiFi智能墙壁插座", @@ -1472,6 +1556,7 @@ "morfun.kettle.mf809": "MORFUN Smart Instant Heating Water Dispenser", "morfun.ysj.mf208": "MORFUN Smart Instant Heating Water Dispenser MF208", "moyu.washer.s1hm": "Moyu Smart Baby Washing Machine", + "mpe.bed.bed01": "MPE智能床", "mpe.bed.mpe": "MPE智能床", "mpkx.curtain.050001": "智能窗帘", "mpkx.light.04000a": "灯带", @@ -1501,6 +1586,7 @@ "mxiang.cateye.xmcatt1": "Xiaomo Smart Peep Hole", "nbczwl.airer.airer": "Smart Airer", "nbczwl.airer.kfe01": "KFE", + "nbczwl.airer.zbs01": "ZBS", "nbym.curtain.yznv1": "Cloud intelligent electric curtain", "nhy.airrtc.v1": "Golan Denmark Heating", "nhy.rtc.pexrtc730": "丹麦格澜空调", @@ -1508,13 +1594,14 @@ "ninebot.scooter.v1": "Electric Scooter", "ninebot.scooter.v2": "Mi Electric Scooter Pro", "ninebot.scooter.v3": "Mi Electric Scooter 1S", - "ninebot.scooter.v4": "Mi Electric Scooter Pro 2", + "ninebot.scooter.v4": "Mi Electric Scooter Pro 2 series", "ninebot.scooter.v5": "Mi Electric Scooter Essential", "ninebot.scooter.v6": "Mi Electric Scooter 1S", "nnleaf.light.panels": "Light Panels", "nnleaf.light.strips": "Light Strips", "nnleaf.light.ulp": "Nanoleaf ULP", "noc196.light.mdctd9": "Midian Intelligent Bedside Healthy Lamp", + "noc196.light.mdyctd": "MIDIAN NO BLUE LIGHT MUSIC LAMP", "nuwa.robot.minikiwi": "Nuwa Robotics Danny Robot", "nuwa.robot.nb1": "KebbiAir", "nvc.light.c209": "雷士吸顶灯", @@ -1599,6 +1686,8 @@ "pair.light.3": "开关", "pair.light.4": "调光灯", "pair.light.5": "可调双色温灯", + "pak.light.pak002": "pak ceiling lamp", + "pak.light.pak005": "Pak LED Smart Home Ceiling Lamp", "pak.light.pak01": "Pak LED Smart Ceiling Lamp", "park.switch.fp509": "车位来智能地锁", "permay.switch.kgkgk": "开关", @@ -1664,6 +1753,8 @@ "pwzn.relay.apple": "16 relays module", "pwzn.relay.banana": "16 relays module modbus version", "pze.light.wy0a01": "Puzhuoer Smart Light", + "qdhkl.aircondition.md01": "Sumi central air conditioning controller", + "qhzm.light.wy0a01": "Qinghe Smart Light", "qicyc.bike.tdp02z": "qicybike", "qicyc.bike.xmdzlzxc01qj": "Mi Smart Electric Folding Bike", "qike.bhf_light.qk201801": "Smart Bath(Basic)", @@ -1707,6 +1798,7 @@ "roidmi.cleaner.v2": "ROIDMI Cordless Vacuum Cleaner NEX2", "roidmi.cleaner.v382": "ROIDMI Cordless Vacuum Cleaner NEX2 Pro", "roidmi.vacuum.v1": "ROIDMI Handheld Vacuum Cleaner", + "roidmi.vacuum.v60": "ROIDMI EVE", "rokid.robot.alien": "Rokid Alien", "rokid.robot.alien2": "Rokid Alien", "rokid.robot.me": "Rokid Me", @@ -1746,9 +1838,19 @@ "scishare.coffee.s1102": "SCISHARE Smart Capsule Coffee Machine", "scmkcz.curtain.chcl": "curtain", "scmkcz.light.chll": "light", + "scmkcz.light.cr2l": "双色灯", + "scmkcz.light.cr2lv2": "双色灯", + "scmkcz.light.cr2pv2": "调光调色开关", + "scmkcz.light.crsl": "单色灯", + "scmkcz.light.crslv2": "单色灯", "scmkcz.light.mwl2m": "风扇灯", "scmkcz.plug.chcz": "outlet", + "scmkcz.switch.2msw": "双路开关", + "scmkcz.switch.3msw": "三路开关", + "scmkcz.switch.4msw": "多路开关", "scmkcz.switch.chsw": "switch", + "scmkcz.switch.cr2p": "调光调色开关", + "scmkcz.switch.scene": "场景", "sfl.light.aaas": "FSL AI ceiling lamp", "sfl.light.wl215": "5w调光调色筒灯", "shanhe.switch.1": "善禾物联", @@ -1759,9 +1861,11 @@ "shjszn.lock.kx": "Justree Smart Lock Kx", "shuii.humidifier.jsq001": "The fog free of humidifier", "shuii.humidifier.jsq002": "Zero-fog low-temperature evaporative humidifier (upgrade)", + "shy.heater.h": "智能温控", "sig.curtain.welink": "微联小智-窗帘", "silen.fryer.sck501": "Silencare AirFryer 1.8L", "silen.fryer.sck505": "Silencare AirFryer", + "silen.mfcp.sck307": "Silencare Grill", "simon.aircondition.ac": "空调", "simon.aircondition.bst": "Bus空调", "simon.aircondition.bvt": "Bus新风温控器", @@ -1795,10 +1899,12 @@ "skyrc.airp.su001": "Petoneer Smart Odor Eliminator-Pro", "skyrc.airpurifier.pur": "Petoneer AirMaster", "skyrc.feeder.fed": " Petoneer Nutri Mini Feeder", + "skyrc.feeder.vdfeed": "Nutri Vision Mini", "skyrc.feeder.vfed": " Petoneer Nutri Vision", "skyrc.pet_waterer.fre1": "Fresco Mini Plus", "smartj.curtain.sjd82p": "SmartJoy Intelligent curtain motor (WiFi Pro)", "smartj.curtain.sjdt82": "SmartJoy intelligent curtain motor", + "smartj.light.sjdlds": "SmartJoy Zhizhen Dimmer (Bluetooth Mesh version)", "smartj.plug.sjsc86": "Smartjoy smart wall outlet (WiFi version)", "smartj.switch.sjlh01": "SmartJoy zhijian wall switch (one key WiFi)", "smartj.switch.sjlh02": "SmartJoy zhijian wall switch (two key WiFi)", @@ -1825,6 +1931,8 @@ "soocare.toothbrush.m1s": "Mi Smart Electric Toothbrush T500", "soocare.toothbrush.mc1": "Mi Kids Electric Toothbrush", "soocare.toothbrush.x3": "Soocare Electric Toothbrush", + "srkj.aircondition.v001": "空调", + "srkj.curtain.cl01": "窗帘", "srkj.light.test": "调光灯", "srkj.switch.sw1": "开关", "srkj.waterheater.re01": "热水器", @@ -1974,6 +2082,7 @@ "tuya.stb.tystb01": "STB", "tuya.switch.tyws02": "Wi-Fi Switch", "tuya.tv.tytv01": "TV", + "twzm.light.wy0a01": "Taiwo Magnetic attraction lamp", "txdd.wifispeaker.x1": "Tencent Smart Display", "tyzhjt.aircondition.air": "小小空调", "tyzhjt.airpurifier.001": "空气净化器", @@ -2094,6 +2203,7 @@ "viomi.cooker.v2": "Viomi Rice Cooker (4L Premium Edition)", "viomi.cooker.v4": "Viomi Smart IH Rice Cooker 3L", "viomi.cooker.v5": "Viomi Smart Rice Cooker 3L", + "viomi.curtain.v1": "Viomi Internet curtain motor (Wi-Fi)", "viomi.dishwasher.m01": "Mi Smart Built-in Dishwasher (8 Dining Sets)", "viomi.dishwasher.m02": "Mi Smart Dishwasher (4 Dining Sets)", "viomi.dishwasher.v01": "Viomi Dishwasher", @@ -2103,6 +2213,7 @@ "viomi.dishwasher.v07": "Viomi Smart Protable dishwasher", "viomi.dishwasher.v08": "Viomi Smart dishwahser 8 settings 0803A", "viomi.dishwasher.v09": "Viomi Smart dishwahser 4 settings 0402", + "viomi.dishwasher.v12": "viomi smart dishwasher 8 settings 0803B", "viomi.fan.v5": "Viomi ButterflyFan DC", "viomi.fridge.b1": "Viomi Smart Refrigerator(SBS 380L)", "viomi.fridge.b3": "Viomi Refrigerator(Cross Door 408L)", @@ -2133,6 +2244,9 @@ "viomi.fridge.u39": "Viomi Refrigerator (Cross Door 485L)", "viomi.fridge.u4": "Viomi Smart Refrigerator iLive (side-by-side 483L)", "viomi.fridge.u40": "Viomi Refrigerator iLive (French Door 416L)", + "viomi.fridge.u41": "Viomi Refrigerator (SBS 639L)", + "viomi.fridge.u42": "Viomi Refrigerator (Cross Door 509L)", + "viomi.fridge.u43": "Viomi Refrigerator (French Door 445L)", "viomi.fridge.u44": "Viomi Refrigerator (Cross Door 513L)", "viomi.fridge.u45": "Viomi Refrigerator (French Door 508L)", "viomi.fridge.u6": "Viomi Smart Refrigerator iLive (4-door 486L)", @@ -2193,12 +2307,15 @@ "viomi.oven.so2": "VIOMI Internet steaming and baking all-in-one machine queen (embedded)", "viomi.oven.so3": "Viomi Smart Steaming and Baking Machine King Pro(Build-in)", "viomi.steriliser.v1": "Viomi Disinfection Cabinet (Build-in)", + "viomi.switch.s1": "云米无线开关inkRock(单键版)", "viomi.vacuum.v11": "Viomi V-SLAM Robot Vacuum", "viomi.vacuum.v12": "Viomi Cleanning Robot X2", "viomi.vacuum.v13": "Viomi V3", - "viomi.vacuum.v17": "viomi Alpha 1A", + "viomi.vacuum.v17": "Viomi Robot Vacuum-Mop JC", "viomi.vacuum.v18": "Viomi S9", "viomi.vacuum.v19": "Viomi SE", + "viomi.vacuum.v20": "VIOMI FIERCE-UV", + "viomi.vacuum.v21": "VIOMI ALPHA-UV", "viomi.vacuum.v3": "Viomi sweeper PRO", "viomi.vacuum.v6": "Viomi Cleaning Robot", "viomi.vacuum.v7": "Mi Robot Vacuum-Mop P", @@ -2215,6 +2332,7 @@ "viomi.washer.v13": "Viomi Washer \u0026 Dryer Neo (10kg)", "viomi.washer.v14": "Viomi Washing Machine Neo (10kg)", "viomi.washer.v17": "Viomi Washer\u0026Dryer Eyebot(11kg)", + "viomi.washer.v18": "Viomi Washer\u0026Dryer Eyebot Face 11kg", "viomi.washer.v19": "Viomi Washer\u0026Dryer Neo(10kg DD)", "viomi.washer.v20": "Viomi Washer\u0026Dryer(10kg Navi Version)", "viomi.washer.v21": "Viomi Washing Machine(10kg Navi Version)", @@ -2224,6 +2342,7 @@ "viomi.washer.v26": "Viomi Washer\u0026Dryer Neo2 Pro(10kg DD)", "viomi.washer.v27": "Viomi Washing Machine Neo2 Pro(10kg DD)", "viomi.washer.v28": "Viomi Washer \u0026 Dryer Neo2 (10kg)", + "viomi.washer.v32": "Viomi Washer Eyebot(12KG)", "viomi.washer.v4": "Viomi Washing Drying Machine (8KG)", "viomi.washer.v5": "Viomi Washing Drying Machine (10KG)", "viomi.washer.v6": "Viomi Washer\u0026Dryer Rose 9kg", @@ -2299,6 +2418,7 @@ "wlank.light.001": "调光灯", "wlank.light.002": "RGB灯", "wlank.light.light": "灯光", + "worth.plug.787076": "智能插座", "wxzn.switch.801438": "开关面板", "xbyk.switch.xh01s": "云开关", "xckj.dishwasher.idw01": "Ocooker The Dishwasher", @@ -2312,6 +2432,16 @@ "xckj.waterpuri.ihwp01": "Ocooker Water Purifier", "xckj.waterpuri.js01": "Circle kitchen household water purifier", "xgds.light.wy0a01": "Romne Intelligent lamp", + "xgds.light.wy0a02": "Romne smart ceiling lamp", + "xhuan.light.wy0a01": "Sumi Ruying Light Strip", + "xhuan.light.wy0a02": "Sumi Downlight Lite", + "xhuan.light.wy0a03": "Sumi Magnetic Floodlight", + "xhuan.light.wy0a04": "Sumi Magnetic Grille Lamp", + "xhuan.light.wy0a05": "Sumi Magnetic Spotlight", + "xhuan.light.wy0a06": "Sumi Ruguang Downlight", + "xhuan.light.wy0a07": "Sumi Ruguang Spotlight", + "xhuan.light.wy0a08": "Sumi Bulb Light Lite", + "xhzm.light.wy0a01": "Xinhong ceiling lamp", "xiaomi.aircondition.c10": "Mi Smart Ultra Electricity Saving Vertical Air Conditioner (2HP/Inverter/New China Energy Label Level 1)", "xiaomi.aircondition.c11": "Mi Smart Ultra Electricity Saving Vertical Air Conditioner (3HP/Inverter/New China Energy Label Level 1)", "xiaomi.aircondition.c14": "Mi Smart Ultra Electricity Saving Vertical Air Conditioner (3HP/Inverter/New China Energy Label Level 3)", @@ -2368,6 +2498,7 @@ "xiaomi.router.r4ac": "Mi Router 4A", "xiaomi.router.r4c": "Mi Router 4Q", "xiaomi.router.r4cm": "Mi Router 4C", + "xiaomi.router.ra50": "Redmi路由器AX5 京东云无线宝", "xiaomi.router.ra67": "Redmi路由器AX5", "xiaomi.router.ra69": "Redmi路由器AX6", "xiaomi.router.ra72": "小米路由器AX6000", @@ -2530,7 +2661,10 @@ "yeelink.light.ceil31": "Yeelight Shaohua Ceiling Light A2002", "yeelink.light.ceil32": "Yeelight Ceiling Light E2001", "yeelink.light.ceil33": "Yeelight Ceiling Light for Children C2002", + "yeelink.light.ceil34": "Mi Smart LED Ceiling Light (350mm)", "yeelink.light.ceila": "Yeelight LED Ceiling Light Pro", + "yeelink.light.ceilb": "Yeelight Arwen Ceiling Light 450S/550S", + "yeelink.light.ceilc": "Yeelight Arwen Ceiling Light 450C/550C", "yeelink.light.ceiling1": "Yeelight Ceiling Light", "yeelink.light.ceiling10": "Yeelight Crystal Pendant Lamp", "yeelink.light.ceiling11": "Yeelight Ceiling Light 320 1S", @@ -2562,12 +2696,16 @@ "yeelink.light.color5": "Mi Smart LED Bulb Essential (White and Color)", "yeelink.light.color8": "Yeelight LED Bulb 1S(Color)", "yeelink.light.colora": "Yeelight Smart LED Bulb 1SE (color)", + "yeelink.light.colorb": "Yeelight LED smart bulb W3(Multicolor)", + "yeelink.light.colorc": "Yeelight GU10 smart bulb W1(multicolor)", "yeelink.light.ct2": "Yeelight LED Bulb (Tunable)", + "yeelink.light.cta": "Yeelight LED smart bulb W3(tunable white)", "yeelink.light.dn2grp": "Mi Mesh Downlight", "yeelink.light.dnlight2": "Yeelight Mesh LED Downlight", "yeelink.light.fancl1": "Yeelight Smart Ceiling Fan", "yeelink.light.fancl2": "Yeelight Smart Ceiling Fan S2001", "yeelink.light.fancl5": "Yeelight Smart Ceiling Fan C900", + "yeelink.light.fancl6": "Yeelight Smart Ceiling Fan C1060", "yeelink.light.lamp1": "Mi LED Desk Lamp", "yeelink.light.lamp10": "Yeelight Star Floor Lamp", "yeelink.light.lamp15": "Yeelight Screen Light Bar", @@ -2591,6 +2729,8 @@ "yeelink.light.mono4": "Yeelight LED Bulb 1S(Dimmable)", "yeelink.light.mono5": "Yeelight LED Filament Bulb", "yeelink.light.mono6": "Mi Smart LED Bulb", + "yeelink.light.monoa": "Yeelight LED smart bulb W3(dimmable)", + "yeelink.light.monob": "Yeelight GU10 Smart Bulb W1(dimmable)", "yeelink.light.nl1": "Mi Motion-Activated Night Light 2 (Bluetooth)", "yeelink.light.panel1": "Yeelight Whiteglow Panel Light", "yeelink.light.panel3": "Yeelight Haobai LED Panel light Pro", @@ -2629,6 +2769,7 @@ "ymj.light.wyymj1": "EMG Top halo ceiling lamp", "ymt.flowerpot.v1": "Yimitian smart food", "yongqi.aircondition.ac": "空调", + "yongqi.airfresh.fw": "新风", "yongqi.curtain.curt": "窗帘", "yongqi.switch.yq0001": "开关", "yonsz.aircondition.air": "红外空调", @@ -2646,6 +2787,7 @@ "yszj.aircondition.f0025": "空调遥控器", "yszj.bed.5110": "智能床垫", "yszj.curtain.2a10": "智能窗帘", + "yszj.curtain.2a10a": "智能窗帘2", "yszj.light.2010": "彩色灯", "yszj.light.2110": "吸顶灯", "yszj.light.2210": "单色灯", @@ -2684,6 +2826,8 @@ "yunmi.waterpuri.f4": "Viomi Smart Water purifier Easy3 (400G)", "yunmi.waterpuri.f5": "Viomi Smart Water purifier Easy3 (600G)", "yunmi.waterpuri.f6": "Viomi Smart Water purifier Easy3 (800G)", + "yunmi.waterpuri.f7": "viomi smart water purifier Fast3 1000G", + "yunmi.waterpuri.f8": "viomi smart water purifier Surging 1000G", "yunmi.waterpuri.lx11": "Mi Water Purifier C1 (Triple Setting)", "yunmi.waterpuri.lx12": "Mi Water Purifier S1", "yunmi.waterpuri.lx14": "Mi Water Purifier H1000G", @@ -2700,6 +2844,10 @@ "yunmi.waterpuri.s11": "Viomi Smart Water purifier Blues (400G)", "yunmi.waterpuri.s12": "Viomi Smart Water purifier Blues (600G)", "yunmi.waterpuri.s14": "云米·泉先互联网净水器 小白龙 600G", + "yunmi.waterpuri.s15": "viomi smart water purifier dolphin 400G", + "yunmi.waterpuri.s16": "viomi smart water purifier dolphin 600G", + "yunmi.waterpuri.s17": "viomi smart water purifier Fast3 800G", + "yunmi.waterpuri.s18": "viomi smart water purifier Mee 600G", "yunmi.waterpuri.s3": "Viomi smart water purifier SE(400G)", "yunmi.waterpuri.s4": "Viomi smart water purifier S2(500G)", "yunmi.waterpuri.s5": "Viomi smart water purifier S2(600G)", @@ -2847,6 +2995,7 @@ "zhuyun.curtain.1": "电机控制", "zhuyun.light.22": "智能LED灯", "zhuyun.light.zy22": "智能LED灯3", + "zichen.light.wy0a01": "Xiao ran all copper Smart Light", "zigma.airp.aerio": "Zigma空气净化器系列", "zigma.vacuum.laser": "Zigma扫地机系列", "zimi.clock.myk01": "Mi AI Alarm", diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ReadmeHelper.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ReadmeHelper.java index c14d4cbf2ef59..6366c51210969 100644 --- a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ReadmeHelper.java +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ReadmeHelper.java @@ -292,8 +292,7 @@ JsonObject convertFileToJSON(String fileName) { JsonObject jsonObject = new JsonObject(); try { - JsonParser parser = new JsonParser(); - JsonElement jsonElement = parser.parse(new FileReader(fileName)); + JsonElement jsonElement = JsonParser.parseReader(new FileReader(fileName)); jsonObject = jsonElement.getAsJsonObject(); } catch (FileNotFoundException e) { // diff --git a/bundles/org.openhab.binding.millheat/src/main/java/org/openhab/binding/millheat/internal/client/RequestLogger.java b/bundles/org.openhab.binding.millheat/src/main/java/org/openhab/binding/millheat/internal/client/RequestLogger.java index 742a9a5dc3cc9..2b1e4f245ff04 100644 --- a/bundles/org.openhab.binding.millheat/src/main/java/org/openhab/binding/millheat/internal/client/RequestLogger.java +++ b/bundles/org.openhab.binding.millheat/src/main/java/org/openhab/binding/millheat/internal/client/RequestLogger.java @@ -41,12 +41,10 @@ public final class RequestLogger { private final Logger logger = LoggerFactory.getLogger(RequestLogger.class); private final AtomicLong nextId = new AtomicLong(); - private final JsonParser parser; private final Gson gson; private final String prefix; public RequestLogger(final String prefix, final Gson gson) { - this.parser = new JsonParser(); this.gson = gson; this.prefix = prefix; } @@ -124,7 +122,7 @@ public Request listenTo(final Request request) { private String reformatJson(final String jsonString) { try { - final JsonElement json = parser.parse(jsonString); + final JsonElement json = JsonParser.parseString(jsonString); return gson.toJson(json); } catch (final JsonSyntaxException e) { logger.debug("Could not reformat malformed JSON due to '{}'", e.getMessage()); diff --git a/bundles/org.openhab.binding.modbus.e3dc/README.md b/bundles/org.openhab.binding.modbus.e3dc/README.md index 0fad11771f86e..01499f575fde2 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/README.md +++ b/bundles/org.openhab.binding.modbus.e3dc/README.md @@ -85,32 +85,32 @@ The E3DC device offers quite an amount of channels. For clustering 4 Channel Gro ### Channel Group _Power Block_ -| Channel Label | Channel Group ID | Channel ID | Type | Description | -|-------------------------------|-------------------|------------------------------|------------------------|------------------------------| -| PV Output | power | pv-power-supply | Number:Power | Photovoltaic Power Production | -| Battery Discharge | power | battery-power-supply | Number:Power | Battery discharges and provides Power | -| Battery Charge | power | battery-power-consumption | Number:Power | Battery charges and consumes Power | -| Household Consumption | power | household-power-consumption | Number:Power | Household consuming Power | -| Grid Power Consumption | power | grid-power-consumption | Number:Power | Grid Power is needed in order to satisfy your overall Power consumption | -| Grid Power Supply | power | grid-power-supply | Number:Power | More Photovoltaic Power is produced than needed. Additional Power is provided towards the Grid | -| External Power Supply | power | external-power-supply | Number:Power | Power produced by an external device which is attached to your E3DC device | -| Wallbox Power Consumption | power | wallbox-power-consumption | Number:Power | Power consumption of attached Wallboxes | -| Wallbox PV Power Consumption | power | wallbox-pv-power-consumption | Number:Power | Photovoltaic Power consumption (PV plus Battery) of attached Wallboxes | -| Autarky | power | autarky-channel | Number:Dimensionless | Your current Autarky Level in Percent | -| Self Consumption | power | self-consumption | Number:Dimensionless | Your current Photovoltaic Self Consumption Level in Percent | -| Battery State Of Charge | power | battery-soc | Number:Dimensionless | Charge Level of your attached Battery in Percent | +| Channel Label | Channel Group ID | Channel ID | Type | Description | +|-------------------------------|-------------------|------------------------------|------------------------|----------------------------------------------------------------------------------------| +| PV Output | power | pv-power-supply | Number:Power | Photovoltaic Power Production | +| Battery Discharge | power | battery-power-supply | Number:Power | Battery discharges and provides Power | +| Battery Charge | power | battery-power-consumption | Number:Power | Battery charges and consumes Power | +| Household Consumption | power | household-power-consumption | Number:Power | Household consuming Power | +| Grid Power Consumption | power | grid-power-consumption | Number:Power | More Photovoltaic Power is produced than needed. Additional Power is consumed by Grid | +| Grid Power Supply | power | grid-power-supply | Number:Power | Grid Power is needed in order to satisfy your overall Power consumption | +| External Power Supply | power | external-power-supply | Number:Power | Power produced by an external device which is attached to your E3DC device | +| Wallbox Power Consumption | power | wallbox-power-consumption | Number:Power | Power consumption of attached Wallboxes | +| Wallbox PV Power Consumption | power | wallbox-pv-power-consumption | Number:Power | Photovoltaic Power consumption (PV plus Battery) of attached Wallboxes | +| Autarky | power | autarky-channel | Number:Dimensionless | Your current Autarky Level in Percent | +| Self Consumption | power | self-consumption | Number:Dimensionless | Your current Photovoltaic Self Consumption Level in Percent | +| Battery State Of Charge | power | battery-soc | Number:Dimensionless | Charge Level of your attached Battery in Percent | ### Channel Group _String Details Block_ -| Channel Label | Channel Group ID | Channel ID | Type | Description | -|-----------------------|------------------|--------------------|---------------------------|------------------------------| -| String 1 Potential | strings | string1-dc-voltage | Number:ElectricPotential | Voltage on String 1 | -| String 2 Potential | strings | string2-dc-voltage | Number:ElectricPotential | Voltage on String 2 | -| String 3 Potential | strings | string3-dc-voltage | Number:ElectricPotential | Voltage on String 3 | -| String 1 Current | strings | string1-dc-current | Number:ElectricCurrent | Current on String 1 | -| String 2 Current | strings | string2-dc-current | Number:ElectricCurrent | Current on String 2 | -| String 3 Current | strings | string3-dc-current | Number:ElectricCurrent | Current on String 3 | +| Channel Label | Channel Group ID | Channel ID | Type | Description | +|-----------------------|------------------|--------------------|---------------------------|----------------------------| +| String 1 Potential | strings | string1-dc-voltage | Number:ElectricPotential | Voltage on String 1 | +| String 2 Potential | strings | string2-dc-voltage | Number:ElectricPotential | Voltage on String 2 | +| String 3 Potential | strings | string3-dc-voltage | Number:ElectricPotential | Voltage on String 3 | +| String 1 Current | strings | string1-dc-current | Number:ElectricCurrent | Current on String 1 | +| String 2 Current | strings | string2-dc-current | Number:ElectricCurrent | Current on String 2 | +| String 3 Current | strings | string3-dc-current | Number:ElectricCurrent | Current on String 3 | | String 1 Power | strings | string1-dc-output | Number:Power | Power produced by String 1 | | String 2 Power | strings | string2-dc-output | Number:Power | Power produced by String 2 | | String 3 Power | strings | string3-dc-output | Number:Power | Power produced by String 3 | @@ -175,18 +175,18 @@ String E3DC_ModelName "E3DC Model" (e3dc) String E3DC_Firmware "E3DC Modbus ID" (e3dc) { channel="modbus:e3dc:device:powerplant:info#firmware-release" } String E3DC_SerialNumber "E3DC Modbus ID" (e3dc) { channel="modbus:e3dc:device:powerplant:info#serial-number" } -Number:Power E3DC_PVPower "E3DC PV Power" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#pv-power-supply" } -Number:Power E3DC_BatteryDischarge "E3DC Battery Discharge" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-power-supply" } -Number:Power E3DC_BatteryCharge "E3DC Battery Charge" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-power-consumption" } -Number:Power E3DC_Household "E3DC Household Consumption" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#household-power-consumption" } -Number:Power E3DC_GridConsumption "E3DC Grid Consumption" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#grid-power-consumption" } -Number:Power E3DC_GridSupply "E3DC Grid Supply " (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#grid-power-supply" } -Number:Power E3DC_ExternalSupply "E3DC External Supply" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#external-power-supply" } -Number:Power E3DC_WallboxConsumption "E3DC Wallbox Consumption" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#wallbox-power-consumption" } -Number:Power E3DC_WallboxPVConsumption "E3DC Wallbox PV Consumption" (e3dc) { channel="modbus:e3dc:device:powerplant:power#wallbox-pv-power-consumption" } -Number:Dimensionless E3DC_AutarkyLevel "E3DC Autarky Level" (e3dc) { channel="modbus:e3dc:device:powerplant:power#autarky" } -Number:Dimensionless E3DC_SelfConsumptionLevel "E3DC Self Consumption Level" (e3dc) { channel="modbus:e3dc:device:powerplant:power#self-consumption" } -Number:Dimensionless E3DC_BatterySOC "E3DC Battery SOC" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-soc" } +Number:Power E3DC_PVPower "E3DC PV Power" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#pv-power-supply" } +Number:Power E3DC_BatteryDischarge "E3DC Battery Discharge" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-power-supply" } +Number:Power E3DC_BatteryCharge "E3DC Battery Charge" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-power-consumption" } +Number:Power E3DC_Household "E3DC Household Consumption" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#household-power-consumption" } +Number:Power E3DC_GridConsumption "E3DC Power to Grid" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#grid-power-consumption" } +Number:Power E3DC_GridSupply "E3DC Power from Grid" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#grid-power-supply" } +Number:Power E3DC_ExternalSupply "E3DC External Supply" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#external-power-supply" } +Number:Power E3DC_WallboxConsumption "E3DC Wallbox Consumption" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#wallbox-power-consumption" } +Number:Power E3DC_WallboxPVConsumption "E3DC Wallbox PV Consumption" (e3dc) { channel="modbus:e3dc:device:powerplant:power#wallbox-pv-power-consumption" } +Number:Dimensionless E3DC_AutarkyLevel "E3DC Autarky Level" (e3dc) { channel="modbus:e3dc:device:powerplant:power#autarky" } +Number:Dimensionless E3DC_SelfConsumptionLevel "E3DC Self Consumption Level" (e3dc) { channel="modbus:e3dc:device:powerplant:power#self-consumption" } +Number:Dimensionless E3DC_BatterySOC "E3DC Battery SOC" (e3dc,persist) { channel="modbus:e3dc:device:powerplant:power#battery-soc" } Switch E3DC_WB_Available "E3DC WB available" (e3dc) { channel="modbus:e3dc-wallbox:device:powerplant:wallbox0:wb-available" } Switch E3DC_WB_Sunmode "E3DC WB Sunmode" (e3dc) { channel="modbus:e3dc-wallbox:device:powerplant:wallbox0:wb-sunmode" } @@ -332,7 +332,7 @@ I like the Grafana approach and I used the [InfluxDB & Grafana Tutorial](https:/ from the Community to set this up. I prepared my machine and I'm quite pleased with the results. - + In the above picture there are two graphs @@ -368,6 +368,6 @@ Items { Having these values in the timeline you're able to cross check how the forecast influences the Photovoltaic Production. - + I personally would like to have more steering control of the E3DC to react on such forecast e.g. "stop charging the car if it gets too cloudy" diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/i18n/e3dc_de.properties b/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/i18n/e3dc_de.properties index 2fc6bb963ceb0..ff76bc4546159 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/i18n/e3dc_de.properties +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/i18n/e3dc_de.properties @@ -59,8 +59,8 @@ channel-type.modbus.pv-power-supply-channel.label = Photovoltaik Leistung channel-type.modbus.battery-power-supply-channel.label = Batterie Entladen channel-type.modbus.battery-power-consumption-channel.label = Batterie Laden channel-type.modbus.household-power-consumption-channel.label = Haushalt Verbrauch -channel-type.modbus.grid-power-consumption-channel.label = Stromnetz Verbrauch -channel-type.modbus.grid-power-supply-channel.label = Stromnetz Einspeisung +channel-type.modbus.grid-power-consumption-channel.label = Stromnetz Einspeisung +channel-type.modbus.grid-power-supply-channel.label = Stromnetz Verbrauch channel-type.modbus.external-power-supply-channel.label = Externer Stromproduzent channel-type.modbus.wallbox-power-consumption-channel.label = Wallbox Ladestrom channel-type.modbus.wallbox-pv-power-consumption-channel.label = Wallbox Photovoltaik Ladestrom diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/thing/power-channel-types.xml b/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/thing/power-channel-types.xml index 2c607b1138b87..57035ee7b0702 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/thing/power-channel-types.xml +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/resources/OH-INF/thing/power-channel-types.xml @@ -26,12 +26,12 @@ Number:Power - Grid Power is needed in order to satisfy your overall Power consumption + More Photovoltaic Power is produced than needed. Additional Power is consumed by Grid Number:Power - More Photovoltaic Power is produced than needed. Additional Power is provided towards the Grid + Grid Power is needed in order to satisfy your overall Power consumption Number:Power diff --git a/bundles/org.openhab.binding.modbus.studer/README.md b/bundles/org.openhab.binding.modbus.studer/README.md index f03eca6bdddb5..019bc163f0aa0 100644 --- a/bundles/org.openhab.binding.modbus.studer/README.md +++ b/bundles/org.openhab.binding.modbus.studer/README.md @@ -28,6 +28,25 @@ For defining a thing textually, you have to find out the start address of the mo While the length is usually fixed, the address is not. Please refer to your device's vendor documentation how model blocks are laid for your equipment. +OR: If there is no offset configured (default config) on the dip switch in RS-485, the following are mostly interesting for getting things up and running: + +|--------|-----------------------| +| Offset | Device | +|--------|-----------------------| +| 10 | Multicast Xtender | +| 11-19 | Xtender 1-9 | +| 20 | Multicast Variotrack | +| 21-35 | Variotrack 1-15 | +| 40 | Multicast Variostring | +| 41-55 | Variostring 1-15 | +| 61 | BSP/Xcom-CAN | +|--------|-----------------------| + +More Details about that can be found in the technical specification and appendix for Studer RTU Modbus protocol. Check default config (dip switches 1 and 2 off) while configuring the pin-out on the RS-485! + +Multicast writes on any devices of given class, but reads only on the first available device (Not Summary!). As currently there are no writes available, 10/20/40 is useless for now. + + The following parameters are valid for all thing types: | Parameter | Type | Required | Default if omitted | Description | @@ -119,29 +138,48 @@ Bridge modbus:tcp:bridge [host="192.168.178.56", port=502, rtuEncoded=true] ... -Thing modbus:xtender:bridge:xtenderdevice "Xtender" (modbus:serial:modbusbridge) [ slaveAddress=10, refresh=5 ] +Thing modbus:xtender:bridge:xtender_Phase1 "Xtender" (modbus:serial:modbusbridge) [ slaveAddress=11, refresh=5 ] +Thing modbus:variostring:bridge:variostring_left "Xtender" (modbus:serial:modbusbridge) [ slaveAddress=41, refresh=5 ] +Thing modbus:variostring:bridge:variostring_right "Xtender" (modbus:serial:modbusbridge) [ slaveAddress=42, refresh=5 ] +Thing modbus:bsp:bridge:byd "BydBox" (modbus:serial:modbusbridge) [ slaveAddress=61, refresh=5 ] ``` + Note: Make sure that refresh and slave address are numerical, without quotes. ### Item Configuration ``` -Number XtenderStuderThing_InputVoltage "Input Voltage [%.2f %unit%]" -{channel="modbus:xtender:bridge:xtenderdevice:inputVoltage"} - -Number XtenderStuderThing_InputCurrent "Input Current [%.2f %unit%]" {channel="modbus:xtender:bridge:xtenderdevice:inputCurrent"} - -String XtenderStuderThing_StateInverter "State: [%s]" {channel="modbus:xtender:bridge:xtenderdevice:stateInverter"} +Number Studer_Xtender_Phase1_InputVoltage "Input Voltage [%.2f V]" {channel="modbus:xtender:bridge:xtender_Phase1:inputVoltage"} +Number Studer_Xtender_Phase1_InputCurrent "Input Current [%.2f A]" {channel="modbus:xtender:bridge:xtender_Phase1:inputCurrent"} +String Studer_Xtender_Phase1_StateInverter "State: [%s]" {channel="modbus:xtender:bridge:xtender_Phase1:stateInverter"} + +Number Studer_PVCurrent_Left "Current [%.2f]" {channel="modbus:variostring:bridge:variostring_left:PVCurrent"} +Number Studer_PVPower_Left "Power" {channel="modbus:variostring:bridge:variostring_left:PVPower"} +Number Studer_ProductionPVCurrentDay_Left "ProductionCurrentDay [%.3f kW]" {channel="modbus:variostring:bridge:variostring_left:ProductionPVCurrentDay"} +String Studer_PVMode_Left "Mode: [%s]" {channel="modbus:variostring:bridge:variostring_left:PVMode"} +String Studer_stateVarioString_Left "State: [%s]" {channel="modbus:variostring:bridge:variostring_left:stateVarioString"} + +Number Studer_BSP_SOC "State: [%s]" {channel="modbus:bsp:bridge:byd:stateOfCharge"} +Number Studer_BSP_batteryVoltage "Battery Voltage: [%s]" {channel="modbus:bsp:bridge:byd:batteryVoltage"} ``` ### Sitemap Configuration ``` -Text item=XtenderStuderThing_InputVoltage -Text item=XtenderStuderThing_InputCurrent -Text item=XtenderStuderThing_StateInverter +Text item=Studer_Xtender_Phase1_InputVoltage +Text item=Studer_Xtender_Phase1_InputCurrent +Text item=Studer_Xtender_Phase1_StateInverter -Chart item=XtenderStuderThing_InputVoltage period=D refresh=600000 -Chart item=XtenderStuderThing_InputCurrent period=D refresh=30000 -``` \ No newline at end of file +Chart item=Studer_Xtender_Phase1_InputVoltage period=D refresh=600000 +Chart item=Studer_Xtender_Phase1_InputCurrent period=D refresh=30000 + +Text item=Studer_BSP_SOC +Text item=Studer_BSP_batteryVoltage + +Text item=Studer_PVCurrent_Left +Text item=Studer_PVPower_Left +Text item=Studer_ProductionPVCurrentDay_Left +Text item=Studer_PVMode_Left +Text item=Studer_stateVarioString_Left +``` diff --git a/bundles/org.openhab.binding.modbus.sunspec/README.md b/bundles/org.openhab.binding.modbus.sunspec/README.md index 9a258f352eef0..18f44668f1773 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/README.md +++ b/bundles/org.openhab.binding.modbus.sunspec/README.md @@ -109,7 +109,7 @@ This group contains summarized values for the power meter over all phases. | ac-total-reactive-power | Number:Power | Total Reactive Power over all phases (W) | | ac-average-power-factor | Number:Dimensionless | Average AC Power Factor over all phases (%) | | ac-total-exported-real-energy | Number:Energy | Total Real Energy Exported over all phases (Wh) | -| ac-total-imported-real-energy | Number:Energy | Total Real Energy Imported over all phases (Wh) | +| ac-total-imported-real-energy | Number:Energy | Total Real Energy Imported over all phases (Wh) | | ac-total-exported-apparent-energy | Number:Energy | Total Apparent Energy Exported over all phases (VAh) | | ac-total-imported-apparent-energy | Number:Energy | Total Apparent Energy Imported over all phases (VAh) | | ac-total-imported-reactive-energy-q1 | Number:Energy | Total Reactive Energy Imported Quadrant 1 over all phases (VARh) | @@ -134,7 +134,7 @@ acPhaseC: available only for inverter-three-phase type inverters. | Channel ID | Item Type | Description | |----------------------|--------------------------|---------------------------------------------------------------------| -| ac-phase-current | Number:ElectricCurrent | Actual current over this phase in Watts | +| ac-phase-current | Number:ElectricCurrent | Actual current over this phase in Ampere | | ac-voltage-to-next | Number:ElectricPotential | Voltage of this phase relative to the next phase, or to the ground in case of single phase inverter. Note: some single phase SolarEdge inverters incorrectly use this value to report the voltage to neutral value| | ac-voltage-to-n | Number:ElectricPotential | Voltage of this phase relative to the ground | diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml index fa0506bbc02a8..0ee3ccb91824a 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml @@ -20,14 +20,14 @@ Number:ElectricPotential This phase's AC voltage relative to the next phase - + Number:ElectricPotential This phase's AC voltage relative to N line - + diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/meter-channel-types.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/meter-channel-types.xml index 2187f9bc8c86e..c36244b677c57 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/meter-channel-types.xml +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/meter-channel-types.xml @@ -7,12 +7,12 @@ Number:ElectricPotential - + Number:ElectricPotential - + Number:Power diff --git a/bundles/org.openhab.binding.modbus/README.md b/bundles/org.openhab.binding.modbus/README.md index c6f561aa443c1..ae30bbe53e4f2 100644 --- a/bundles/org.openhab.binding.modbus/README.md +++ b/bundles/org.openhab.binding.modbus/README.md @@ -146,7 +146,7 @@ Basic parameters | stopBits | text | ✓ | | Stop bits. Valid values are: `"1.0"`, `"1.5"`, `"2.0"`. | | | parity | text | ✓ | | Parity. Valid values are: `"none"`, `"even"`, `"odd"`. | | | dataBits | integer | ✓ | | Data bits. Valid values are: `5`, `6`, `7` and `8`. | | -| encoding | text | ✓ | | Encoding. Valid values are: `"ascii"`, `"rtu"`, `"bin"`. | | +| encoding | text | | `"rtu"` | Encoding. Valid values are: `"ascii"`, `"rtu"`, `"bin"`. | | | echo | boolean | | `false` | Flag for setting the RS485 echo mode. This controls whether we should try to read back whatever we send on the line, before reading the response. Valid values are: `true`, `false`. | | Advanced parameters diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java index 32804d38c4ecc..8e81973a0ec52 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java @@ -24,19 +24,19 @@ @NonNullByDefault public class ModbusSerialConfiguration { private @Nullable String port; - private int id; + private int id = 1; private int baud; private @Nullable String stopBits; private @Nullable String parity; private int dataBits; - private @Nullable String encoding; + private String encoding = "rtu"; private boolean echo; - private int receiveTimeoutMillis; - private @Nullable String flowControlIn; - private @Nullable String flowControlOut; - private int timeBetweenTransactionsMillis; - private int connectMaxTries; - private int connectTimeoutMillis; + private int receiveTimeoutMillis = 1500; + private String flowControlIn = "none"; + private String flowControlOut = "none"; + private int timeBetweenTransactionsMillis = 35; + private int connectMaxTries = 1; + private int connectTimeoutMillis = 10_000; private boolean enableDiscovery; public @Nullable String getPort() { diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java index 28ff1d2a2c991..f565f2082a58f 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java @@ -25,12 +25,12 @@ public class ModbusTcpConfiguration { private @Nullable String host; private int port; - private int id; - private int timeBetweenTransactionsMillis; + private int id = 1; + private int timeBetweenTransactionsMillis = 60; private int timeBetweenReconnectMillis; - private int connectMaxTries; + private int connectMaxTries = 1; private int reconnectAfterMillis; - private int connectTimeoutMillis; + private int connectTimeoutMillis = 10_000; private boolean enableDiscovery; private boolean rtuEncoded; diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java index ceb2e7f8f5b47..35e6dd1c605db 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java @@ -45,7 +45,7 @@ public abstract class AbstractModbusEndpointThingHandler 100) { + updateChannel(CHANNEL_VOLUME, UnDefType.UNDEF); + } else { + updateChannel(CHANNEL_VOLUME, new PercentType(volume)); + } State newControlState = UnDefType.UNDEF; switch (status.getState()) { diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java index 6182f79266451..c2566c264cc9a 100644 --- a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java @@ -347,10 +347,7 @@ public void connectMQTT() { "Bridge is missing or offline, you need to setup a working MQTT broker first."); return; } - ThingUID thingUID = localBridge.getBridgeUID(); - if (thingUID == null) { - return; - } + ThingUID thingUID = localBridge.getUID(); Thing thing = thingRegistry.get(thingUID); if (thing == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelState.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelState.java index 878a01d83bfed..88138a9c05e1e 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelState.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelState.java @@ -21,7 +21,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.values.TextValue; @@ -78,7 +77,7 @@ public ChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedVal this.channelStateUpdateListener = channelStateUpdateListener; this.channelUID = channelUID; this.cachedValue = cachedValue; - this.readOnly = StringUtils.isBlank(config.commandTopic); + this.readOnly = config.commandTopic.isBlank(); } public boolean isReadOnly() { @@ -242,7 +241,7 @@ public boolean isStateful() { */ public CompletableFuture<@Nullable Void> stop() { final MqttBrokerConnection connection = this.connection; - if (connection != null && StringUtils.isNotBlank(config.stateTopic)) { + if (connection != null && !config.stateTopic.isBlank()) { return connection.unsubscribe(config.stateTopic, this).thenRun(this::internalStop); } else { internalStop(); @@ -297,7 +296,7 @@ private void receivedOrTimeout() { this.connection = connection; - if (StringUtils.isBlank(config.stateTopic)) { + if (config.stateTopic.isBlank()) { return CompletableFuture.completedFuture(null); } diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/GenericMQTTThingHandler.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/GenericMQTTThingHandler.java index 3a991d9471f16..8cc05429ec1b6 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/GenericMQTTThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/GenericMQTTThingHandler.java @@ -21,7 +21,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler; @@ -164,9 +164,8 @@ public void initialize() { Value value = ValueFactory.createValueState(channelConfig, channelTypeUID.getId()); ChannelState channelState = createChannelState(channelConfig, channel.getUID(), value); channelStateByChannelUID.put(channel.getUID(), channelState); - StateDescription description = value - .createStateDescription(StringUtils.isBlank(channelConfig.commandTopic)).build() - .toStateDescription(); + StateDescription description = value.createStateDescription(channelConfig.commandTopic.isBlank()) + .build().toStateDescription(); if (description != null) { stateDescProvider.setDescription(channel.getUID(), description); } diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java index 9b5f3aab601fc..1e4ffa3c0d9e4 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java @@ -12,12 +12,13 @@ */ package org.openhab.binding.mqtt.generic.values; +import static java.util.function.Predicate.not; + import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.CoreItemFactory; @@ -45,7 +46,7 @@ public class TextValue extends Value { */ public TextValue(String[] states) { super(CoreItemFactory.STRING, Collections.singletonList(StringType.class)); - Set s = Stream.of(states).filter(e -> StringUtils.isNotBlank(e)).collect(Collectors.toSet()); + Set s = Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet()); if (!s.isEmpty()) { this.states = s; } else { diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java index ff8002c07a2af..57e593e7b837c 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.mqtt.generic.values; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mqtt.generic.ChannelConfig; import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants; @@ -35,7 +34,7 @@ public static Value createValueState(ChannelConfig config, String channelTypeID) Value value; switch (channelTypeID) { case MqttBindingConstants.STRING: - value = StringUtils.isBlank(config.allowedStates) ? new TextValue() + value = config.allowedStates.isBlank() ? new TextValue() : new TextValue(config.allowedStates.split(",")); break; case MqttBindingConstants.DATETIME: diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java index 8961fe6026eba..f8862fc7dd6e8 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java @@ -16,7 +16,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; @@ -54,8 +53,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } private boolean isHomeassistantDynamicType(ThingTypeUID thingTypeUID) { - return StringUtils.equals(MqttBindingConstants.BINDING_ID, thingTypeUID.getBindingId()) - && StringUtils.startsWith(thingTypeUID.getId(), MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()); + return MqttBindingConstants.BINDING_ID.equals(thingTypeUID.getBindingId()) + && thingTypeUID.getId().startsWith(MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()); } @Activate diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java index 854de33b85d17..bb17b03530cf1 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java @@ -16,7 +16,6 @@ import java.util.Map; import java.util.Objects; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.thing.Thing; @@ -103,9 +102,9 @@ static class Device { protected @Nullable String name; protected @Nullable String sw_version; - @Nullable - public String getId() { - return StringUtils.join(identifiers, "_"); + public @Nullable String getId() { + List identifiers = this.identifiers; + return identifiers == null ? null : String.join("_", identifiers); } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java index 4eca5244e56ba..cffc7027d25b0 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java @@ -16,7 +16,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.ChannelConfigBuilder; @@ -149,9 +148,9 @@ public Builder stateTopic(@Nullable String state_topic) { public Builder stateTopic(@Nullable String state_topic, @Nullable String... templates) { this.state_topic = state_topic; - if (StringUtils.isNotBlank(state_topic)) { + if (state_topic != null && !state_topic.isBlank()) { for (String template : templates) { - if (StringUtils.isNotBlank(template)) { + if (template != null && !template.isBlank()) { this.templateIn = template; break; } @@ -204,7 +203,8 @@ public CChannel build(boolean addToComponent) { .withCommandTopic(command_topic).makeTrigger(trigger).build(), channelUID, valueState, channelStateUpdateListener); - if (StringUtils.isBlank(state_topic) || this.trigger) { + String localStateTopic = state_topic; + if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) { type = ChannelTypeBuilder.trigger(channelTypeUID, label) .withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL)).build(); } else { diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java index 563e39fe89421..5d61e981aeb87 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.lang.reflect.Field; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -119,7 +118,7 @@ private void expandTidleInTopics(BaseChannelConfiguration config) { final String oldValue = (String) field.get(config); String newValue = oldValue; - if (StringUtils.isNotBlank(oldValue)) { + if (oldValue != null && !oldValue.isBlank()) { if (oldValue.charAt(0) == '~') { newValue = tilde + oldValue.substring(1); } else if (oldValue.charAt(oldValue.length() - 1) == '~') { diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLock.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLock.java index fb78f52358bac..aad866a2d155e 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLock.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLock.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.mqtt.homeassistant.internal; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.values.OnOffValue; @@ -46,7 +45,7 @@ public ComponentLock(CFactory.ComponentConfiguration componentConfiguration) { super(componentConfiguration, ChannelConfiguration.class); // We do not support all HomeAssistant quirks - if (channelConfiguration.optimistic && StringUtils.isNotBlank(channelConfiguration.state_topic)) { + if (channelConfiguration.optimistic && !channelConfiguration.state_topic.isBlank()) { throw new UnsupportedOperationException("Component:Lock does not support forced optimistic mode"); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java index e3769c002868a..2e18a3a2dd6f1 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; @@ -61,7 +60,7 @@ public ComponentSensor(CFactory.ComponentConfiguration componentConfiguration) { String uom = channelConfiguration.unit_of_measurement; - if (uom != null && StringUtils.isNotBlank(uom)) { + if (uom != null && !uom.isBlank()) { value = new NumberValue(null, null, null, uom); } else { value = new TextValue(); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java index 3326572f65d40..87f179d132b27 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.mqtt.homeassistant.internal; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.values.OnOffValue; @@ -52,9 +51,9 @@ public ComponentSwitch(CFactory.ComponentConfiguration componentConfiguration) { super(componentConfiguration, ChannelConfiguration.class); boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic - : StringUtils.isBlank(channelConfiguration.state_topic); + : channelConfiguration.state_topic.isBlank(); - if (optimistic && StringUtils.isNotBlank(channelConfiguration.state_topic)) { + if (optimistic && !channelConfiguration.state_topic.isBlank()) { throw new UnsupportedOperationException("Component:Switch does not support forced optimistic mode"); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java index 48eba3bab940b..c7f895cd4557e 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Collection; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.core.Configuration; @@ -93,7 +92,7 @@ private HaID(String baseTopic, String objectID, String nodeID, String component) private static final String createTopic(HaID id) { StringBuilder str = new StringBuilder(); str.append(id.baseTopic).append('/').append(id.component).append('/'); - if (StringUtils.isNotBlank(id.nodeID)) { + if (!id.nodeID.isBlank()) { str.append(id.nodeID).append('/'); } str.append(id.objectID).append('/'); @@ -175,7 +174,7 @@ public static Collection fromConfig(HandlerConfiguration config) { */ public String toShortTopic() { String objectID = this.objectID; - if (StringUtils.isNotBlank(nodeID)) { + if (!nodeID.isBlank()) { objectID = nodeID + "/" + objectID; } @@ -192,10 +191,10 @@ public String getGroupId(@Nullable final String uniqueId) { String result = uniqueId; // the null test is only here so the compile knows, result is not null afterwards - if (result == null || StringUtils.isBlank(result)) { + if (result == null || result.isBlank()) { StringBuilder str = new StringBuilder(); - if (StringUtils.isNotBlank(nodeID)) { + if (!nodeID.isBlank()) { str.append(nodeID).append('_'); } str.append(objectID).append('_').append(component); diff --git a/bundles/org.openhab.binding.mqtt/README.md b/bundles/org.openhab.binding.mqtt/README.md index 0dc69e6eebc57..01521d6fa7c6c 100644 --- a/bundles/org.openhab.binding.mqtt/README.md +++ b/bundles/org.openhab.binding.mqtt/README.md @@ -68,3 +68,5 @@ Configuration parameters are: * __stateTopic__: This channel will trigger on this MQTT topic. This topic can contain wildcards like + and # for example "all/in/#" or "sensors/+/config". * __payload__: An optional condition on the value of the MQTT topic that must match before this channel is triggered. + +Note for new users - direct broker Bridge channels are rarely needed. You almost certainly will want to be using one of the binding extensions, or the generic Things and Channels features for most devices or services. diff --git a/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/handler/BrokerHandler.java b/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/handler/BrokerHandler.java index 91d803dd43a71..f26cd46b8407f 100644 --- a/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/handler/BrokerHandler.java +++ b/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/handler/BrokerHandler.java @@ -19,7 +19,6 @@ import javax.net.ssl.TrustManager; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.internal.ssl.Pin; @@ -60,10 +59,12 @@ public void connectionStateChanged(MqttConnectionState state, @Nullable Throwabl super.connectionStateChanged(state, error); // Store generated client ID if none was set by the user final MqttBrokerConnection connection = this.connection; - if (connection != null && state == MqttConnectionState.CONNECTED && StringUtils.isBlank(config.clientID)) { - config.clientID = connection.getClientId(); + String clientID = config.clientID; + if (connection != null && state == MqttConnectionState.CONNECTED && (clientID == null || clientID.isBlank())) { + clientID = connection.getClientId(); + config.clientID = clientID; Configuration editConfig = editConfiguration(); - editConfig.put("clientid", config.clientID); + editConfig.put("clientid", clientID); updateConfiguration(editConfig); } } @@ -147,7 +148,7 @@ protected void assignSSLContextProvider(BrokerHandlerConfig config, MqttBrokerCo if (config.certificatepin) { try { Pin pin; - if (StringUtils.isBlank(config.certificate)) { + if (config.certificate.isBlank()) { pin = Pin.LearningPin(PinType.CERTIFICATE_TYPE); } else { String[] split = config.certificate.split(":"); @@ -165,7 +166,7 @@ protected void assignSSLContextProvider(BrokerHandlerConfig config, MqttBrokerCo if (config.publickeypin) { try { Pin pin; - if (StringUtils.isBlank(config.publickey)) { + if (config.publickey.isBlank()) { pin = Pin.LearningPin(PinType.PUBLIC_KEY_TYPE); } else { String[] split = config.publickey.split(":"); @@ -190,7 +191,7 @@ protected void assignSSLContextProvider(BrokerHandlerConfig config, MqttBrokerCo */ protected MqttBrokerConnection createBrokerConnection() throws IllegalArgumentException { String host = config.host; - if (StringUtils.isBlank(host) || host == null) { + if (host == null || host.isBlank()) { throw new IllegalArgumentException("Host is empty!"); } @@ -199,7 +200,7 @@ protected MqttBrokerConnection createBrokerConnection() throws IllegalArgumentEx final String username = config.username; final String password = config.password; - if (StringUtils.isNotBlank(username) && password != null) { + if (username != null && !username.isBlank() && password != null) { connection.setCredentials(username, password); // Empty passwords are allowed } diff --git a/bundles/org.openhab.binding.myq/NOTICE b/bundles/org.openhab.binding.myq/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.myq/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.myq/README.md b/bundles/org.openhab.binding.myq/README.md new file mode 100644 index 0000000000000..a8094c3546967 --- /dev/null +++ b/bundles/org.openhab.binding.myq/README.md @@ -0,0 +1,68 @@ +# MyQ Binding + +This binding integrates with the [The Chamberlain Group MyQ](https://www.myq.com) cloud service. It allows monitoring and control over [MyQ](https://www.myq.com) enabled garage doors manufactured by LiftMaster, Chamberlain and Craftsman. + +## Supported Things + +### Account + +This represents the MyQ cloud account and uses the same credentials needed when using the MyQ mobile application. + +ThingTypeUID: `account` + +### Garage Door + +This represents a garage door associated with an account. Multiple garage doors are supported. + +ThingTypeUID: `garagedoor` + +### Lamp + +This represents a lamp associated with an account. Multiple lamps are supported. + +ThingTypeUID: `lamp` + +## Discovery + +Once an account has been added, garage doors and lamps will automatically be discovered and added to the inbox. + +## Channels + +| Channel | Item Type | Thing Type | States | +|---------------|---------------|------------------|--------------------------------------------------------| +| status | String | garagedoor | opening, closed, closing, stopped, transition, unknown | +| rollershutter | Rollershutter | garagedoor | UP, DOWN, 0%, 100% | +| switch | Switch | garagedoor, lamp | ON (open), OFF (closed) + +## Full Example + +### Thing Configuration + +```xtend +Bridge myq:account:home "MyQ Account" [ username="foo@bar.com", password="secret", refreshInterval=60 ] { + Thing garagedoor abcd12345 "MyQ Garage Door" [ serialNumber="abcd12345" ] + Thing lamp efgh6789 "MyQ Lamp" [ serialNumber="efgh6789" ] +} +``` + +### Items + +```xtend +String MyQGarageDoor1Status "Door Status [%s]" {channel = "myq:garagedoor:home:abcd12345:status"} +Switch MyQGarageDoor1Switch "Door Switch [%s]" {channel = "myq:garagedoor:home:abcd12345:switch"} +Rollershutter MyQGarageDoor1Rollershutter "Door Rollershutter [%s]" {channel = "myq:garagedoor:home:abcd12345:rollershutter"} +Switch MyQGarageDoorLamp "Lamp [%s]" {channel = "myq:lamp:home:efgh6789:switch"} +} +``` + +### Sitemaps + +```xtend +sitemap MyQ label="MyQ Demo Sitemap" { + Frame label="Garage Door" { + String item=MyQGarageDoor1Status + Switch item=MyQGarageDoor1Switch + Rollershutter item=MyQGarageDoor1Rollershutter + } +} +``` diff --git a/bundles/org.openhab.binding.myq/pom.xml b/bundles/org.openhab.binding.myq/pom.xml new file mode 100644 index 0000000000000..a0e8b2e06cdc3 --- /dev/null +++ b/bundles/org.openhab.binding.myq/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.myq + + openHAB Add-ons :: Bundles :: MyQ Binding + + diff --git a/bundles/org.openhab.binding.myq/src/main/feature/feature.xml b/bundles/org.openhab.binding.myq/src/main/feature/feature.xml new file mode 100644 index 0000000000000..60382373bb63b --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.myq/${project.version} + + diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java new file mode 100644 index 0000000000000..3e8cb305720ab --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link MyQBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQBindingConstants { + + public static final String BINDING_ID = "myq"; + + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); + public static final ThingTypeUID THING_TYPE_GARAGEDOOR = new ThingTypeUID(BINDING_ID, "garagedoor"); + public static final ThingTypeUID THING_TYPE_LAMP = new ThingTypeUID(BINDING_ID, "lamp"); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_GARAGEDOOR, + THING_TYPE_LAMP); + public static final Set SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GARAGEDOOR, + THING_TYPE_LAMP); +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java new file mode 100644 index 0000000000000..883d2988cf736 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal; + +import static org.openhab.binding.myq.internal.MyQBindingConstants.BINDING_ID; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.myq.internal.dto.DevicesDTO; +import org.openhab.binding.myq.internal.handler.MyQAccountHandler; +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.config.discovery.DiscoveryService; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; + +/** + * The {@link MyQDiscoveryService} is responsible for discovering MyQ things + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { + + private static final Set SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set + .of(MyQBindingConstants.THING_TYPE_GARAGEDOOR, MyQBindingConstants.THING_TYPE_LAMP); + private @Nullable MyQAccountHandler accountHandler; + + public MyQDiscoveryService() { + super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 1, true); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_DISCOVERY_THING_TYPES_UIDS; + } + + @Override + public void startScan() { + MyQAccountHandler accountHandler = this.accountHandler; + if (accountHandler != null) { + DevicesDTO devices = accountHandler.devicesCache(); + if (devices != null) { + devices.items.forEach(device -> { + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily); + if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) { + ThingUID thingUID = new ThingUID(thingTypeUID, accountHandler.getThing().getUID(), + device.serialNumber.toLowerCase()); + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("MyQ " + device.name) + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, thingUID.getId()) + .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER) + .withBridge(accountHandler.getThing().getUID()).build(); + thingDiscovered(result); + } + }); + } + } + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof MyQAccountHandler) { + accountHandler = (MyQAccountHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return accountHandler; + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java new file mode 100644 index 0000000000000..2d01eee7788ae --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal; + +import static org.openhab.binding.myq.internal.MyQBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.myq.internal.handler.MyQAccountHandler; +import org.openhab.binding.myq.internal.handler.MyQGarageDoorHandler; +import org.openhab.binding.myq.internal.handler.MyQLampHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MyQHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.myq", service = ThingHandlerFactory.class) +public class MyQHandlerFactory extends BaseThingHandlerFactory { + private final HttpClient httpClient; + + @Activate + public MyQHandlerFactory(final @Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) { + return new MyQAccountHandler((Bridge) thing, httpClient); + } + + if (THING_TYPE_GARAGEDOOR.equals(thingTypeUID)) { + return new MyQGarageDoorHandler(thing); + } + + if (THING_TYPE_LAMP.equals(thingTypeUID)) { + return new MyQLampHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java new file mode 100644 index 0000000000000..b6f3d23c2524d --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MyQAccountConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQAccountConfiguration { + public String username = ""; + public String password = ""; + public Integer refreshInterval = 60; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java new file mode 100644 index 0000000000000..8cab68231271e --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MyQDeviceConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQDeviceConfiguration { + public String serialNumber = ""; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java new file mode 100644 index 0000000000000..2957918320957 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link AccountDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class AccountDTO { + + public UsersDTO users; + public Boolean admin; + public AccountInfoDTO account; + public String analyticsId; + public String userId; + public String userName; + public String email; + public String firstName; + public String lastName; + public String cultureCode; + public AddressDTO address; + public TimeZoneDTO timeZone; + public Boolean mailingListOptIn; + public Boolean requestAccountLinkInfo; + public String phone; + public Boolean diagnosticDataOptIn; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java new file mode 100644 index 0000000000000..193f464d044bb --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link AccountInfoDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class AccountInfoDTO { + + public String href; + public String id; +} diff --git a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameHcOption.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java similarity index 51% rename from bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameHcOption.java rename to bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java index 70a8b9489466b..a38aa1e075d1b 100644 --- a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameHcOption.java +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java @@ -10,24 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.teleinfo.internal.dto.common; +package org.openhab.binding.myq.internal.dto; /** - * The {@link FrameHcOption} interface defines common attributes for HC option. + * The {@link ActionDTO} entity from the MyQ API * - * @author Nicolas SIBERIL - Initial contribution + * @author Dan Cunningham - Initial contribution */ -public interface FrameHcOption { +public class ActionDTO { - int getHchc(); + public ActionDTO(String actionType) { + super(); + this.actionType = actionType; + } - void setHchc(int hchc); - - int getHchp(); - - void setHchp(int hchp); - - Hhphc getHhphc(); - - void setHhphc(Hhphc hhphc); + public String actionType; } diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java new file mode 100644 index 0000000000000..1c4d858ff8b08 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link AddressDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class AddressDTO { + + public String addressLine1; + public String city; + public String postalCode; + public CountryDTO country; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java new file mode 100644 index 0000000000000..594a1ee32e7c0 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link CountryDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class CountryDTO { + + public String code; + public Boolean isEEACountry; + public String href; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java new file mode 100644 index 0000000000000..426d691b2fcfb --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link DeviceDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class DeviceDTO { + public String href; + public String serialNumber; + public String deviceFamily; + public String devicePlatform; + public String deviceType; + public String name; + public String createdDate; + public DeviceStateDTO state; + public String parentDevice; + public String parentDeviceId; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java new file mode 100644 index 0000000000000..169119b8c6116 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +import java.util.List; + +/** + * The {@link DeviceStateDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class DeviceStateDTO { + + public Boolean gdoLockConnected; + public Boolean attachedWorkLightErrorPresent; + public String doorState; + public String lampState; + public String open; + public String close; + public String lastUpdate; + public String passthroughInterval; + public String doorAjarInterval; + public String invalidCredentialWindow; + public String invalidShutoutPeriod; + public Boolean isUnattendedOpenAllowed; + public Boolean isUnattendedCloseAllowed; + public String auxRelayDelay; + public Boolean useAuxRelay; + public String auxRelayBehavior; + public Boolean rexFiresDoor; + public Boolean commandChannelReportStatus; + public Boolean controlFromBrowser; + public Boolean reportForced; + public Boolean reportAjar; + public Integer maxInvalidAttempts; + public Boolean online; + public String lastStatus; + public String firmwareVersion; + public Boolean homekitCapable; + public Boolean homekitEnabled; + public String learn; + public Boolean learnMode; + public String updatedDate; + public List physicalDevices = null; + public Boolean pendingBootloadAbandoned; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java new file mode 100644 index 0000000000000..f170101c81dda --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +import java.util.List; + +/** + * The {@link DevicesDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class DevicesDTO { + public String href; + public Integer count; + public List items; +} diff --git a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameEjpOption.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java similarity index 50% rename from bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameEjpOption.java rename to bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java index 3b198fe21f3cb..8b2eaf54014c6 100644 --- a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/dto/common/FrameEjpOption.java +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java @@ -10,24 +10,21 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.teleinfo.internal.dto.common; +package org.openhab.binding.myq.internal.dto; /** - * The {@link FrameEjpOption} interface defines common attributes for EJP option. + * The {@link LoginRequestDTO} entity from the MyQ API * - * @author Nicolas SIBERIL - Initial contribution + * @author Dan Cunningham - Initial contribution */ -public interface FrameEjpOption { +public class LoginRequestDTO { - int getEjphpm(); + public LoginRequestDTO(String username, String password) { + super(); + this.username = username; + this.password = password; + } - void setEjphpm(int ejphpm); - - int getEjphn(); - - void setEjphn(int ejphn); - - Integer getPejp(); - - void setPejp(Integer pejp); + public String username; + public String password; } diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java new file mode 100644 index 0000000000000..2dfcd637d21bb --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link LoginResponseDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class LoginResponseDTO { + public String securityToken; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java new file mode 100644 index 0000000000000..e324225ecdd11 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link TimeZoneDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class TimeZoneDTO { + + public String id; + public String name; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java new file mode 100644 index 0000000000000..c988dba883888 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.dto; + +/** + * The {@link UsersDTO} entity from the MyQ API + * + * @author Dan Cunningham - Initial contribution + */ +public class UsersDTO { + + public String href; +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java new file mode 100644 index 0000000000000..f9dc5f11bcb0f --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java @@ -0,0 +1,344 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.handler; + +import static org.openhab.binding.myq.internal.MyQBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.myq.internal.MyQDiscoveryService; +import org.openhab.binding.myq.internal.config.MyQAccountConfiguration; +import org.openhab.binding.myq.internal.dto.AccountDTO; +import org.openhab.binding.myq.internal.dto.ActionDTO; +import org.openhab.binding.myq.internal.dto.DevicesDTO; +import org.openhab.binding.myq.internal.dto.LoginRequestDTO; +import org.openhab.binding.myq.internal.dto.LoginResponseDTO; +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.ThingTypeUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link MyQAccountHandler} is responsible for communicating with the MyQ API based on an account. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQAccountHandler extends BaseBridgeHandler { + private static final String BASE_URL = "https://api.myqdevice.com/api"; + private static final Integer RAPID_REFRESH_SECONDS = 5; + private final Logger logger = LoggerFactory.getLogger(MyQAccountHandler.class); + private final Gson gsonUpperCase = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) + .create(); + private final Gson gsonLowerCase = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + private @Nullable Future normalPollFuture; + private @Nullable Future rapidPollFuture; + private @Nullable String securityToken; + private @Nullable AccountDTO account; + private @Nullable DevicesDTO devicesCache; + private Integer normalRefreshSeconds = 60; + private HttpClient httpClient; + private String username = ""; + private String password = ""; + private String userAgent = ""; + + public MyQAccountHandler(Bridge bridge, HttpClient httpClient) { + super(bridge); + this.httpClient = httpClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + MyQAccountConfiguration config = getConfigAs(MyQAccountConfiguration.class); + normalRefreshSeconds = config.refreshInterval; + username = config.username; + password = config.password; + // MyQ can get picky about blocking user agents apparently + userAgent = MyQAccountHandler.randomString(40); + securityToken = null; + updateStatus(ThingStatus.UNKNOWN); + restartPolls(false); + } + + @Override + public void dispose() { + stopPolls(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(MyQDiscoveryService.class); + } + + @Override + public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { + DevicesDTO localDeviceCaches = devicesCache; + if (localDeviceCaches != null && childHandler instanceof MyQDeviceHandler) { + MyQDeviceHandler handler = (MyQDeviceHandler) childHandler; + localDeviceCaches.items.stream() + .filter(d -> ((MyQDeviceHandler) childHandler).getSerialNumber().equalsIgnoreCase(d.serialNumber)) + .findFirst().ifPresent(handler::handleDeviceUpdate); + } + } + + /** + * Sends an action to the MyQ API + * + * @param serialNumber + * @param action + */ + public void sendAction(String serialNumber, String action) { + AccountDTO localAccount = account; + if (localAccount != null) { + try { + HttpResult result = sendRequest( + String.format("%s/v5.1/Accounts/%s/Devices/%s/actions", BASE_URL, localAccount.account.id, + serialNumber), + HttpMethod.PUT, securityToken, + new StringContentProvider(gsonLowerCase.toJson(new ActionDTO(action))), "application/json"); + if (HttpStatus.isSuccess(result.responseCode)) { + restartPolls(true); + } else { + logger.debug("Failed to send action {} : {}", action, result.content); + } + } catch (InterruptedException e) { + } + } + } + + /** + * Last known state of MyQ Devices + * + * @return cached MyQ devices + */ + public @Nullable DevicesDTO devicesCache() { + return devicesCache; + } + + private void stopPolls() { + stopNormalPoll(); + stopRapidPoll(); + } + + private synchronized void stopNormalPoll() { + stopFuture(normalPollFuture); + normalPollFuture = null; + } + + private synchronized void stopRapidPoll() { + stopFuture(rapidPollFuture); + rapidPollFuture = null; + } + + private void stopFuture(@Nullable Future future) { + if (future != null) { + future.cancel(true); + } + } + + private synchronized void restartPolls(boolean rapid) { + stopPolls(); + if (rapid) { + normalPollFuture = scheduler.scheduleWithFixedDelay(this::normalPoll, 35, normalRefreshSeconds, + TimeUnit.SECONDS); + rapidPollFuture = scheduler.scheduleWithFixedDelay(this::rapidPoll, 3, RAPID_REFRESH_SECONDS, + TimeUnit.SECONDS); + } else { + normalPollFuture = scheduler.scheduleWithFixedDelay(this::normalPoll, 0, normalRefreshSeconds, + TimeUnit.SECONDS); + } + } + + private void normalPoll() { + stopRapidPoll(); + fetchData(); + } + + private void rapidPoll() { + fetchData(); + } + + private synchronized void fetchData() { + try { + if (securityToken == null) { + login(); + if (securityToken != null) { + getAccount(); + } + } + if (securityToken != null) { + getDevices(); + } + } catch (InterruptedException e) { + } + } + + private void login() throws InterruptedException { + HttpResult result = sendRequest(BASE_URL + "/v5/Login", HttpMethod.POST, null, + new StringContentProvider(gsonUpperCase.toJson(new LoginRequestDTO(username, password))), + "application/json"); + LoginResponseDTO loginResponse = parseResultAndUpdateStatus(result, gsonUpperCase, LoginResponseDTO.class); + if (loginResponse != null) { + securityToken = loginResponse.securityToken; + } else { + securityToken = null; + if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) { + // bad credentials, stop trying to login + stopPolls(); + } + } + } + + private void getAccount() throws InterruptedException { + HttpResult result = sendRequest(BASE_URL + "/v5/My?expand=account", HttpMethod.GET, securityToken, null, null); + account = parseResultAndUpdateStatus(result, gsonUpperCase, AccountDTO.class); + } + + private void getDevices() throws InterruptedException { + AccountDTO localAccount = account; + if (localAccount == null) { + return; + } + HttpResult result = sendRequest(String.format("%s/v5.1/Accounts/%s/Devices", BASE_URL, localAccount.account.id), + HttpMethod.GET, securityToken, null, null); + DevicesDTO devices = parseResultAndUpdateStatus(result, gsonLowerCase, DevicesDTO.class); + if (devices != null) { + devicesCache = devices; + devices.items.forEach(device -> { + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily); + if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) { + for (Thing thing : getThing().getThings()) { + ThingHandler handler = thing.getHandler(); + if (handler != null && ((MyQDeviceHandler) handler).getSerialNumber() + .equalsIgnoreCase(device.serialNumber)) { + ((MyQDeviceHandler) handler).handleDeviceUpdate(device); + } + } + } + }); + } + } + + private synchronized HttpResult sendRequest(String url, HttpMethod method, @Nullable String token, + @Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException { + try { + Request request = httpClient.newRequest(url).method(method) + .header("MyQApplicationId", "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu") + .header("ApiVersion", "5.1").header("BrandId", "2").header("Culture", "en").agent(userAgent) + .timeout(10, TimeUnit.SECONDS); + if (token != null) { + request = request.header("SecurityToken", token); + } + if (content != null & contentType != null) { + request = request.content(content, contentType); + } + // use asyc jetty as the API service will response with a 401 error when credentials are wrong, + // but not a WWW-Authenticate header which causes Jetty to throw a generic execution exception which + // prevents us from knowing the response code + logger.trace("Sending {} to {}", request.getMethod(), request.getURI()); + final CompletableFuture futureResult = new CompletableFuture<>(); + request.send(new BufferingResponseListener() { + @NonNullByDefault({}) + @Override + public void onComplete(Result result) { + futureResult.complete(new HttpResult(result.getResponse().getStatus(), getContentAsString())); + } + }); + HttpResult result = futureResult.get(); + logger.trace("Account Response - status: {} content: {}", result.responseCode, result.content); + return result; + } catch (ExecutionException e) { + return new HttpResult(0, e.getMessage()); + } + } + + @Nullable + private T parseResultAndUpdateStatus(HttpResult result, Gson parser, Class classOfT) { + if (HttpStatus.isSuccess(result.responseCode)) { + try { + T responseObject = parser.fromJson(result.content, classOfT); + if (responseObject != null) { + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + return responseObject; + } + } catch (JsonSyntaxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Invalid JSON Response " + result.content); + } + } else if (result.responseCode == HttpStatus.UNAUTHORIZED_401) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Unauthorized - Check Credentials"); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Invalid Response Code " + result.responseCode + " : " + result.content); + } + return null; + } + + private class HttpResult { + public final int responseCode; + public @Nullable String content; + + public HttpResult(int responseCode, @Nullable String content) { + this.responseCode = responseCode; + this.content = content; + } + } + + private static String randomString(int length) { + int low = 97; // a-z + int high = 122; // A-Z + StringBuilder sb = new StringBuilder(length); + Random random = new Random(); + for (int i = 0; i < length; i++) { + sb.append((char) (low + (int) (random.nextFloat() * (high - low + 1)))); + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java new file mode 100644 index 0000000000000..030d780bea5af --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.myq.internal.dto.DeviceDTO; + +/** + * The {@link MyQDeviceHandler} is responsible for handling device updates + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public interface MyQDeviceHandler { + public void handleDeviceUpdate(DeviceDTO device); + + public String getSerialNumber(); +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java new file mode 100644 index 0000000000000..c83c4acdabdad --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.myq.internal.MyQBindingConstants; +import org.openhab.binding.myq.internal.config.MyQDeviceConfiguration; +import org.openhab.binding.myq.internal.dto.DeviceDTO; +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.library.types.UpDownType; +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.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; + +/** + * The {@link MyQGarageDoorHandler} is responsible for handling commands for a garage door thing, which are + * sent to one of the channels. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceHandler { + private @Nullable DeviceDTO deviceState; + private String serialNumber; + + public MyQGarageDoorHandler(Thing thing) { + super(thing); + serialNumber = getConfigAs(MyQDeviceConfiguration.class).serialNumber; + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateState(); + return; + } + Bridge bridge = getBridge(); + final DeviceDTO localState = deviceState; + if (bridge != null && localState != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + String cmd = null; + if (command instanceof OnOffType) { + cmd = command == OnOffType.ON ? "open" : "close"; + } + if (command instanceof UpDownType) { + cmd = command == UpDownType.UP ? "open" : "close"; + } + if (command instanceof PercentType) { + cmd = ((PercentType) command).as(UpDownType.class) == UpDownType.UP ? "open" : "close"; + } + if (command instanceof StringType) { + cmd = command.toString(); + } + if (cmd != null) { + ((MyQAccountHandler) handler).sendAction(localState.serialNumber, cmd); + } + } + } + } + + @Override + public String getSerialNumber() { + return serialNumber; + } + + protected void updateState() { + final DeviceDTO localState = deviceState; + if (localState != null) { + String doorState = localState.state.doorState; + updateState("status", new StringType(doorState)); + switch (doorState) { + case "open": + case "opening": + case "closing": + case "stopped": + case "transition": + updateState("switch", OnOffType.ON); + updateState("rollershutter", UpDownType.UP); + break; + case "closed": + updateState("switch", OnOffType.OFF); + updateState("rollershutter", UpDownType.DOWN); + break; + default: + updateState("switch", UnDefType.UNDEF); + updateState("rollershutter", UnDefType.UNDEF); + break; + } + } + } + + @Override + public void handleDeviceUpdate(DeviceDTO device) { + if (!MyQBindingConstants.THING_TYPE_GARAGEDOOR.getId().equals(device.deviceFamily)) { + return; + } + deviceState = device; + if (device.state.online) { + updateStatus(ThingStatus.ONLINE); + updateState(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline"); + } + } +} diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java new file mode 100644 index 0000000000000..e7988bae00ee4 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2021 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.myq.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.myq.internal.MyQBindingConstants; +import org.openhab.binding.myq.internal.config.MyQDeviceConfiguration; +import org.openhab.binding.myq.internal.dto.DeviceDTO; +import org.openhab.core.library.types.OnOffType; +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.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; + +/** + * The {@link MyQLampHandler} is responsible for handling commands for a lamp thing, which are + * sent to one of the channels. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler { + private @Nullable DeviceDTO deviceState; + private String serialNumber; + + public MyQLampHandler(Thing thing) { + super(thing); + serialNumber = getConfigAs(MyQDeviceConfiguration.class).serialNumber; + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateState(); + return; + } + + if (command instanceof OnOffType) { + Bridge bridge = getBridge(); + final DeviceDTO localState = deviceState; + if (bridge != null && localState != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + ((MyQAccountHandler) handler).sendAction(localState.serialNumber, + command == OnOffType.ON ? "turnon" : "turnoff"); + } + } + } + } + + @Override + public String getSerialNumber() { + return serialNumber; + } + + protected void updateState() { + final DeviceDTO localState = deviceState; + if (localState != null) { + String lampState = localState.state.lampState; + updateState("switch", "on".equals(lampState) ? OnOffType.ON : OnOffType.OFF); + } + } + + @Override + public void handleDeviceUpdate(DeviceDTO device) { + if (!MyQBindingConstants.THING_TYPE_LAMP.getId().equals(device.deviceFamily)) { + return; + } + deviceState = device; + if (device.state.online) { + updateStatus(ThingStatus.ONLINE); + updateState(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline"); + } + } +} diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..b024ec7451b50 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + MyQ Binding + The MyQ binding allows monitoring and control of garage doors that are MyQ enabled. + + diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..beaf772f2412f --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,37 @@ + + + + + + Account username + + + + Account password + password + + + + Specifies the refresh interval in seconds + 60 + + + + + + + Serial number of the garage door + + + + + + + Serial number of the lamp + + + + diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..dbe68946840e5 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,67 @@ + + + + + + MyQ Cloud Account + + + + + + + + + MyQ Garage Door + + + + + + serialNumber + + + + + + + + + MyQ Lamp + + + + serialNumber + + + + + String + + + + + + + + + + + + + + + Switch + + + + Rollershutter + + + + Switch + + + diff --git a/bundles/org.openhab.binding.mystrom/README.md b/bundles/org.openhab.binding.mystrom/README.md index a4de343bb95e6..a0389947e8f20 100644 --- a/bundles/org.openhab.binding.mystrom/README.md +++ b/bundles/org.openhab.binding.mystrom/README.md @@ -9,6 +9,9 @@ This bundle adds the following thing types: | Thing | ThingTypeID | Description | | ------------------ | ----------- | -------------------------------------------------- | | myStrom Smart Plug | mystromplug | A myStrom smart plug | +| myStrom Bulb | mystrombulb | A myStrom bulb | + +According to the myStrom API documentation all request specific to the myStrom Bulb are also work on the LED strip. ## Discovery @@ -24,13 +27,37 @@ The following parameters are valid for all thing types: | hostname | string | yes | localhost | The IP address or hostname of the myStrom smart plug | | refresh | integer | no | 10 | Poll interval in seconds. Increase this if you encounter connection errors | +## Properties + +In addition to the configuration a myStrom thing has the following properties. +The properties are updated during initialize. +Disabling/enabling the thing can be used to update the properties. + +| Property-Name | Description | +| ------------- | --------------------------------------------------------------------- | +| version | Current firmware version | +| type | The type of the device (i.e. bulb = 102) | +| ssid | SSID of the currently connected network | +| ip | Current ip address | +| mask | Mask of the current network | +| gateway | Gateway of the current network | +| dns | DNS of the current network | +| static | Whether or not the ip address is static | +| connected | Whether or not the device is connected to the internet | +| mac | The mac address of the bridge in upper case letters without delimiter | + ## Channels -| Channel ID | Item Type | Read only | Description | -| ---------------- | -------------------- | --------- | ------------------------------------------------------------- | -| switch | Switch | false | Turn the smart plug on or off | -| power | Number:Power | true | The currently delivered power | -| temperature | Number:Temperature | true | The temperature at the plug | +| Channel ID | Item Type | Read only | Description | Thing types supporting this channel | +| ---------------- | -------------------- | --------- | --------------------------------------------------------------------- |-------------------------------------| +| switch | Switch | false | Turn the device on or off | mystromplug, mystrombulb | +| power | Number:Power | true | The currently delivered power | mystromplug, mystrombulb | +| temperature | Number:Temperature | true | The temperature at the plug | mystromplug | +| color | Color | false | The color we set the bulb to (mode 'hsv') | mystrombulb | +| colorTemperature | Dimmer | false | The color temperature of the bulb in mode 'mono' (percentage) | mystrombulb | +| brightness | Dimmer | false | The brightness of the bulb in mode 'mono' | mystrombulb | +| ramp | Number:Time | false | Transition time from the light’s current state to the new state. [ms] | mystrombulb | +| mode | String | false | The color mode we want the Bulb to set to (rgb, hsv or mono) | mystrombulb | ## Full Example diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java new file mode 100644 index 0000000000000..1be559b58b42b --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2021 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.mystrom.internal; + +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_CONNECTED; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_DNS; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_GW; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_IP; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_LAST_REFRESH; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_MAC; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_MASK; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_SSID; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_STATIC; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_TYPE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_VERSION; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; + +import com.google.gson.Gson; + +/** + * The {@link AbstractMyStromHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractMyStromHandler extends BaseThingHandler { + protected static final String COMMUNICATION_ERROR = "Error while communicating to the myStrom plug: "; + protected static final String HTTP_REQUEST_URL_PREFIX = "http://"; + + protected final HttpClient httpClient; + protected String hostname = ""; + protected String mac = ""; + + private @Nullable ScheduledFuture pollingJob; + protected final Gson gson = new Gson(); + + public AbstractMyStromHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.httpClient = httpClient; + } + + @Override + public final void initialize() { + MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); + this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname; + + updateStatus(ThingStatus.UNKNOWN); + scheduler.schedule(this::initializeInternal, 0, TimeUnit.SECONDS); + } + + @Override + public final void dispose() { + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + super.dispose(); + } + + private void updateProperties() throws MyStromException { + String json = sendHttpRequest(HttpMethod.GET, "/api/v1/info", null); + MyStromDeviceInfo deviceInfo = gson.fromJson(json, MyStromDeviceInfo.class); + if (deviceInfo == null) { + throw new MyStromException("Cannot retrieve device info from myStrom device " + getThing().getUID()); + } + this.mac = deviceInfo.mac; + Map properties = editProperties(); + properties.put(PROPERTY_MAC, deviceInfo.mac); + properties.put(PROPERTY_VERSION, deviceInfo.version); + properties.put(PROPERTY_TYPE, Long.toString(deviceInfo.type)); + properties.put(PROPERTY_SSID, deviceInfo.ssid); + properties.put(PROPERTY_IP, deviceInfo.ip); + properties.put(PROPERTY_MASK, deviceInfo.mask); + properties.put(PROPERTY_GW, deviceInfo.gw); + properties.put(PROPERTY_DNS, deviceInfo.dns); + properties.put(PROPERTY_STATIC, Boolean.toString(deviceInfo.staticState)); + properties.put(PROPERTY_CONNECTED, Boolean.toString(deviceInfo.connected)); + Calendar calendar = Calendar.getInstance(); + DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, Locale.getDefault()); + properties.put(PROPERTY_LAST_REFRESH, formatter.format(calendar.getTime())); + updateProperties(properties); + } + + /** + * Calls the API with the given http method, request path and actual data. + * + * @param method the http method to make the call with + * @param path The path of the API endpoint + * @param requestData the actual raw data to send in the request body, may be {@code null} + * @return String contents of the response for the GET request. + * @throws MyStromException Throws on communication error + */ + protected final String sendHttpRequest(HttpMethod method, String path, @Nullable String requestData) + throws MyStromException { + String url = hostname + path; + try { + Request request = httpClient.newRequest(url).timeout(10, TimeUnit.SECONDS).method(method); + if (requestData != null) { + request = request.content(new StringContentProvider(requestData)).header(HttpHeader.CONTENT_TYPE, + "application/x-www-form-urlencoded"); + } + ContentResponse response = request.send(); + if (response.getStatus() != HttpStatus.OK_200) { + throw new MyStromException("Error sending HTTP " + method + " request to " + url + + ". Got response code: " + response.getStatus()); + } + return response.getContentAsString(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new MyStromException(COMMUNICATION_ERROR + e.getMessage()); + } + } + + private void initializeInternal() { + try { + updateProperties(); + updateStatus(ThingStatus.ONLINE); + MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); + pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevice, 0, config.refresh, TimeUnit.SECONDS); + } catch (MyStromException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + protected abstract void pollDevice(); +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java index 99969fc837b9a..8e73faf290216 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java @@ -20,6 +20,7 @@ * used across the whole binding. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Add constants for myStrom bulb support */ @NonNullByDefault public class MyStromBindingConstants { @@ -30,9 +31,36 @@ public class MyStromBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_PLUG = new ThingTypeUID(BINDING_ID, "mystromplug"); + public static final ThingTypeUID THING_TYPE_BULB = new ThingTypeUID(BINDING_ID, "mystrombulb"); // List of all Channel ids public static final String CHANNEL_SWITCH = "switch"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_COLOR = "color"; + public static final String CHANNEL_RAMP = "ramp"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature"; + public static final String CHANNEL_BRIGHTNESS = "brightness"; + + // Config + public static final String CONFIG_MAC = "mac"; + + // List of all Properties + public static final String PROPERTY_MAC = "mac"; + public static final String PROPERTY_VERSION = "version"; + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_SSID = "ssid"; + public static final String PROPERTY_IP = "ip"; + public static final String PROPERTY_MASK = "mask"; + public static final String PROPERTY_GW = "gw"; + public static final String PROPERTY_DNS = "dns"; + public static final String PROPERTY_STATIC = "static"; + public static final String PROPERTY_CONNECTED = "connected"; + public static final String PROPERTY_LAST_REFRESH = "lastRefresh"; + + // myStrom Bulb modes + public static final String RGB = "rgb"; + public static final String HSV = "hsv"; + public static final String MONO = "mono"; } diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java new file mode 100644 index 0000000000000..259136291438e --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2010-2021 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.mystrom.internal; + +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_BRIGHTNESS; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR_TEMPERATURE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_MODE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_POWER; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_RAMP; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_SWITCH; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.HSV; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.MONO; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.RGB; +import static org.openhab.core.library.unit.Units.SECOND; +import static org.openhab.core.library.unit.Units.WATT; + +import java.lang.reflect.Type; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.Fields; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link MyStromBulbHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public class MyStromBulbHandler extends AbstractMyStromHandler { + + private static final Type DEVICE_INFO_MAP_TYPE = new TypeToken>() { + }.getType(); + + private final Logger logger = LoggerFactory.getLogger(MyStromBulbHandler.class); + + private final ExpiringCache> cache = new ExpiringCache<>( + Duration.ofSeconds(3), this::getReport); + + private PercentType lastBrightness = PercentType.HUNDRED; + private PercentType lastColorTemperature = new PercentType(50); + private String lastMode = MONO; + private HSBType lastColor = HSBType.WHITE; + + public MyStromBulbHandler(Thing thing, HttpClient httpClient) { + super(thing, httpClient); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + if (command instanceof RefreshType) { + pollDevice(); + } else { + String sResp = null; + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + if (command instanceof OnOffType) { + sResp = sendToBulb(command == OnOffType.ON ? "on" : "off", null, null, null); + } + break; + case CHANNEL_COLOR: + if (command instanceof HSBType) { + if (Objects.equals(((HSBType) command).as(OnOffType.class), OnOffType.OFF)) { + sResp = sendToBulb("off", null, null, null); + } else { + String hsv = command.toString().replaceAll(",", ";"); + sResp = sendToBulb("on", hsv, null, HSV); + } + } + break; + case CHANNEL_BRIGHTNESS: + if (command instanceof PercentType) { + if (Objects.equals(((PercentType) command).as(OnOffType.class), OnOffType.OFF)) { + sResp = sendToBulb("off", null, null, null); + } else { + if (lastMode.equals(MONO)) { + String mono = convertPercentageToMyStromCT(lastColorTemperature) + ";" + + command.toString(); + sResp = sendToBulb("on", mono, null, MONO); + } else { + String hsv = lastColor.getHue().intValue() + ";" + lastColor.getSaturation() + ";" + + command.toString(); + sResp = sendToBulb("on", hsv, null, HSV); + } + } + } + break; + case CHANNEL_COLOR_TEMPERATURE: + if (command instanceof PercentType) { + String mono = convertPercentageToMyStromCT((PercentType) command) + ";" + + lastBrightness.toString(); + sResp = sendToBulb("on", mono, null, MONO); + } + break; + case CHANNEL_RAMP: + if (command instanceof DecimalType) { + sResp = sendToBulb(null, null, command.toString(), null); + } + break; + case CHANNEL_MODE: + if (command instanceof StringType) { + sResp = sendToBulb(null, null, null, command.toString()); + } + break; + default: + } + + if (sResp != null) { + Map report = gson.fromJson(sResp, DEVICE_INFO_MAP_TYPE); + if (report != null) { + report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst() + .ifPresent(info -> updateDevice(info.getValue())); + } + } + } + } catch (MyStromException e) { + logger.warn("Error while handling command {}", e.getMessage()); + } + } + + private @Nullable Map getReport() { + try { + String returnContent = sendHttpRequest(HttpMethod.GET, "/api/v1/device", null); + Map report = gson.fromJson(returnContent, DEVICE_INFO_MAP_TYPE); + updateStatus(ThingStatus.ONLINE); + return report; + } catch (MyStromException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return null; + } + } + + @Override + protected void pollDevice() { + Map report = cache.getValue(); + if (report != null) { + report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst() + .ifPresent(info -> updateDevice(info.getValue())); + } + } + + private void updateDevice(@Nullable MyStromBulbResponse deviceInfo) { + if (deviceInfo != null) { + updateState(CHANNEL_SWITCH, deviceInfo.on ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_RAMP, QuantityType.valueOf(deviceInfo.ramp, MetricPrefix.MILLI(SECOND))); + if (deviceInfo instanceof MyStromDeviceSpecificInfo) { + updateState(CHANNEL_POWER, QuantityType.valueOf(((MyStromDeviceSpecificInfo) deviceInfo).power, WATT)); + } + if (deviceInfo.on) { + try { + lastMode = deviceInfo.mode; + long numSemicolon = deviceInfo.color.chars().filter(c -> c == ';').count(); + if (numSemicolon == 1 && deviceInfo.mode.equals(MONO)) { + String[] xy = deviceInfo.color.split(";"); + lastColorTemperature = new PercentType(convertMyStromCTToPercentage(xy[0])); + lastBrightness = PercentType.valueOf(xy[1]); + lastColor = new HSBType(lastColor.getHue() + ",0," + lastBrightness); + updateState(CHANNEL_COLOR_TEMPERATURE, lastColorTemperature); + } else if (numSemicolon == 2 && deviceInfo.mode.equals(HSV)) { + lastColor = HSBType.valueOf(deviceInfo.color.replaceAll(";", ",")); + lastBrightness = lastColor.getBrightness(); + } else if (!deviceInfo.color.equals("") && deviceInfo.mode.equals(RGB)) { + int r = Integer.parseInt(deviceInfo.color.substring(2, 4), 16); + int g = Integer.parseInt(deviceInfo.color.substring(4, 6), 16); + int b = Integer.parseInt(deviceInfo.color.substring(6, 8), 16); + lastColor = HSBType.fromRGB(r, g, b); + lastBrightness = lastColor.getBrightness(); + } + updateState(CHANNEL_COLOR, lastColor); + updateState(CHANNEL_BRIGHTNESS, lastBrightness); + updateState(CHANNEL_MODE, StringType.valueOf(lastMode)); + } catch (IllegalArgumentException e) { + logger.warn("Error while updating {}", e.getMessage()); + } + } + } + } + + /** + * Given a URL and a set parameters, send a HTTP POST request to the URL location + * created by the URL and parameters. + * + * @param action The action we want to take (on,off or toggle) + * @param color The color we set the bulb to (When using RGBW mode the first two hex numbers are used for the + * white channel! hsv is of form ;;) + * @param ramp Transition time from the light’s current state to the new state. [ms] + * @param mode The color mode we want the Bulb to set to (rgb or hsv or mono) + * @return String contents of the response for the GET request. + * @throws MyStromException Throws on communication error + */ + private String sendToBulb(@Nullable String action, @Nullable String color, @Nullable String ramp, + @Nullable String mode) throws MyStromException { + Fields fields = new Fields(); + if (action != null) { + fields.put("action", action); + } + if (color != null) { + fields.put("color", color); + } + if (ramp != null) { + fields.put("ramp", ramp); + } + if (mode != null) { + fields.put("mode", mode); + } + StringBuilder builder = new StringBuilder(fields.getSize() * 32); + for (Fields.Field field : fields) { + for (String value : field.getValues()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(field.getName()).append("=").append(value); + } + } + return sendHttpRequest(HttpMethod.POST, "/api/v1/device/" + mac, builder.toString()); + } + + /** + * Convert the color temperature from myStrom (1-18) to openHAB (percentage) + * + * @param ctValue Color temperature in myStrom: "1" = warm to "18" = cold. + * @return Color temperature (0-100%). 0% is the coldest setting. + * @throws NumberFormatException if the argument is not an integer + */ + private int convertMyStromCTToPercentage(String ctValue) throws NumberFormatException { + int ct = Integer.parseInt(ctValue); + return Math.round((18 - limitColorTemperature(ct)) / 17F * 100F); + } + + /** + * Convert the color temperature from openHAB (percentage) to myStrom (1-18) + * + * @param colorTemperature Color temperature from openHab. 0 = coldest, 100 = warmest + * @return Color temperature from myStrom. 1 = warmest, 18 = coldest + */ + private String convertPercentageToMyStromCT(PercentType colorTemperature) { + int ct = 18 - Math.round(colorTemperature.floatValue() * 17F / 100F); + return Integer.toString(limitColorTemperature(ct)); + } + + private int limitColorTemperature(int colorTemperature) { + return Math.max(1, Math.min(colorTemperature, 18)); + } + + private static class MyStromBulbResponse { + public boolean on; + public String color = ""; + public String mode = ""; + public long ramp; + + @Override + public String toString() { + return "MyStromBulbResponse{" + "on=" + on + ", color='" + color + '\'' + ", mode='" + mode + '\'' + + ", ramp=" + ramp + '}'; + } + } + + private static class MyStromDeviceSpecificInfo extends MyStromBulbResponse { + public double power; + } +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java new file mode 100644 index 0000000000000..893b464bf6965 --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 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.mystrom.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link MyStromDeviceInfo} class contains fields mapping thing thing properties + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public class MyStromDeviceInfo { + public String version = ""; + public String mac = ""; + public long type; + public String ssid = ""; + public String ip = ""; + public String mask = ""; + public String gw = ""; + public String dns = ""; + @SerializedName("static") + public boolean staticState = false; + public boolean connected = false; +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java index d0a2aeb669cab..3c47ee92f4637 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java @@ -12,9 +12,9 @@ */ package org.openhab.binding.mystrom.internal; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.THING_TYPE_BULB; import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.THING_TYPE_PLUG; -import java.util.Collections; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -34,14 +34,15 @@ * handlers. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Add support for myStrom bulb */ @NonNullByDefault @Component(configurationPid = "binding.mystrom", service = ThingHandlerFactory.class) public class MyStromHandlerFactory extends BaseThingHandlerFactory { - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLUG); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PLUG, THING_TYPE_BULB); - private HttpClientFactory httpClientFactory; + private final HttpClientFactory httpClientFactory; @Activate public MyStromHandlerFactory(@Reference HttpClientFactory httpClientFactory) { @@ -58,7 +59,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_PLUG.equals(thingTypeUID)) { - return new MyStromHandler(thing, httpClientFactory.getCommonHttpClient()); + return new MyStromPlugHandler(thing, httpClientFactory.getCommonHttpClient()); + } else if (THING_TYPE_BULB.equals(thingTypeUID)) { + return new MyStromBulbHandler(thing, httpClientFactory.getCommonHttpClient()); } return null; diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java similarity index 53% rename from bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java rename to bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java index 191a45fa8cc19..6bdb9e130dade 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java @@ -19,15 +19,11 @@ import static org.openhab.core.library.unit.Units.WATT; import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpMethod; import org.openhab.core.cache.ExpiringCache; import org.openhab.core.library.types.OnOffType; @@ -36,22 +32,20 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; - /** - * The {@link MyStromHandler} is responsible for handling commands, which are + * The {@link MyStromPlugHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Extends from new abstract class */ @NonNullByDefault -public class MyStromHandler extends BaseThingHandler { +public class MyStromPlugHandler extends AbstractMyStromHandler { private static class MyStromReport { @@ -60,22 +54,12 @@ private static class MyStromReport { public float temperature; } - private static final int HTTP_OK_CODE = 200; - private static final String COMMUNICATION_ERROR = "Error while communicating to the myStrom plug: "; - private static final String HTTP_REQUEST_URL_PREFIX = "http://"; - - private final Logger logger = LoggerFactory.getLogger(MyStromHandler.class); - - private HttpClient httpClient; - private String hostname = ""; + private final Logger logger = LoggerFactory.getLogger(MyStromPlugHandler.class); - private @Nullable ScheduledFuture pollingJob; - private ExpiringCache cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport); - private final Gson gson = new Gson(); + private final ExpiringCache cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport); - public MyStromHandler(Thing thing, HttpClient httpClient) { - super(thing); - this.httpClient = httpClient; + public MyStromPlugHandler(Thing thing, HttpClient httpClient) { + super(thing, httpClient); } @Override @@ -85,7 +69,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { pollDevice(); } else { if (command instanceof OnOffType && CHANNEL_SWITCH.equals(channelUID.getId())) { - sendHttpGet("relay?state=" + (command == OnOffType.ON ? "1" : "0")); + sendHttpRequest(HttpMethod.GET, "/relay?state=" + (command == OnOffType.ON ? "1" : "0"), null); scheduler.schedule(this::pollDevice, 500, TimeUnit.MILLISECONDS); } } @@ -96,7 +80,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { private @Nullable MyStromReport getReport() { try { - String returnContent = sendHttpGet("report"); + String returnContent = sendHttpRequest(HttpMethod.GET, "/report", null); MyStromReport report = gson.fromJson(returnContent, MyStromReport.class); updateStatus(ThingStatus.ONLINE); return report; @@ -106,7 +90,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void pollDevice() { + @Override + protected void pollDevice() { MyStromReport report = cache.getValue(); if (report != null) { updateState(CHANNEL_SWITCH, report.relay ? OnOffType.ON : OnOffType.OFF); @@ -114,46 +99,4 @@ private void pollDevice() { updateState(CHANNEL_TEMPERATURE, QuantityType.valueOf(report.temperature, CELSIUS)); } } - - @Override - public void initialize() { - MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); - this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname; - - updateStatus(ThingStatus.UNKNOWN); - pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevice, 0, config.refresh, TimeUnit.SECONDS); - } - - @Override - public void dispose() { - if (pollingJob != null) { - pollingJob.cancel(true); - pollingJob = null; - } - super.dispose(); - } - - /** - * Given a URL and a set parameters, send a HTTP GET request to the URL location - * created by the URL and parameters. - * - * @param url The URL to send a GET request to. - * @return String contents of the response for the GET request. - * @throws Exception - */ - public String sendHttpGet(String action) throws MyStromException { - String url = hostname + "/" + action; - ContentResponse response = null; - try { - response = httpClient.newRequest(url).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET).send(); - } catch (InterruptedException | TimeoutException | ExecutionException e) { - throw new MyStromException(COMMUNICATION_ERROR + e.getMessage()); - } - - if (response.getStatus() != HTTP_OK_CODE) { - throw new MyStromException( - "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus()); - } - return response.getContentAsString(); - } } diff --git a/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml index 52735564eb0c8..b60710d94d188 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,6 +14,21 @@ + + + + + + + + + + + + + + mac + @@ -30,11 +45,57 @@ + + + Controls the myStrom bulb + + + + + + + + + + + + + + + + + + + + + + + + + mac + + + + + The host name or IP address of the myStrom bulb. + network-address + localhost + + + + Specifies the refresh interval in seconds. + 10 + + + + + + Number:Power The current power delivered by the plug - + @@ -43,4 +104,25 @@ The current temperature at the plug + + + Number:Time + + Transition time from the light’s current state to the new state. + + + + + String + + The color mode we want the Bulb to set to + + + + + + + + + diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index ec2435cb03504..47ba4db643fb8 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -11,35 +11,30 @@ The binding uses the [Nanoleaf OpenAPI](https://forum.nanoleaf.me/docs/openapi), ## Supported Things -Nanoleaf provides a bunch of devices of which some are connected to Wifi whereas other use the new Thread Technology. This binding only supports devices that are connected to Wifi. +Nanoleaf provides a bunch of devices of which some are connected to Wifi whereas other use the new Thread Technology. This binding only supports devices that are connected through Wifi. Currently Nanoleaf's "Light Panels" and "Canvas" devices are supported. -Note that only specific types do support the touch functionality, so the binding needs to check these types. The binding supports two thing types: controller and lightpanel. -The controller thing is the bridge for the individually attached panels/canvas and can be perceived as the nanoleaf device at the wall as a whole (either called "light panels" or "canvas" by Nanoleaf). +The controller thing is the bridge for the individually attached panels/canvas and can be perceived as the Nanoleaf device at the wall as a whole (either called "light panels" or "canvas" by Nanoleaf). With the controller thing you can control channels which affect all panels, e.g. selecting effects or setting the brightness. The lightpanel (singular) thing controls one of the individual panels/canvas that are connected to each other. Each individual panel has therefore its own id assigned to it. -You can set the **color** for each panel or turn it on (white) or off (black) and in the case of a nanoleaf canvas you can even detect single and double **touch events** related to an individual panel which opens a whole new world of controlling any other device within your openHAB environment. +You can set the **color** for each panel and in the case of a Nanoleaf canvas you can even detect single and double **touch events** related to an individual panel which opens a whole new world of controlling any other device within your openHAB environment. | Nanoleaf Name | Type | Description | supported | touch support | | ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- | | Light Panels | NL22 | Triangles 1st Generation | X | (-) | | Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X | -| Shapes Hexagon | NL42 | Triangles 2nd Generation (rounded edges) | (X) | (X) | +| Shapes Hexagon | NL42 | Hexagons | X | X | | Shapes Mini Triangles | ?? | Mini Triangles | ? | ? | | Canvas | NL29 | Squares | X | X | x = Supported (x) = Supported but only tested by community (-) = unknown (no device available to test) -Note: In case of major changes of a binding (like adding more features to a thing) it becomes necessary to delete your things due to the things not being compatible anymore. -Don't worry too much though as they will be easily redetected and nothing really is lost. -Just make sure that you delete them and rediscover as described below. - ## Discovery **Adding the Controller as a Thing** @@ -73,13 +68,16 @@ In this case: - stop and then start openHAB - Rediscover like described above -**Knowing which panel has which id** +### Panel Layout + +Unfortunately it is not easy to find out which panel gets which id, and this becomes pretty important if you have lots of them and want to assign rules. -Unfortunately it is not easy to find out which panel gets which id, and this becomes pretty important if you have lots of them and want to assign rules. -Don't worry as the binding comes with some helpful support in the background the canvas type (this is only provided for the canvas device because triangles can have weird layouts that are hard to express in a log output) +For canvas that use square panels, you can request the layout through a console command: -- Set up a switch item with the channel panelLayout on the controller (see NanoRetrieveLayout below) and set the switch to true -- look out for something like "Panel layout and ids" in the openHAB logs. Below that you will see a panel layout similar to +``` +openhab:nanoleaf layout [] +``` +The `thingUID` is an optional parameter. If it is not provided, the command loops through all Nanoleaf controller things it can find and prints the layout for each of them. Compare the following output with the right picture at the beginning of the article @@ -93,9 +91,7 @@ Compare the following output with the right picture at the beginning of the arti 41451 ``` - -Disclaimer: this works best with square devices and not necessarily well with triangles due to the more geometrically flexible layout. - + ## Thing Configuration The controller thing has the following parameters: @@ -106,7 +102,7 @@ The controller thing has the following parameters: | port | Port number of the light panels contoller. Default is 16021 | | authToken | The authentication token received from the controller after successful pairing. | | refreshInterval | Interval in seconds to refresh the state of the light panels settings. Default is 60. | -| deviceType | (readOnly) defines the type: lightpanels (triangle) or canvas (square) | +| deviceType | Defines the type `lightpanels` (triangle) or `canvas` (square or hexagon) | The lightpanel thing has the following parameters: @@ -115,7 +111,7 @@ The lightpanel thing has the following parameters: | id | ID assigned by the controller to the individual panel (e.g. 158) | The IDs of the individual panels can be determined by starting another scan once the controller is configured and online. -This will add all connected panels with their IDs to the inbox. +This discovers all connected panels with their IDs. ## Channels @@ -123,8 +119,7 @@ The controller bridge has the following channels: | Channel | Item Type | Description | Read Only | |---------------------|-----------|------------------------------------------------------------------------|-----------| -| power | Switch | Power state of the light panels | No | -| color | Color | Color of all light panels | No | +| color | Color | Color, power and brightness of all light panels | No | | colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No | | colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | | colorMode | String | Color mode of the light panels | Yes | @@ -132,48 +127,39 @@ The controller bridge has the following channels: | rhythmState | Switch | Connection state of the rhythm module | Yes | | rhythmActive | Switch | Activity state of the rhythm module | Yes | | rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | -| panelLayout | Switch | Set to true will log out panel layout (returns to off automatically | No | A lightpanel thing has the following channels: -| Channel | Item Type | Description | Read Only | +| Channel | Type | Description | Read Only | |---------------------|-----------|------------------------------------------------------------------------|-----------| -| panelColor | Color | Color of the individual light panel | No | -| singleTap | Switch | [Canvas Only] Is set when the user taps that panel once (1 second pulse) | Yes | -| doubleTap | Switch | [Canvas Only] Is set when the user taps that panel twice (1 second pulse) | Yes | - -**color and panelColor** +| color | Color | Color of the individual light panel | No | +| tap | Trigger | [Canvas Only] Sends events of gestures. Currently, these are SHORT_PRESSED and DOUBLE_PRESSED events. | Yes | -The color and panelColor channels support full color control with hue, saturation and brightness values. +The color channels support full color control with hue, saturation and brightness values. For example, brightness of *all* panels at once can be controlled by defining a dimmer item for the color channel of the *controller thing*. -The same applies to the panelColor channel of an individual lightpanel thing. - -What might not be obvious and even maybe confusing is the fact that brightness and color use the *same* channel but two different *itemTypes*. While the Color-itemtype controls the color, the Dimmer-itemtype controls the brightness on the same channel. +The same applies to the color channel of an individual lightpanel. **Limitations assigning specific colors on individual panels:** - Due to the way the API of the nanoleaf is designed, each time a color is assigned to a panel, it will be directly sent to that panel. The result is that if you send colors to several panels more or less at the same time, they will not be set at the same time but one after the other and rather appear like a sequence but as a one shot. -- Another important limitation is that individual panels cannot be set while a dynamic effect is running on the panel which means that the following happens - - As soon as you set an individual panel a so called "static effect" is created which replaces the chosen dynamic effect. You can even see that in the nanoleaf app that shows that a static effect is now running. - - Unfortunately, at least at the moment, the colors of the current state cannot be retrieved due to the high frequency of color changes that cannot be read quickly enough from the canvas, so all panels go to OFF - - The the first panelColor command is applied to that panel (and of course then all subsequent commands) - - The fact that it is called a static effect does not mean that you cannot create animations. The Rainbow rule below shows a good example for the whole canvas. Just replace the controller item with a panel item and you will get the rainbow effect with an individual panel. +- Another important limitation is that individual panels cannot be set while a dynamic effect is running on the panel which means that as soon as you set an individual panel the "static effect" is set, which disables the chosen dynamic effect. The nanoleaf app shows that a static effect is now running, too. +- The colors of the current state cannot be retrieved due to the high frequency of color changes that cannot be read quickly enough from the canvas, so all panels go to OFF +- The first panelColor command is applied to that panel (and of course then all subsequent commands) +- The fact that it is called a static effect does not mean that you cannot create animations. The Rainbow rule below shows a good example for the whole canvas. Just replace the controller item with a panel item and you will get the rainbow effect with an individual panel. **Touch Support** -Nanoleaf's Canvas introduces a whole new experience by adding touch support to it. This allows single and double taps on individual panels to be detected and then processed via rules to further control any other device! +Nanoleaf's Canvas introduces a whole new experience by supporting touch. This allows single and double taps on individual panels to be detected and processed via rules. Note that even gestures like up, down, left, right are sent but can only be detected on the whole set of panels and not on an individual panel. These four gestures are not yet supported by the binding but may be added in a later release. -To detect single and double taps the panel's have been extended to have two additional channels named singleTap and doubleTap which act like switches that are turned on as soon as a tap type is detected. +To detect single and double taps the panels have been extended to have two additional channels named singleTap and doubleTap which act like switches that are turned on as soon as a tap type is detected. These switches then act as a pulse to further control anything else via rules. -If a panel is tapped the switch is set to ON and automatically reset to OFF after 1 second (this may be configured in the future) to simulate a pulse. A rule can easily detect the transition from OFF to ON and later detect another tap as it is automatically reset by the binding. See the example below on Panel 2. - Keep in mind that the double tap is used as an already built-in functionality by default when you buy the nanoleaf: it switches all panels (hence the controller) to on or off like a light switch for all the panels at once. To circumvent that - Within the nanoleaf app go to the dashboard and choose your device. Enter the settings for that device by clicking the cog icon in the upper right corner. - Enable "Touch Gesture" and assign the gestures you want to happen but set the double tap to unassigned. -- To still have the possibility to switch on the whole canvas device with all its panels by double tapping a specific panel, you can easily write a rule that triggers on the double tap channel of that panel and then toggles the Power Channel of the controller. See the example below on Panel 1. +- To still have the possibility to switch on the whole canvas device with all its panels by double tapping a specific panel, you can easily write a rule that triggers on the tap channel of that panel and then sends an ON to the color channel of the controller. See the example below on Panel 1. More details can be found in the full example below. @@ -190,7 +176,7 @@ Bridge nanoleaf:controller:MyLightPanels @ "mylocation" [ address="192.168.1.100 } ``` -If you define your device statically in the thing file, autodiscovery of the same thing is suppressed by using +If you define your device statically in the thing file, auto-discovery of the same thing is suppressed by using * the [address="..." ] of the controller * and the [id=123] of the lightpanel @@ -208,13 +194,13 @@ e.g. via command line `curl --location --request POST 'http://
:16021/ap ### nanoleaf.items -Note: If you did autodiscover your things and items: +Note: If you auto-discovered your things and items: - A controller item looks like nanoleaf:controller:F0ED4F9351AF:power where F0ED4F9351AF is the id of the controller that has been automatically assigned by the binding. - A panel item looks like nanoleaf:lightpanel:F0ED4F9351AF:39755:singleTap where 39755 is the id of the panel that has been automatically assigned by the binding. ``` -Switch NanoleafPower "Nanoleaf" { channel="nanoleaf:controller:MyLightPanels:power" } +Switch NanoleafPower "Nanoleaf" { channel="nanoleaf:controller:MyLightPanels:color" } Color NanoleafColor "Color" { channel="nanoleaf:controller:MyLightPanels:color" } Dimmer NanoleafBrightness "Brightness [%.0f]" { channel="nanoleaf:controller:MyLightPanels:color" } String NanoleafHue "Hue [%s]" @@ -226,15 +212,11 @@ String NanoleafEffect "Effect" { channel="nanoleaf:controller:MyLightPanels:effe Switch NanoleafRhythmState "Rhythm connected [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmState" } Switch NanoleafRhythmActive "Rhythm active [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmActive" } Number NanoleafRhythmSource "Rhythm source [%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmMode" } -Switch NanoRetrieveLayout "Nano Layout" { channel="nanoleaf:controller:D81E7A7E424E:panelLayout" } // note that the next to items use the exact same channel but the two different types Color and Dimmer to control different parameters -Color Panel1Color "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" } -Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" } -Switch Panel1DoubleTap "Toggle device on and off" { channel="nanoleaf:lightpanel:MyLightPanels:135:doubleTap" } -Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:panelColor" } -Switch Panel2SingleTap "Panel 2 Single Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:singleTap" } -Switch Panel2DoubleTap "Panel 2 Double Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:doubleTap" } +Color PanelColor "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" } +Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" } +Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:color" } Switch NanoleafRainbowScene "Show Rainbow Scene" ``` @@ -252,11 +234,10 @@ sitemap nanoleaf label="Nanoleaf" Slider item=NanoleafColorTemp Setpoint item=NanoleafColorTempAbs step=100 minValue=1200 maxValue=6500 Text item=NanoleafColorMode - Selection item=NanoleafEffect mappings=["Color Burst"="Color Burst", "Fireworks" = "Fireworks", "Flames" = "Flames", "Forest" = "Forest", "Inner Peace" = "Inner Peace", "Meteor Shower" = "Meteor Shower", "Nemo" = "Nemo", "Northern Lights" = "Northern Lights", "Paint Splatter" = "Paint Splatter", "Pulse Pop Beats" = "Pulse Pop Beats", "Rhythmic Northern Lights" = "Rhythmic Northern Lights", "Ripple" = "Ripple", "Romantic" = "Romantic", "Snowfall" = "Snowfall", "Sound Bar" = "Sound Bar", "Streaking Notes" = "Streaking Notes", "moonlight" = "Moonlight", "*Static*" = "Color (single panels)", "*Dynamic*" = "Color (all panels)" ] + Selection item=NanoleafEffect Text item=NanoleafRhythmState Text item=NanoleafRhythmActive Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"] - Switch item=NanoRetrieveLayout } Frame label="Panels" { @@ -271,9 +252,6 @@ sitemap nanoleaf label="Nanoleaf" } ``` -Note: The mappings to effects in the selection item are specific for each Nanoleaf installation and should be adapted accordingly. -Only the effects "\*Static\*" and "\*Dynamic\*" are predefined by the controller and should always be present in the mappings. - ### nanoleaf.rules ``` @@ -313,8 +291,7 @@ end rule "Nanoleaf canvas touch detection Panel 2" when - Item Panel2SingleTap changed from NULL to ON or - Item Panel2SingleTap changed from OFF to ON + Channel "nanoleaf:lightpanel:MyLightPanels:158:tap" triggered SHORT_PRESS then logInfo("CanvasTouch", "Nanoleaf Canvas Panel 2 was touched once") @@ -327,8 +304,7 @@ end rule "Nanoleaf double tap toggles power of device" when - Item Panel1DoubleTap changed from NULL to ON or - Item Panel1DoubleTap changed from OFF to ON + Channel "nanoleaf:lightpanel:MyLightPanels:135:tap" triggered DOUBLE_PRESS then logInfo("CanvasTouch", "Nanoleaf Canvas Panel 1 was touched twice. Toggle Power of whole canvas.") diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index 57933199d4641..6a4b08f7da80b 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -27,7 +27,7 @@ @NonNullByDefault public class NanoleafBindingConstants { - private static final String BINDING_ID = "nanoleaf"; + public static final String BINDING_ID = "nanoleaf"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller"); @@ -44,7 +44,6 @@ public class NanoleafBindingConstants { public static final String CONFIG_PANEL_ID = "id"; // List of controller channels - public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_COLOR = "color"; public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature"; public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs"; @@ -53,12 +52,10 @@ public class NanoleafBindingConstants { public static final String CHANNEL_RHYTHM_STATE = "rhythmState"; public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive"; public static final String CHANNEL_RHYTHM_MODE = "rhythmMode"; - public static final String CHANNEL_PANEL_LAYOUT = "panelLayout"; // List of light panel channels - public static final String CHANNEL_PANEL_COLOR = "panelColor"; - public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap"; - public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap"; + public static final String CHANNEL_PANEL_COLOR = "color"; + public static final String CHANNEL_PANEL_TAP = "tap"; // Nanoleaf OpenAPI URLs public static final String API_V1_BASE_URL = "/api/v1"; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java index 8c7f2a32e5663..f942a10f0d698 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java @@ -15,9 +15,6 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*; import java.util.Collections; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,19 +22,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler; import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler; -import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.io.net.http.HttpClientFactory; 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.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -49,6 +42,7 @@ * and panel (thing) handlers. * * @author Martin Raepple - Initial contribution + * @author Kai Kreuzer - made discovery a handler service */ @NonNullByDefault @Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class) @@ -58,7 +52,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory { .unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet())); private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class); - private final Map> discoveryServiceRegs = new HashMap<>(); private final HttpClient httpClient; @Activate @@ -77,7 +70,6 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) { NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient); - registerDiscoveryService(handler); logger.debug("Nanoleaf controller handler created."); return handler; } else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) { @@ -87,30 +79,4 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } return null; } - - @Override - protected void removeHandler(ThingHandler thingHandler) { - if (thingHandler instanceof NanoleafControllerHandler) { - unregisterDiscoveryService(thingHandler.getThing()); - logger.debug("Nanoleaf controller handler removed."); - } - } - - private synchronized void registerDiscoveryService(NanoleafControllerHandler bridgeHandler) { - NanoleafPanelsDiscoveryService discoveryService = new NanoleafPanelsDiscoveryService(bridgeHandler); - discoveryServiceRegs.put(bridgeHandler.getThing().getUID(), - bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); - logger.debug("Discovery service for panels registered."); - } - - @SuppressWarnings("null") - private synchronized void unregisterDiscoveryService(Thing thing) { - @Nullable - ServiceRegistration serviceReg = discoveryServiceRegs.remove(thing.getUID()); - // would require null check but "if (response!=null)" throws warning on comoile time :´-( - if (serviceReg != null) { - serviceReg.unregister(); - } - logger.debug("Discovery service for panels removed."); - } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java index 1875d474f7afe..af7658cb52c61 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -57,7 +58,7 @@ public static Request requestBuilder(HttpClient httpClient, NanoleafControllerCo LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(), requestURI.getPath()); - return httpClient.newRequest(requestURI).method(method); + return httpClient.newRequest(requestURI).method(method).timeout(10, TimeUnit.SECONDS); } public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query) diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java new file mode 100644 index 0000000000000..d47e100729d19 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2021 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.nanoleaf.internal.command; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; +import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Console commands for interacting with Nanoleaf integration + * + * @author Kai Kreuzer - Initial contribution + */ +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class NanoleafCommandExtension extends AbstractConsoleCommandExtension { + + private static final String CMD_LAYOUT = "layout"; + private final ThingRegistry thingRegistry; + + @Activate + public NanoleafCommandExtension(@Reference ThingRegistry thingRegistry) { + super("nanoleaf", "Interact with the Nanoleaf integration."); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length > 0) { + String subCommand = args[0]; + switch (subCommand) { + case CMD_LAYOUT: + if (args.length == 1) { + thingRegistry.getAll().forEach(thing -> { + if (thing.getUID().getBindingId().equals(NanoleafBindingConstants.BINDING_ID)) { + ThingHandler handler = thing.getHandler(); + if (handler instanceof NanoleafControllerHandler) { + NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler; + String layout = nanoleafControllerHandler.getLayout(); + console.println("Layout of Nanoleaf controller '" + thing.getUID().getAsString() + + "' with label '" + thing.getLabel() + "':" + System.lineSeparator()); + console.println(layout); + console.println(System.lineSeparator()); + } + } + }); + } else if (args.length == 2) { + String uid = args[1]; + Thing thing = thingRegistry.get(new ThingUID(uid)); + if (thing != null) { + ThingHandler handler = thing.getHandler(); + if (handler instanceof NanoleafControllerHandler) { + NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler; + String layout = nanoleafControllerHandler.getLayout(); + console.println(layout); + } else { + console.println("Thing with UID '" + uid.toString() + + "' is not an initialized Nanoleaf controller."); + } + } else { + console.println("Thing with UID '" + uid.toString() + "' does not exist."); + } + } else { + printUsage(console); + } + break; + + default: + console.println("Unknown command '" + subCommand + "'"); + printUsage(console); + break; + } + } + } + + @Override + public List getUsages() { + return Arrays.asList(buildCommandUsage(CMD_LAYOUT + " ", "Prints the panel layout on the console.")); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java new file mode 100644 index 0000000000000..3d59ba3f53035 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2021 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.nanoleaf.internal.commanddescription; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; +import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener; +import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler; +import org.openhab.binding.nanoleaf.internal.model.ControllerInfo; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; +import org.openhab.core.types.CommandOption; +import org.osgi.service.component.annotations.Component; + +/** + * This class provides the available effects as dynamic options as they are read from the Nanoleaf controller. + * + * @author Kai Kreuzer - Initial contribution + * + */ +@NonNullByDefault +@Component(service = { DynamicCommandDescriptionProvider.class }) +public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider + implements NanoleafControllerListener, ThingHandlerService { + + private @Nullable ChannelUID effectChannelUID; + + private @Nullable NanoleafControllerHandler bridgeHandler; + + @Override + public void setThingHandler(ThingHandler handler) { + this.bridgeHandler = (NanoleafControllerHandler) handler; + bridgeHandler.registerControllerListener(this); + effectChannelUID = new ChannelUID(handler.getThing().getUID(), NanoleafBindingConstants.CHANNEL_EFFECT); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + public void deactivate() { + if (bridgeHandler != null) { + bridgeHandler.unregisterControllerListener(this); + } + super.deactivate(); + } + + @Override + public void onControllerInfoFetched(@NonNull ThingUID bridge, @NonNull ControllerInfo controllerInfo) { + List<@NonNull String> effects = controllerInfo.getEffects().getEffectsList(); + ChannelUID uid = effectChannelUID; + if (effects != null && uid != null && uid.getThingUID().equals(bridge)) { + List<@NonNull CommandOption> commandOptions = effects.stream() // + .map(effect -> new CommandOption(effect, effect)) // + .collect(Collectors.toList()); + setCommandOptions(uid, commandOptions); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafMDNSDiscoveryParticipant.java index 751f90712e98a..aa18f063f8e49 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafMDNSDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafMDNSDiscoveryParticipant.java @@ -79,10 +79,10 @@ public String getServiceType() { logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port, firmwareVersion, modelId, qualifiedName); - logger.debug("Adding Nanoleaf controller {} with FW version {} found at {} {} to inbox", qualifiedName, + logger.debug("Adding Nanoleaf controller {} with FW version {} found at {}:{} to inbox", qualifiedName, firmwareVersion, host, port); if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) { - logger.warn("Nanoleaf controller firmware is too old. Must be {} or higher", + logger.debug("Nanoleaf controller firmware is too old. Must be {} or higher", MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS); } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java index 391d36f63b000..73e4404718b9d 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java @@ -33,6 +33,8 @@ import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,36 +43,37 @@ * panels connected to the controller. * * @author Martin Raepple - Initial contribution + * @author Kai Kreuzer - Made it a ThingHandlerService */ @NonNullByDefault -public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService implements NanoleafControllerListener { +public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService + implements NanoleafControllerListener, ThingHandlerService { private static final int SEARCH_TIMEOUT_SECONDS = 60; private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class); - private final NanoleafControllerHandler bridgeHandler; + private @Nullable NanoleafControllerHandler bridgeHandler; + private @Nullable ControllerInfo controllerInfo; /** - * Constructs a new {@link NanoleafPanelsDiscoveryService} attached to the given bridge handler. - * - * @param nanoleafControllerHandler The bridge handler this discovery service is attached to + * Constructs a new {@link NanoleafPanelsDiscoveryService}. */ - public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) { + public NanoleafPanelsDiscoveryService() { super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false); - this.bridgeHandler = nanoleafControllerHandler; } @Override - protected void startScan() { - logger.debug("Starting Nanoleaf panel discovery"); - bridgeHandler.registerControllerListener(this); + public void deactivate() { + if (bridgeHandler != null) { + bridgeHandler.unregisterControllerListener(this); + } + super.deactivate(); } @Override - protected synchronized void stopScan() { - logger.debug("Stopping Nanoleaf panel discovery"); - super.stopScan(); - bridgeHandler.unregisterControllerListener(this); + protected void startScan() { + logger.debug("Starting Nanoleaf panel discovery"); + createResultsFromControllerInfo(); } /** @@ -81,38 +84,60 @@ protected synchronized void stopScan() { */ @Override public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) { - logger.debug("Discover panels connected to controller with id {}", bridge.getAsString()); - final PanelLayout panelLayout = controllerInfo.getPanelLayout(); - @Nullable - Layout layout = panelLayout.getLayout(); + this.controllerInfo = controllerInfo; + } - if (layout != null && layout.getNumPanels() > 0) { + private void createResultsFromControllerInfo() { + ThingUID bridgeUID; + if (bridgeHandler != null) { + bridgeUID = bridgeHandler.getThing().getUID(); + } else { + return; + } + if (controllerInfo != null) { + final PanelLayout panelLayout = controllerInfo.getPanelLayout(); @Nullable - final List positionData = layout.getPositionData(); - if (positionData != null) { - Iterator iterator = positionData.iterator(); - while (iterator.hasNext()) { - @Nullable - PositionDatum panel = iterator.next(); - ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, bridge, - Integer.toString(panel.getPanelId())); - - final Map properties = new HashMap<>(1); - properties.put(CONFIG_PANEL_ID, panel.getPanelId()); - - DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridge) - .withProperties(properties).withLabel("Light Panel " + panel.getPanelId()) - .withRepresentationProperty(CONFIG_PANEL_ID).build(); - - logger.debug("Adding panel with id {} to inbox", panel.getPanelId()); - thingDiscovered(newPanel); + Layout layout = panelLayout.getLayout(); + + if (layout != null && layout.getNumPanels() > 0) { + @Nullable + final List positionData = layout.getPositionData(); + if (positionData != null) { + Iterator iterator = positionData.iterator(); + while (iterator.hasNext()) { + @Nullable + PositionDatum panel = iterator.next(); + ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, + bridgeUID, Integer.toString(panel.getPanelId())); + + final Map properties = new HashMap<>(1); + properties.put(CONFIG_PANEL_ID, panel.getPanelId()); + + DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridgeUID) + .withProperties(properties).withLabel("Light Panel " + panel.getPanelId()) + .withRepresentationProperty(CONFIG_PANEL_ID).build(); + + logger.debug("Adding panel with id {} to inbox", panel.getPanelId()); + thingDiscovered(newPanel); + } + } else { + logger.debug("Couldn't add panels to inbox as layout position data was null"); } + } else { - logger.debug("Couldn't add panels to inbox as layout position data was null"); + logger.debug("No panels found or connected to controller"); } - - } else { - logger.info("No panels found or connected to controller"); } } + + @Override + public void setThingHandler(ThingHandler handler) { + this.bridgeHandler = (NanoleafControllerHandler) handler; + this.bridgeHandler.registerControllerListener(this); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index ce5d0f0677a32..c36ef3bcf1f18 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -17,7 +17,11 @@ import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Scanner; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; @@ -36,10 +40,11 @@ import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener; import org.openhab.binding.nanoleaf.internal.NanoleafException; -import org.openhab.binding.nanoleaf.internal.NanoleafInterruptedException; import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException; import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; +import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; +import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.Brightness; @@ -67,6 +72,7 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.slf4j.Logger; @@ -81,15 +87,13 @@ * * @author Martin Raepple - Initial contribution * @author Stefan Höhn - Canvas Touch Support + * @author Kai Kreuzer - refactoring, bug fixing and code clean up */ @NonNullByDefault public class NanoleafControllerHandler extends BaseBridgeHandler { // Pairing interval in seconds - private static final int PAIRING_INTERVAL = 25; - - // Panel discovery interval in seconds - private static final int PANEL_DISCOVERY_INTERVAL = 30; + private static final int PAIRING_INTERVAL = 10; private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class); private HttpClient httpClient; @@ -98,7 +102,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { // Pairing, update and panel discovery jobs and touch event job private @NonNullByDefault({}) ScheduledFuture pairingJob; private @NonNullByDefault({}) ScheduledFuture updateJob; - private @NonNullByDefault({}) ScheduledFuture panelDiscoveryJob; private @NonNullByDefault({}) ScheduledFuture touchJob; // JSON parser for API responses @@ -120,7 +123,7 @@ public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) { @Override public void initialize() { logger.debug("Initializing the controller (bridge)"); - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED); + updateStatus(ThingStatus.UNKNOWN); NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); setAddress(config.address); setPort(config.port); @@ -157,13 +160,9 @@ public void initialize() { "@text/error.nanoleaf.controller.noToken"); startPairingJob(); stopUpdateJob(); - stopPanelDiscoveryJob(); } else { - logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs"); - updateStatus(ThingStatus.ONLINE); stopPairingJob(); startUpdateJob(); - startPanelDiscoveryJob(); startTouchJob(); } } catch (IllegalArgumentException iae) { @@ -186,11 +185,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { updateFromControllerInfo(); } else { switch (channelUID.getId()) { - case CHANNEL_POWER: case CHANNEL_COLOR: case CHANNEL_COLOR_TEMPERATURE: case CHANNEL_COLOR_TEMPERATURE_ABS: - case CHANNEL_PANEL_LAYOUT: sendStateCommand(channelUID.getId(), command); break; case CHANNEL_EFFECT: @@ -218,25 +215,28 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void handleRemoval() { - // delete token for openHAB - ContentResponse deleteTokenResponse; - try { - Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER, - HttpMethod.DELETE); - deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest); - if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) { - logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus()); - return; + scheduler.execute(() -> { + // delete token for openHAB + ContentResponse deleteTokenResponse; + try { + Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), + API_DELETE_USER, HttpMethod.DELETE); + deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest); + if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) { + logger.warn("Failed to delete token for openHAB. Response code is {}", + deleteTokenResponse.getStatus()); + return; + } + logger.debug("Successfully deleted token for openHAB from controller"); + } catch (NanoleafUnauthorizedException e) { + logger.warn("Attempt to delete token for openHAB failed. Token unauthorized."); + } catch (NanoleafException ne) { + logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage()); } - logger.debug("Successfully deleted token for openHAB from controller"); - } catch (NanoleafUnauthorizedException e) { - logger.warn("Attempt to delete token for openHAB failed. Token unauthorized."); - } catch (NanoleafException ne) { - logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage()); - } - stopAllJobs(); - super.handleRemoval(); - logger.debug("Nanoleaf controller removed"); + stopAllJobs(); + super.handleRemoval(); + logger.debug("Nanoleaf controller removed"); + }); } @Override @@ -246,34 +246,37 @@ public void dispose() { logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID()); } + @Override + public Collection> getServices() { + return List.of(NanoleafPanelsDiscoveryService.class, NanoleafCommandDescriptionProvider.class); + } + public boolean registerControllerListener(NanoleafControllerListener controllerListener) { logger.debug("Register new listener for controller {}", getThing().getUID()); - boolean result = controllerListeners.add(controllerListener); - if (result) { - startPanelDiscoveryJob(); - } - return result; + return controllerListeners.add(controllerListener); } public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) { logger.debug("Unregister listener for controller {}", getThing().getUID()); - boolean result = controllerListeners.remove(controllerListener); - if (result) { - stopPanelDiscoveryJob(); - } - return result; + return controllerListeners.remove(controllerListener); } public NanoleafControllerConfig getControllerConfig() { NanoleafControllerConfig config = new NanoleafControllerConfig(); config.address = Objects.requireNonNullElse(getAddress(), ""); config.port = getPort(); - config.refreshInterval = getRefreshIntervall(); + config.refreshInterval = getRefreshInterval(); config.authToken = getAuthToken(); config.deviceType = Objects.requireNonNullElse(getDeviceType(), ""); return config; } + public String getLayout() { + Layout layout = controllerInfo.getPanelLayout().getLayout(); + String layoutView = (layout != null) ? layout.getLayoutView() : ""; + return layoutView; + } + public synchronized void startPairingJob() { if (pairingJob == null || pairingJob.isCancelled()) { logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL); @@ -293,8 +296,8 @@ private synchronized void startUpdateJob() { String localAuthToken = getAuthToken(); if (localAuthToken != null && !localAuthToken.isEmpty()) { if (updateJob == null || updateJob.isCancelled()) { - logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall()); - updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(), + logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval()); + updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(), TimeUnit.SECONDS); } } else { @@ -311,24 +314,6 @@ private synchronized void stopUpdateJob() { } } - public synchronized void startPanelDiscoveryJob() { - logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}", - !controllerListeners.isEmpty(), panelDiscoveryJob); - if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) { - logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL); - panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL, - TimeUnit.SECONDS); - } - } - - private synchronized void stopPanelDiscoveryJob() { - if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) { - logger.debug("Stop panel discovery job"); - panelDiscoveryJob.cancel(true); - this.panelDiscoveryJob = null; - } - } - private synchronized void startTouchJob() { NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) { @@ -367,11 +352,7 @@ private void runUpdate() { try { updateFromControllerInfo(); startTouchJob(); // if device type has changed, start touch detection. - // controller might have been offline, e.g. for firmware update. In this case, return to online state - if (ThingStatus.OFFLINE.equals(getThing().getStatus())) { - logger.debug("Controller {} is back online", thing.getUID()); - updateStatus(ThingStatus.ONLINE); - } + updateStatus(ThingStatus.ONLINE); } catch (NanoleafUnauthorizedException nae) { logger.warn("Status update unauthorized: {}", nae.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -403,7 +384,8 @@ private void runPairing() { return; } ContentResponse authTokenResponse = OpenAPIUtils - .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send(); + .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST) + .timeout(20, TimeUnit.SECONDS).send(); if (logger.isTraceEnabled()) { logger.trace("Auth token response: {}", authTokenResponse.getContentAsString()); } @@ -429,7 +411,6 @@ private void runPairing() { stopPairingJob(); startUpdateJob(); - startPanelDiscoveryJob(); startTouchJob(); } else { logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString()); @@ -446,7 +427,7 @@ private void runPairing() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.nanoleaf.controller.noTokenReceived"); } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.warn("Cannot send authorization request to controller: ", e); + logger.debug("Cannot send authorization request to controller: ", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.nanoleaf.controller.authRequest"); } catch (RuntimeException e) { @@ -459,34 +440,6 @@ private void runPairing() { } } - private void runPanelDiscovery() { - logger.debug("Run panel discovery job"); - // Trigger a new discovery of connected panels - for (NanoleafControllerListener controllerListener : controllerListeners) { - try { - controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo()); - } catch (NanoleafUnauthorizedException nue) { - logger.warn("Panel discovery unauthorized: {}", nue.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.nanoleaf.controller.invalidToken"); - String localAuthToken = getAuthToken(); - if (localAuthToken == null || localAuthToken.isEmpty()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "@text/error.nanoleaf.controller.noToken"); - } - } catch (NanoleafInterruptedException nie) { - logger.info("Panel discovery has been stopped."); - } catch (NanoleafException ne) { - logger.warn("Failed to discover panels: ", ne); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.nanoleaf.controller.communication"); - } catch (RuntimeException e) { - logger.warn("Panel discovery job failed", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime"); - } - } - } - /** * This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq */ @@ -579,14 +532,9 @@ private void handleTouchEvents(TouchEvents touchEvents) { private void updateFromControllerInfo() throws NanoleafException { logger.debug("Update channels for controller {}", thing.getUID()); this.controllerInfo = receiveControllerInfo(); - if (controllerInfo == null) { - logger.debug("No Controller Info has been provided"); - return; - } final State state = controllerInfo.getState(); OnOffType powerState = state.getOnOff(); - updateState(CHANNEL_POWER, powerState); @Nullable Ct colorTemperature = state.getColorTemperature(); @@ -662,6 +610,10 @@ private void updateFromControllerInfo() throws NanoleafException { panelHandler.updatePanelColorChannel(); } }); + + for (NanoleafControllerListener controllerListener : controllerListeners) { + controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); + } } private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException { @@ -674,17 +626,6 @@ private ControllerInfo receiveControllerInfo() throws NanoleafException, Nanolea private void sendStateCommand(String channel, Command command) throws NanoleafException { State stateObject = new State(); switch (channel) { - case CHANNEL_POWER: - if (command instanceof OnOffType) { - // On/Off command - turns controller on/off - BooleanState state = new On(); - state.setValue(OnOffType.ON.equals(command)); - stateObject.setState(state); - } else { - logger.warn("Unhandled command type: {}", command.getClass().getName()); - return; - } - break; case CHANNEL_COLOR: if (command instanceof OnOffType) { // On/Off command - turns controller on/off @@ -780,13 +721,6 @@ private void sendStateCommand(String channel, Command command) throws NanoleafEx return; } break; - case CHANNEL_PANEL_LAYOUT: - @Nullable - Layout layout = controllerInfo.getPanelLayout().getLayout(); - String layoutView = (layout != null) ? layout.getLayoutView() : ""; - logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView); - updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF); - break; default: logger.warn("Unhandled command type: {}", command.getClass().getName()); return; @@ -844,7 +778,7 @@ private void setPort(int port) { this.port = port; } - private int getRefreshIntervall() { + private int getRefreshInterval() { return refreshIntervall; } @@ -871,7 +805,6 @@ private void setDeviceType(String deviceType) { private void stopAllJobs() { stopPairingJob(); stopUpdateJob(); - stopPanelDiscoveryJob(); stopTouchJob(); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java index 628f11960693d..f25c54822ecd8 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java @@ -15,11 +15,11 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -28,13 +28,21 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpMethod; -import org.openhab.binding.nanoleaf.internal.*; +import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException; +import org.openhab.binding.nanoleaf.internal.NanoleafException; +import org.openhab.binding.nanoleaf.internal.NanoleafNotFoundException; +import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException; +import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.model.Effects; import org.openhab.binding.nanoleaf.internal.model.Write; -import org.openhab.core.library.types.*; +import org.openhab.core.library.types.HSBType; +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.CommonTriggerEvents; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; @@ -165,8 +173,9 @@ private void initializePanel(ThingStatusInfo panelStatus) { private void sendRenderedEffectCommand(Command command) throws NanoleafException { logger.debug("Command Type: {}", command.getClass()); HSBType currentPanelColor = getPanelColor(); - if (currentPanelColor != null) + if (currentPanelColor != null) { logger.debug("currentPanelColor: {}", currentPanelColor.toString()); + } HSBType newPanelColor = new HSBType(); if (command instanceof HSBType) { @@ -205,11 +214,11 @@ private void sendRenderedEffectCommand(Command command) throws NanoleafException // transform to RGB PercentType[] rgbPercent = newPanelColor.toRGB(); logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]); - int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP) + int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP) .multiply(new BigDecimal(255)).intValue(); - int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP) + int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP) .multiply(new BigDecimal(255)).intValue(); - int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP) + int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP) .multiply(new BigDecimal(255)).intValue(); logger.trace("Setting new rgb {} {} {}", red, green, blue); Bridge bridge = getBridge(); @@ -253,8 +262,9 @@ public void updatePanelColorChannel() { @Nullable HSBType panelColor = getPanelColor(); logger.trace("updatePanelColorChannel: panelColor: {}", panelColor); - if (panelColor != null) + if (panelColor != null) { updateState(CHANNEL_PANEL_COLOR, panelColor); + } } /** @@ -265,28 +275,14 @@ public void updatePanelColorChannel() { public void updatePanelGesture(int gesture) { switch (gesture) { case 0: - updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON); - singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS); - logger.debug("Asserting single tap of panel {} to ON", getPanelID()); + triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.SHORT_PRESSED); break; case 1: - updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON); - doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS); - logger.debug("Asserting double tap of panel {} to ON", getPanelID()); + triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.DOUBLE_PRESSED); break; } } - private void resetSingleTap() { - updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF); - logger.debug("Resetting single tap of panel {} to OFF", getPanelID()); - } - - private void resetDoubleTap() { - updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF); - logger.debug("Resetting double tap of panel {} to OFF", getPanelID()); - } - public String getPanelID() { String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(); return panelID; @@ -327,7 +323,7 @@ public String getPanelID() { } catch (NanoleafException nue) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.nanoleaf.panel.communication"); - logger.warn("Panel data could not be retrieved: {}", nue.getMessage()); + logger.debug("Panel data could not be retrieved: {}", nue.getMessage()); } return panelInfo.get(panelID); diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java index dd8a3230163a8..2c3db6be1e568 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java @@ -60,7 +60,7 @@ public void setPositionData(List positionData) { * Returns an text representation for a canvas layout. * * Note only canvas supported currently due to its easy geometry - * + * * @return a String containing the layout */ public String getLayoutView() { @@ -97,6 +97,11 @@ public String getLayoutView() { int shiftWidth = getSideLength() / 2; + if (shiftWidth == 0) { + // seems we do not have squares here + return "Cannot render layout. Please note that layout views are only supported for square panels."; + } + int lineY = maxy; Map map; @@ -108,8 +113,9 @@ public String getLayoutView() { @Nullable PositionDatum panel = positionData.get(index); - if (panel != null && panel.getPosY() == lineY) + if (panel != null && panel.getPosY() == lineY) { map.put(panel.getPosX(), panel); + } } } lineY -= shiftWidth; @@ -118,14 +124,16 @@ public String getLayoutView() { @Nullable PositionDatum panel = map.get(x); view += String.format("%5s ", panel.getPanelId()); - } else + } else { view += " "; + } } - view += "\n"; + view += System.lineSeparator(); } return view; - } else + } else { return ""; + } } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml index 22348de623dc6..76ce1758656a2 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml @@ -25,7 +25,7 @@ @text/thing-type.config.nanoleaf.controller.refreshInterval.description 60 - + @text/thing-type.config.nanoleaf.controller.deviceType.description lightPanels diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index 39aefb6623b89..caf3424be8f77 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -1,5 +1,5 @@ binding.nanoleaf.name = Nanoleaf Binding -binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v010221) +binding.nanoleaf.description = Integrates the Nanoleaf light panels # thing types thing-type.nanoleaf.controller.name = Nanoleaf Controller @@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = ID Of The Panel thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller # channel types -channel-type.nanoleaf.power.label = Power -channel-type.nanoleaf.power.description = Power state of the controller -channel-type.nanoleaf.color.label = Color -channel-type.nanoleaf.color.description = Color setting for an individual or all panels -channel-type.nanoleaf.colorTemperature.label = Color Temperature -channel-type.nanoleaf.colorTemperature.description = Color temperature in percent -channel-type.nanoleaf.colorTemperatureAbs.label = Color Temperature (K) -channel-type.nanoleaf.colorTemperatureAbs.description = Color temperature in Kelvin (K) channel-type.nanoleaf.colorMode.label = Color Mode channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature channel-type.nanoleaf.effect.label = Effect @@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Active channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module channel-type.nanoleaf.rhythmMode.label = Rhythm Mode channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable) -channel-type.nanoleaf.panelLayout.label = Panel Layout -channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request channel-type.nanoleaf.panelColor.label = Panel Color channel-type.nanoleaf.panelColor.description = Color of the individual panel -channel-type.nanoleaf.singleTap.label = SingleTap -channel-type.nanoleaf.singleTap.description = Single tap on the panel -channel-type.nanoleaf.doubleTap.label = DoubleTap -channel-type.nanoleaf.doubleTap.description = Double tap on the panel +channel-type.nanoleaf.tap.label = Button +channel-type.nanoleaf.tap.description = Button events of the panel # error messages error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again. error.nanoleaf.controller.noToken = No authorization token found. To start pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern. error.nanoleaf.controller.invalidToken = Invalid token. Replace with valid token or start pairing again by removing the invalid token from the configuration. -error.nanoleaf.controller.communication = Communication failed. Please check configuration. +error.nanoleaf.controller.communication = Communication failed. Please check your network and configuration. error.nanoleaf.controller.pairingFailed = Pairing failed. Press the on-off button for 5-7 seconds until the LED starts flashing in a pattern. error.nanoleaf.controller.invalidData = Pairing failed. Received invalid data error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties index b21023e07cdd4..d94531575837c 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties @@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = Paneel ID thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels # channel types -channel-type.nanoleaf.power.label = Power -channel-type.nanoleaf.power.description = Ermöglicht das An- und Ausschalten des Nanoleaf Light Panels -channel-type.nanoleaf.color.label = Farbe -channel-type.nanoleaf.color.description = Farbe aller oder eines einzelnen Paneels -channel-type.nanoleaf.colorTemperature.label = Farbtemperatur -channel-type.nanoleaf.colorTemperature.description = Farbtemperatur in Prozent -channel-type.nanoleaf.colorTemperatureAbs.label = Farbtemperatur (K) -channel-type.nanoleaf.colorTemperatureAbs.description = Farbtemperatur in Kelvin (K) channel-type.nanoleaf.colorMode.label = Farbmodus channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele. channel-type.nanoleaf.effect.label = Effekt @@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist. channel-type.nanoleaf.rhythmMode.label = Rhythm Modus channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel. -channel-type.nanoleaf.panelLayout.label = PanelLayout -channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout channel-type.nanoleaf.panelColor.label = Paneelfarbe channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels -channel-type.nanoleaf.singleTap.label = Einzel-Tap -channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt -channel-type.nanoleaf.doubleTap.label = Doppel-Tap -channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt +channel-type.nanoleaf.tap.label = Taster +channel-type.nanoleaf.tap.description = Tastevents des Panels # error messages error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert. error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen. error.nanoleaf.controller.noToken = Kein Authentifizierungstoken gefunden. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken. error.nanoleaf.controller.invalidToken = Ungültiges Authentifizierungstoken. Token ändern oder das Pairing neu starten durch Löschen des ungültigen Tokens. -error.nanoleaf.controller.communication = Kommunikationsfehler. Konfiguration des Controllers prüfen. +error.nanoleaf.controller.communication = Kommunikationsfehler. Netzwerk und Konfiguration des Controllers prüfen. error.nanoleaf.controller.pairingFailed = Pairing fehlgeschlagen. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken. error.nanoleaf.controller.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen. error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index a8a7f33d80cc9..5aa026613d7e1 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -9,16 +9,14 @@ @text/thing-type.nanoleaf.controller.description - - - - + + + - @@ -40,9 +38,8 @@ @text/thing-type.nanoleaf.lightpanel.description - - - + + id @@ -50,35 +47,13 @@ - - Switch - - @text/channel-type.nanoleaf.power.description - Switch - - - - - - - - - - Dimmer - - @text/channel-type.nanoleaf.colorTemperature.description - - - - - Number - - @text/channel-type.nanoleaf.colorTemperatureAbs.description - ColorLight - + + String + + @text/channel-type.nanoleaf.effect.description - + String @text/channel-type.nanoleaf.colorMode.description @@ -91,62 +66,30 @@ - - Color - - @text/Color of the Light Panels - - - - String - - @text/channel-type.nanoleaf.effect.description - - - + Switch @text/channel-type.nanoleaf.rhythmState.description - + Switch @text/channel-type.nanoleaf.rhythmActive.description - + Number @text/channel-type.nanoleaf.rhythmMode.description - + - - Switch - - @text/channel-type.nanoleaf.singleTap.description - - - - - Switch - - @text/channel-type.nanoleaf.doubleTap.description - - - - - Switch - - @text/channel-type.nanoleaf.panelLayout.description - - diff --git a/bundles/org.openhab.binding.neato/src/main/java/org/openhab/binding/neato/internal/handler/NeatoHandler.java b/bundles/org.openhab.binding.neato/src/main/java/org/openhab/binding/neato/internal/handler/NeatoHandler.java index ba355b733065a..8cd011da807cd 100644 --- a/bundles/org.openhab.binding.neato/src/main/java/org/openhab/binding/neato/internal/handler/NeatoHandler.java +++ b/bundles/org.openhab.binding.neato/src/main/java/org/openhab/binding/neato/internal/handler/NeatoHandler.java @@ -17,7 +17,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang3.ObjectUtils; import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.neato.internal.CouldNotFindRobotException; import org.openhab.binding.neato.internal.NeatoBindingConstants; diff --git a/bundles/org.openhab.binding.neato/src/main/resources/OH-INF/thing/vacuumcleaner.xml b/bundles/org.openhab.binding.neato/src/main/resources/OH-INF/thing/vacuumcleaner.xml index 0649460169657..c937ee4682983 100644 --- a/bundles/org.openhab.binding.neato/src/main/resources/OH-INF/thing/vacuumcleaner.xml +++ b/bundles/org.openhab.binding.neato/src/main/resources/OH-INF/thing/vacuumcleaner.xml @@ -9,16 +9,14 @@ Access to Neato Account. Used to discover robots tied to account. - + E-mail address for your Neato Cloud account. - true email - + Password for your Neato Cloud account. - true password diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java index 3d027ced5f32e..90d08da70d14b 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java @@ -71,7 +71,7 @@ public void resetCache() { * @see https://developers.nest.com/documentation/cloud/how-to-handle-redirects */ private String resolveRedirectUrl() throws FailedResolvingNestUrlException { - HttpClient httpClient = new HttpClient(new SslContextFactory()); + HttpClient httpClient = new HttpClient(new SslContextFactory.Client()); httpClient.setFollowRedirects(false); Request request = httpClient.newRequest(NestBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30, diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml index 21c880e10fbae..1a406a5880ed3 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml @@ -14,15 +14,13 @@ Local settings - + The product ID from the Nest product page - true - + The product secret from the Nest product page - true diff --git a/bundles/org.openhab.binding.netatmo/pom.xml b/bundles/org.openhab.binding.netatmo/pom.xml index 3048a3f8c0cdd..d9ba59a62b736 100644 --- a/bundles/org.openhab.binding.netatmo/pom.xml +++ b/bundles/org.openhab.binding.netatmo/pom.xml @@ -79,6 +79,12 @@ 1.8 compile + + com.google.code.gson + gson + 2.8.5 + compile + diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/ChannelTypeUtils.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/ChannelTypeUtils.java index 16cbaeda2d8d0..909187a1895c2 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/ChannelTypeUtils.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/ChannelTypeUtils.java @@ -13,6 +13,7 @@ package org.openhab.binding.netatmo.internal; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -83,7 +84,7 @@ public static State toDecimalType(double value) { } public static State toDecimalType(@Nullable BigDecimal decimal) { - return decimal == null ? UnDefType.NULL : new DecimalType(decimal.setScale(2, BigDecimal.ROUND_HALF_UP)); + return decimal == null ? UnDefType.NULL : new DecimalType(decimal.setScale(2, RoundingMode.HALF_UP)); } public static State toDecimalType(@Nullable String textualDecimal) { diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/binding/binding.xml index d199b62c84d4c..a7c08125c6472 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/binding/binding.xml @@ -8,11 +8,10 @@ and Welcome Camera. - + If set to true, the device and its associated modules are updated in the discovery inbox at each API call run to refresh device data. Default is false. - false false diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java index 6dc3bfadcb871..444c32fc8fd6e 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java @@ -36,8 +36,7 @@ import java.util.Set; import java.util.stream.Collectors; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.SystemUtils; +import org.apache.commons.lang3.SystemUtils; import org.apache.commons.net.util.SubnetUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -203,7 +202,7 @@ public IpPingMethodEnum determinePingMethod() { */ public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) { String result = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(100), arpToolPath, "--help"); - if (StringUtils.isBlank(result)) { + if (result == null || result.isBlank()) { return ArpPingUtilEnum.UNKNOWN_TOOL; } else if (result.contains("Thomas Habets")) { if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) { diff --git a/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/Arduino/NibeGW/NibeGW.ino b/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/Arduino/NibeGW/NibeGW.ino index 2e9acc6777819..e84609187d392 100644 --- a/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/Arduino/NibeGW/NibeGW.ino +++ b/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/Arduino/NibeGW/NibeGW.ino @@ -30,6 +30,9 @@ //#define PRODINO_BOARD // Enable if ENC28J60 LAN module is used //#define TRANSPORT_ETH_ENC28J60 +// Enable if you use STM32 NUCLEO-F429ZI +//#define STM32_F429ZI_BOARD + // Enable debug printouts, listen printouts e.g. via netcat (nc -l -u 50000) //#define ENABLE_DEBUG @@ -54,6 +57,11 @@ #ifdef PRODINO_BOARD #define RS485_PORT Serial1 #define RS485_DIRECTION_PIN 3 +#elif defined STM32_F429ZI_BOARD +#include +HardwareSerial Serial1(PG9,PG14); +#define RS485_PORT Serial1 +#define RS485_DIRECTION_PIN PF15 #else #define RS485_PORT Serial #define RS485_DIRECTION_PIN 2 @@ -70,6 +78,10 @@ #ifdef TRANSPORT_ETH_ENC28J60 #include +#elif defined STM32_F429ZI_BOARD +#include +#include +#include #else #include #include @@ -81,7 +93,11 @@ #include "KMPCommon.h" #endif +#ifdef STM32_F429ZI_BOARD +#include +#else #include +#endif #include "NibeGw.h" @@ -142,12 +158,24 @@ void debugPrint(char* data) } #endif +// ######### FUNCTION DEFINITION ###################### + +void nibeCallbackMsgReceived(const byte* const data, int len); +int nibeCallbackTokenReceived(eTokenType token, byte* data); +void sendUdpPacket(const byte * const data, int len); +void initializeEthernet(); + + // ######### SETUP ####################### void setup() { // Start watchdog +#ifdef STM32_F429ZI_BOARD + IWatchdog.begin(2000000); // 2 sec +#else wdt_enable (WDTO_2S); +#endif nibegw.setCallback(nibeCallbackMsgReceived, nibeCallbackTokenReceived); nibegw.setAckModbus40Address(ACK_MODBUS40); @@ -174,7 +202,11 @@ void setup() void loop() { +#ifdef STM32_F429ZI_BOARD + IWatchdog.reload(); +#else wdt_reset(); +#endif long now = millis() / 1000; @@ -227,6 +259,7 @@ void initializeEthernet() { Ethernet.begin(mac, ip, gw, mask); ethernetInitialized = true; + udp.begin(TARGET_PORT); udp4readCmnds.begin(INCOMING_PORT_READCMDS); udp4writeCmnds.begin(INCOMING_PORT_WRITECMDS); } diff --git a/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/RasPi/nibegw.c b/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/RasPi/nibegw.c index 0b7e798dd4b98..5aff2ca2c042e 100644 --- a/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/RasPi/nibegw.c +++ b/bundles/org.openhab.binding.nibeheatpump/contrib/NibeGW/RasPi/nibegw.c @@ -53,8 +53,9 @@ * 3.6.2014 v1.04 * 4.6.2014 v1.10 More options. * 10.9.2014 v1.20 Bidirectional support. - * 30.6.2015 v1.21 Some fixes. - * 20.2.2017 v1.22 Separated read and write token support. + * 30.6.2015 v1.21 Some fixes. + * 20.2.2017 v1.22 Separated read and write token support. + * 7.2.2021 v1.23 Fixed compile error in RasPi. */ #include @@ -71,8 +72,9 @@ #include #include #include +#include -#define VERSION "1.22" +#define VERSION "1.23" #define FALSE 0 #define TRUE 1 @@ -199,8 +201,8 @@ char* getTimeStamp(char* buffer) tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, - tm->tm_min, tm->tm_sec, tv.tv_usec - ); + tm->tm_min, tm->tm_sec, tv.tv_usec + ); return buffer; } diff --git a/bundles/org.openhab.binding.nibeheatpump/src/main/java/org/openhab/binding/nibeheatpump/internal/protocol/NibeHeatPumpProtocol.java b/bundles/org.openhab.binding.nibeheatpump/src/main/java/org/openhab/binding/nibeheatpump/internal/protocol/NibeHeatPumpProtocol.java index 8c81168194626..0ef1c81010172 100644 --- a/bundles/org.openhab.binding.nibeheatpump/src/main/java/org/openhab/binding/nibeheatpump/internal/protocol/NibeHeatPumpProtocol.java +++ b/bundles/org.openhab.binding.nibeheatpump/src/main/java/org/openhab/binding/nibeheatpump/internal/protocol/NibeHeatPumpProtocol.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.nibeheatpump.internal.protocol; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.openhab.binding.nibeheatpump.internal.NibeHeatPumpException; /** diff --git a/bundles/org.openhab.binding.nibeuplink/src/main/java/org/openhab/binding/nibeuplink/internal/config/NibeUplinkConfiguration.java b/bundles/org.openhab.binding.nibeuplink/src/main/java/org/openhab/binding/nibeuplink/internal/config/NibeUplinkConfiguration.java index d444245a255ba..2c8e2f253ff6a 100644 --- a/bundles/org.openhab.binding.nibeuplink/src/main/java/org/openhab/binding/nibeuplink/internal/config/NibeUplinkConfiguration.java +++ b/bundles/org.openhab.binding.nibeuplink/src/main/java/org/openhab/binding/nibeuplink/internal/config/NibeUplinkConfiguration.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.nibeuplink.internal.config; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlBridgeDiscoveryService.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlBridgeDiscoveryService.java index 50b0e1b73fedc..ffe67643dbbae 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlBridgeDiscoveryService.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlBridgeDiscoveryService.java @@ -55,7 +55,7 @@ public class NikoHomeControlBridgeDiscoveryService extends AbstractDiscoveryServ public NikoHomeControlBridgeDiscoveryService() { super(NikoHomeControlBindingConstants.BRIDGE_THING_TYPES_UIDS, TIMEOUT); - logger.debug("Niko Home Control: bridge discovery service started"); + logger.debug("bridge discovery service started"); } /** @@ -65,10 +65,10 @@ private void discoverBridge() { try { String broadcastAddr = networkAddressService.getConfiguredBroadcastAddress(); if (broadcastAddr == null) { - logger.warn("Niko Home Control: discovery not possible, no broadcast address found"); + logger.warn("discovery not possible, no broadcast address found"); return; } - logger.debug("Niko Home Control: discovery broadcast on {}", broadcastAddr); + logger.debug("discovery broadcast on {}", broadcastAddr); NikoHomeControlDiscover nhcDiscover = new NikoHomeControlDiscover(broadcastAddr); if (nhcDiscover.isNhcII()) { addNhcIIBridge(nhcDiscover.getAddr(), nhcDiscover.getNhcBridgeId()); @@ -76,12 +76,12 @@ private void discoverBridge() { addNhcIBridge(nhcDiscover.getAddr(), nhcDiscover.getNhcBridgeId()); } } catch (IOException e) { - logger.debug("Niko Home Control: no bridge found."); + logger.debug("no bridge found."); } } private void addNhcIBridge(InetAddress addr, String bridgeId) { - logger.debug("Niko Home Control: NHC I bridge found at {}", addr); + logger.debug("NHC I bridge found at {}", addr); String bridgeName = "Niko Home Control Bridge"; ThingUID uid = new ThingUID(BINDING_ID, "bridge", bridgeId); @@ -92,7 +92,7 @@ private void addNhcIBridge(InetAddress addr, String bridgeId) { } private void addNhcIIBridge(InetAddress addr, String bridgeId) { - logger.debug("Niko Home Control: NHC II bridge found at {}", addr); + logger.debug("NHC II bridge found at {}", addr); String bridgeName = "Niko Home Control II Bridge"; ThingUID uid = new ThingUID(BINDING_ID, "bridge2", bridgeId); @@ -115,7 +115,7 @@ protected synchronized void stopScan() { @Override protected void startBackgroundDiscovery() { - logger.debug("Niko Home Control: Start background bridge discovery"); + logger.debug("Start background bridge discovery"); ScheduledFuture job = nhcDiscoveryJob; if (job == null || job.isCancelled()) { nhcDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discoverBridge, 0, REFRESH_INTERVAL, @@ -125,7 +125,7 @@ protected void startBackgroundDiscovery() { @Override protected void stopBackgroundDiscovery() { - logger.debug("Niko Home Control: Stop bridge background discovery"); + logger.debug("Stop bridge background discovery"); ScheduledFuture job = nhcDiscoveryJob; if (job != null && !job.isCancelled()) { job.cancel(true); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java index 838b49f2d4920..63e847068dc5d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java @@ -48,7 +48,7 @@ public class NikoHomeControlDiscoveryService extends AbstractDiscoveryService { public NikoHomeControlDiscoveryService(NikoHomeControlBridgeHandler handler) { super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT, false); - logger.debug("Niko Home Control: discovery service {}", handler); + logger.debug("discovery service {}", handler); bridgeUID = handler.getThing().getUID(); this.handler = handler; } @@ -70,10 +70,10 @@ public void discoverDevices() { NikoHomeControlCommunication nhcComm = handler.getCommunication(); if ((nhcComm == null) || !nhcComm.communicationActive()) { - logger.warn("Niko Home Control: not connected."); + logger.warn("not connected"); return; } - logger.debug("Niko Home Control: getting devices on {}", handler.getThing().getUID().getId()); + logger.debug("getting devices on {}", handler.getThing().getUID().getId()); Map actions = nhcComm.getActions(); @@ -99,8 +99,7 @@ public void discoverDevices() { thingName, thingLocation); break; default: - logger.debug("Niko Home Control: unrecognized action type {} for {} {}", nhcAction.getType(), - actionId, thingName); + logger.debug("unrecognized action type {} for {} {}", nhcAction.getType(), actionId, thingName); } }); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NhcJwtToken2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NhcJwtToken2.java index abab7e2036730..38ebff218e410 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NhcJwtToken2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NhcJwtToken2.java @@ -14,17 +14,20 @@ import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NhcJwtToken2} represents the Niko Home Control II hobby API token payload. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault class NhcJwtToken2 { - String sub; - String iat; - String exp; - String aud; - String iss; - String jti; - List role; + String sub = ""; + String iat = ""; + String exp = ""; + String aud = ""; + String iss = ""; + String jti = ""; + List role = List.of(); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionBlindConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionBlindConfig.java new file mode 100644 index 0000000000000..6423c31b030fd --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionBlindConfig.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.nikohomecontrol.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link NikoHomeControlActionBlindConfig} is the config class for Niko Home Control Blind Actions. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class NikoHomeControlActionBlindConfig extends NikoHomeControlActionConfig { + public boolean invert; +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionConfig.java index 2cd963c169055..39c0a3a74d344 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionConfig.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlActionConfig} is the general config class for Niko Home Control Actions. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlActionConfig { - public String actionId; + public String actionId = ""; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionDimmerConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionDimmerConfig.java index 2a5d52f32633c..712c09b42c30c 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionDimmerConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionDimmerConfig.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlActionDimmerConfig} is the config class for Niko Home Control Dimmer Actions. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlActionDimmerConfig extends NikoHomeControlActionConfig { - public int step; + public int step = 10; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionHandler.java index 4b39852d2ba5d..3cd76e9da2d16 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlActionHandler.java @@ -52,10 +52,11 @@ public class NikoHomeControlActionHandler extends BaseThingHandler implements Nh private final Logger logger = LoggerFactory.getLogger(NikoHomeControlActionHandler.class); - private volatile @NonNullByDefault({}) NhcAction nhcAction; + private volatile @Nullable NhcAction nhcAction; private String actionId = ""; private int stepValue; + private boolean invert; public NikoHomeControlActionHandler(Thing thing) { super(thing); @@ -66,8 +67,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: bridge communication not initialized when trying to execute action command " - + actionId); + "Bridge communication not initialized when trying to execute action command " + actionId); return; } @@ -84,7 +84,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { } private void handleCommandSelection(ChannelUID channelUID, Command command) { - logger.debug("Niko Home Control: handle command {} for {}", command, channelUID); + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + + logger.debug("handle command {} for {}", command, channelUID); if (command == REFRESH) { actionEvent(nhcAction.getState()); @@ -109,11 +115,17 @@ private void handleCommandSelection(ChannelUID channelUID, Command command) { default: updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: channel unknown " + channelUID.getId()); + "Channel unknown " + channelUID.getId()); } } private void handleSwitchCommand(Command command) { + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (OnOffType.OFF.equals(s)) { @@ -125,6 +137,12 @@ private void handleSwitchCommand(Command command) { } private void handleBrightnessCommand(Command command) { + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (OnOffType.OFF.equals(s)) { @@ -162,18 +180,24 @@ private void handleBrightnessCommand(Command command) { } private void handleRollershutterCommand(Command command) { + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + if (command instanceof UpDownType) { UpDownType s = (UpDownType) command; if (UpDownType.UP.equals(s)) { - nhcAction.execute(NHCUP); + nhcAction.execute(!invert ? NHCUP : NHCDOWN); } else { - nhcAction.execute(NHCDOWN); + nhcAction.execute(!invert ? NHCDOWN : NHCUP); } } else if (command instanceof StopMoveType) { nhcAction.execute(NHCSTOP); } else if (command instanceof PercentType) { PercentType p = (PercentType) command; - nhcAction.execute(Integer.toString(100 - p.intValue())); + nhcAction.execute(!invert ? Integer.toString(100 - p.intValue()) : Integer.toString(p.intValue())); } } @@ -183,6 +207,9 @@ public void initialize() { if (thing.getThingTypeUID().equals(THING_TYPE_DIMMABLE_LIGHT)) { config = getConfig().as(NikoHomeControlActionDimmerConfig.class); stepValue = ((NikoHomeControlActionDimmerConfig) config).step; + } else if (thing.getThingTypeUID().equals(THING_TYPE_BLIND)) { + config = getConfig().as(NikoHomeControlActionBlindConfig.class); + invert = ((NikoHomeControlActionBlindConfig) config).invert; } else { config = getConfig().as(NikoHomeControlActionConfig.class); } @@ -190,6 +217,8 @@ public void initialize() { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Connection with controller not started yet, could not initialize action " + actionId); return; } @@ -197,15 +226,14 @@ public void initialize() { scheduler.submit(() -> { if (!nhcComm.communicationActive()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: no connection with Niko Home Control, could not initialize action " - + actionId); + "No connection with controller, could not initialize action " + actionId); return; } - nhcAction = nhcComm.getActions().get(actionId); + NhcAction nhcAction = nhcComm.getActions().get(actionId); if (nhcAction == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: actionId does not match an action in the controller " + actionId); + "Action " + actionId + " does not match an action in the controller"); return; } @@ -220,7 +248,9 @@ public void initialize() { actionEvent(nhcAction.getState()); - logger.debug("Niko Home Control: action initialized {}", actionId); + this.nhcAction = nhcAction; + + logger.debug("action initialized {}", actionId); Bridge bridge = getBridge(); if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { @@ -232,6 +262,12 @@ public void initialize() { } private void updateProperties() { + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + Map properties = new HashMap<>(); properties.put("type", String.valueOf(nhcAction.getType())); if (getThing().getThingTypeUID() == THING_TYPE_BLIND) { @@ -250,6 +286,12 @@ private void updateProperties() { @Override public void actionEvent(int actionState) { + NhcAction nhcAction = this.nhcAction; + if (nhcAction == null) { + logger.debug("action with ID {} not initialized", actionId); + return; + } + ActionType actionType = nhcAction.getType(); switch (actionType) { @@ -265,19 +307,28 @@ public void actionEvent(int actionState) { updateStatus(ThingStatus.ONLINE); break; case ROLLERSHUTTER: - updateState(CHANNEL_ROLLERSHUTTER, new PercentType(actionState)); + updateState(CHANNEL_ROLLERSHUTTER, + !invert ? new PercentType(100 - actionState) : new PercentType(actionState)); updateStatus(ThingStatus.ONLINE); break; default: updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: unknown action type " + actionType); + "Unknown action type " + actionType); + } + } + + @Override + public void actionInitialized() { + Bridge bridge = getBridge(); + if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); } } @Override public void actionRemoved() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: action has been removed from the controller " + actionId); + "Action " + actionId + " has been removed from the controller"); } private void restartCommunication(NikoHomeControlCommunication nhcComm) { @@ -286,8 +337,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { nhcComm.restartCommunication(); // If still not active, take thing offline and return. if (!nhcComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: communication socket error"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error"); return; } // Also put the bridge back online @@ -301,7 +351,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler(); if (nhcBridgeHandler == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for action " + actionId); + "No bridge initialized for action " + actionId); return null; } NikoHomeControlCommunication nhcComm = nhcBridgeHandler.getCommunication(); @@ -312,7 +362,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { Bridge nhcBridge = getBridge(); if (nhcBridge == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for action " + actionId); + "No bridge initialized for action " + actionId); return null; } NikoHomeControlBridgeHandler nhcBridgeHandler = (NikoHomeControlBridgeHandler) nhcBridge.getHandler(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig.java index 614cd0ac4ec41..979ad3d8d421d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig.java @@ -12,13 +12,16 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlBridgeConfig} is the general config class for Niko Home Control Bridges. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlBridgeConfig { - public String addr; + public String addr = ""; public int port; public int refresh; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig2.java index 5d1d3eb17c193..384d869ef4181 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeConfig2.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlBridgeConfig2} is the extended config class for Niko Home Control II Bridges. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlBridgeConfig2 extends NikoHomeControlBridgeConfig { - public String profile; - public String password; + public String profile = ""; + public String password = ""; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler.java index f120e93d7eb6f..b7e4472ff5071 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler.java @@ -95,7 +95,7 @@ protected void startCommunication() { if (discovery != null) { discovery.discoverDevices(); } else { - logger.debug("Niko Home Control: cannot discover devices, discovery service not started"); + logger.debug("cannot discover devices, discovery service not started"); } }); } @@ -117,9 +117,9 @@ private void setupRefreshTimer(int refreshInterval) { } // This timer will restart the bridge connection periodically - logger.debug("Niko Home Control: restart bridge connection every {} min", refreshInterval); + logger.debug("restart bridge connection every {} min", refreshInterval); refreshTimer = scheduler.scheduleWithFixedDelay(() -> { - logger.debug("Niko Home Control: restart communication at scheduled time"); + logger.debug("restart communication at scheduled time"); NikoHomeControlCommunication comm = nhcComm; if (comm != null) { @@ -141,7 +141,7 @@ private void setupRefreshTimer(int refreshInterval) { */ protected void bridgeOffline() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Niko Home Control: error starting bridge connection"); + "Error with bridge connection"); } /** @@ -153,8 +153,8 @@ public void bridgeOnline() { } @Override - public void controllerOffline() { - bridgeOffline(); + public void controllerOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); } @Override @@ -231,14 +231,14 @@ public void setNhcDiscovery(@Nullable NikoHomeControlDiscoveryService nhcDiscove @Override public void alarmEvent(String alarmText) { - logger.debug("Niko Home Control: triggering alarm channel with {}", alarmText); + logger.debug("triggering alarm channel with {}", alarmText); triggerChannel(CHANNEL_ALARM, alarmText); updateStatus(ThingStatus.ONLINE); } @Override public void noticeEvent(String alarmText) { - logger.debug("Niko Home Control: triggering notice channel with {}", alarmText); + logger.debug("triggering notice channel with {}", alarmText); triggerChannel(CHANNEL_NOTICE, alarmText); updateStatus(ThingStatus.ONLINE); } @@ -263,7 +263,7 @@ public void updatePropertiesEvent() { try { addr = InetAddress.getByName(config.addr); } catch (UnknownHostException e) { - logger.debug("Niko Home Control: Cannot resolve hostname {} to IP adress", config.addr); + logger.debug("Cannot resolve hostname {} to IP adress", config.addr); } return addr; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler1.java index 5f6408db7f317..e0938e1a2cdb1 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler1.java @@ -41,20 +41,20 @@ public NikoHomeControlBridgeHandler1(Bridge nikoHomeControlBridge) { @Override public void initialize() { - logger.debug("Niko Home Control: initializing bridge handler"); + logger.debug("initializing bridge handler"); setConfig(); InetAddress addr = getAddr(); int port = getPort(); - logger.debug("Niko Home Control: bridge handler host {}, port {}", addr, port); + logger.debug("bridge handler host {}, port {}", addr, port); if (addr != null) { nhcComm = new NikoHomeControlCommunication1(this, scheduler); startCommunication(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Niko Home Control: cannot resolve bridge IP with hostname " + config.addr); + "Cannot resolve bridge IP with hostname " + config.addr); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java index 668d7f33a789d..e6687926e96a2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java @@ -57,7 +57,7 @@ public NikoHomeControlBridgeHandler2(Bridge nikoHomeControlBridge, NetworkAddres @Override public void initialize() { - logger.debug("Niko Home Control: initializing NHC II bridge handler"); + logger.debug("initializing NHC II bridge handler"); setConfig(); @@ -69,15 +69,14 @@ public void initialize() { // advanced configuration, skipping token validation. // This behavior would allow the same logic to be used (with profile UUID) as before token validation // was introduced. - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Niko Home Control: token is empty"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Token is empty"); return; } } else { Date now = new Date(); if (expiryDate.before(now)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Niko Home Control: hobby api token has expired"); + "Hobby api token has expired"); return; } } @@ -91,7 +90,7 @@ public void initialize() { } catch (CertificateException e) { // this should not happen unless there is a programming error updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Niko Home Control: not able to set SSL context"); + "Not able to set SSL context"); return; } } @@ -167,9 +166,8 @@ public String getProfile() { @Override public String getToken() { String token = ((NikoHomeControlBridgeConfig2) config).password; - if ((token == null) || token.isEmpty()) { - logger.debug("Niko Home Control: no JWT token set."); - return ""; + if (token.isEmpty()) { + logger.debug("no JWT token set."); } return token; } @@ -192,10 +190,10 @@ public String getToken() { try { jwtToken = gson.fromJson(tokenPayload, NhcJwtToken2.class); } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected token payload {}", tokenPayload); + logger.debug("unexpected token payload {}", tokenPayload); } catch (NoSuchElementException ignore) { // Ignore if exp not present in response, this should not happen in token payload response - logger.trace("Niko Home Control: no expiry date found in payload {}", tokenPayload); + logger.trace("no expiry date found in payload {}", tokenPayload); } } @@ -206,20 +204,20 @@ public String getToken() { long epoch = Long.parseLong(expiryEpoch) * 1000; // convert to milliseconds expiryDate = new Date(epoch); } catch (NumberFormatException e) { - logger.debug("Niko Home Control: token expiry not valid {}", jwtToken.exp); + logger.debug("token expiry not valid {}", jwtToken.exp); return null; } Date now = new Date(); if (expiryDate.before(now)) { - logger.warn("Niko Home Control: hobby API token expired, was valid until {}", + logger.warn("hobby API token expired, was valid until {}", DateFormat.getDateInstance().format(expiryDate)); } else { Calendar c = Calendar.getInstance(); c.setTime(expiryDate); c.add(Calendar.DATE, -14); if (c.getTime().before(now)) { - logger.info("Niko Home Control: hobby API token will expire in less than 14 days, valid until {}", + logger.info("hobby API token will expire in less than 14 days, valid until {}", DateFormat.getDateInstance().format(expiryDate)); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterConfig.java index dd61f04278771..2af74b26ca2a9 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterConfig.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlEnergyMeterConfig} is the config class for Niko Home Control Thermostats. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlEnergyMeterConfig { - public String energyMeterId; + public String energyMeterId = ""; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterHandler.java index 7f22acc7907fa..637f065f5af01 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlEnergyMeterHandler.java @@ -48,7 +48,7 @@ public class NikoHomeControlEnergyMeterHandler extends BaseThingHandler implemen private final Logger logger = LoggerFactory.getLogger(NikoHomeControlEnergyMeterHandler.class); - private volatile @NonNullByDefault({}) NhcEnergyMeter nhcEnergyMeter; + private volatile @Nullable NhcEnergyMeter nhcEnergyMeter; private String energyMeterId = ""; @@ -58,6 +58,12 @@ public NikoHomeControlEnergyMeterHandler(Thing thing) { @Override public void handleCommand(ChannelUID channelUID, Command command) { + NhcEnergyMeter nhcEnergyMeter = this.nhcEnergyMeter; + if (nhcEnergyMeter == null) { + logger.debug("energy meter with ID {} not initialized", energyMeterId); + return; + } + if (command == REFRESH) { energyMeterEvent(nhcEnergyMeter.getPower()); } @@ -71,6 +77,8 @@ public void initialize() { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Connection with controller not started yet, could not initialize energy meter " + energyMeterId); return; } @@ -79,16 +87,14 @@ public void initialize() { scheduler.submit(() -> { if (!nhcComm.communicationActive()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: no connection with Niko Home Control, could not initialize energy meter " - + energyMeterId); + "No connection with controller, could not initialize energy meter " + energyMeterId); return; } - nhcEnergyMeter = nhcComm.getEnergyMeters().get(energyMeterId); + NhcEnergyMeter nhcEnergyMeter = nhcComm.getEnergyMeters().get(energyMeterId); if (nhcEnergyMeter == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: energyMeterId does not match a energy meter in the controller " - + energyMeterId); + "Energy meter " + energyMeterId + " does not match a energy meter in the controller"); return; } @@ -102,7 +108,9 @@ public void initialize() { nhcComm.startEnergyMeter(energyMeterId); } - logger.debug("Niko Home Control: energy meter intialized {}", energyMeterId); + this.nhcEnergyMeter = nhcEnergyMeter; + + logger.debug("energy meter intialized {}", energyMeterId); Bridge bridge = getBridge(); if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { @@ -144,10 +152,18 @@ public void energyMeterEvent(@Nullable Integer power) { updateStatus(ThingStatus.ONLINE); } + @Override + public void energyMeterInitialized() { + Bridge bridge = getBridge(); + if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); + } + } + @Override public void energyMeterRemoved() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: energy meter has been removed from the controller " + energyMeterId); + "Energy meter " + energyMeterId + " has been removed from the controller"); } @Override @@ -157,8 +173,7 @@ public void channelLinked(ChannelUID channelUID) { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: bridge communication not initialized when trying to start energy meter " - + energyMeterId); + "Bridge communication not initialized when trying to start energy meter " + energyMeterId); return; } @@ -180,8 +195,7 @@ public void channelUnlinked(ChannelUID channelUID) { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: bridge communication not initialized when trying to stop energy meter " - + energyMeterId); + "Bridge communication not initialized when trying to stop energy meter " + energyMeterId); return; } @@ -206,8 +220,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { nhcComm.restartCommunication(); // If still not active, take thing offline and return. if (!nhcComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: communication socket error"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error"); return; } // Also put the bridge back online @@ -221,7 +234,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler(); if (nhcBridgeHandler == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for energy meter " + energyMeterId); + "No bridge initialized for energy meter " + energyMeterId); return null; } NikoHomeControlCommunication nhcComm = nhcBridgeHandler.getCommunication(); @@ -232,7 +245,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { Bridge nhcBridge = getBridge(); if (nhcBridge == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for energy meter " + energyMeterId); + "No bridge initialized for energy meter " + energyMeterId); return null; } NikoHomeControlBridgeHandler nhcBridgeHandler = (NikoHomeControlBridgeHandler) nhcBridge.getHandler(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatConfig.java index d6cf3a453b92f..725a8e0ed9136 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatConfig.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.nikohomecontrol.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * {@link NikoHomeControlThermostatConfig} is the config class for Niko Home Control Thermostats. * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault public class NikoHomeControlThermostatConfig { - public String thermostatId; - public int overruleTime; + public String thermostatId = ""; + public int overruleTime = 60; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatHandler.java index 12da67d0e763b..d852f07ade143 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlThermostatHandler.java @@ -52,7 +52,7 @@ public class NikoHomeControlThermostatHandler extends BaseThingHandler implement private final Logger logger = LoggerFactory.getLogger(NikoHomeControlThermostatHandler.class); - private volatile @NonNullByDefault({}) NhcThermostat nhcThermostat; + private volatile @Nullable NhcThermostat nhcThermostat; private String thermostatId = ""; private int overruleTime; @@ -69,7 +69,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: bridge communication not initialized when trying to execute thermostat command " + "Bridge communication not initialized when trying to execute thermostat command on " + thermostatId); return; } @@ -87,7 +87,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { } private void handleCommandSelection(ChannelUID channelUID, Command command) { - logger.debug("Niko Home Control: handle command {} for {}", command, channelUID); + NhcThermostat nhcThermostat = this.nhcThermostat; + if (nhcThermostat == null) { + logger.debug("thermostat with ID {} not initialized", thermostatId); + return; + } + + logger.debug("handle command {} for {}", command, channelUID); if (REFRESH.equals(command)) { thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(), @@ -140,7 +146,7 @@ private void handleCommandSelection(ChannelUID channelUID, Command command) { default: updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: channel unknown " + channelUID.getId()); + "Channel unknown " + channelUID.getId()); } } @@ -153,6 +159,8 @@ public void initialize() { NikoHomeControlCommunication nhcComm = getCommunication(); if (nhcComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Connection with controller not started yet, could not initialize thermostat " + thermostatId); return; } @@ -161,16 +169,14 @@ public void initialize() { scheduler.submit(() -> { if (!nhcComm.communicationActive()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: no connection with Niko Home Control, could not initialize thermostat " - + thermostatId); + "No connection with controller, could not initialize thermostat " + thermostatId); return; } - nhcThermostat = nhcComm.getThermostats().get(thermostatId); + NhcThermostat nhcThermostat = nhcComm.getThermostats().get(thermostatId); if (nhcThermostat == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: thermostatId does not match a thermostat in the controller " - + thermostatId); + "Thermostat " + thermostatId + " does not match a thermostat in the controller"); return; } @@ -186,7 +192,9 @@ public void initialize() { thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(), nhcThermostat.getOverrule(), nhcThermostat.getDemand()); - logger.debug("Niko Home Control: thermostat intialized {}", thermostatId); + this.nhcThermostat = nhcThermostat; + + logger.debug("thermostat intialized {}", thermostatId); Bridge bridge = getBridge(); if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { @@ -211,6 +219,12 @@ private void updateProperties() { @Override public void thermostatEvent(int measured, int setpoint, int mode, int overrule, int demand) { + NhcThermostat nhcThermostat = this.nhcThermostat; + if (nhcThermostat == null) { + logger.debug("thermostat with ID {} not initialized", thermostatId); + return; + } + updateState(CHANNEL_MEASURED, new QuantityType<>(nhcThermostat.getMeasured() / 10.0, CELSIUS)); int overruletime = nhcThermostat.getRemainingOverruletime(); @@ -263,10 +277,18 @@ private void cancelRefreshTimer() { refreshTimer = null; } + @Override + public void thermostatInitialized() { + Bridge bridge = getBridge(); + if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); + } + } + @Override public void thermostatRemoved() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Niko Home Control: thermostat has been removed from the controller " + thermostatId); + "Thermostat " + thermostatId + " has been removed from the controller"); } private void restartCommunication(NikoHomeControlCommunication nhcComm) { @@ -275,8 +297,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { nhcComm.restartCommunication(); // If still not active, take thing offline and return. if (!nhcComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Niko Home Control: communication socket error"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error"); return; } // Also put the bridge back online @@ -290,7 +311,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler(); if (nhcBridgeHandler == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for thermostat " + thermostatId); + "No bridge initialized for thermostat " + thermostatId); return null; } NikoHomeControlCommunication nhcComm = nhcBridgeHandler.getCommunication(); @@ -301,7 +322,7 @@ private void restartCommunication(NikoHomeControlCommunication nhcComm) { Bridge nhcBridge = getBridge(); if (nhcBridge == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Niko Home Control: no bridge initialized for thermostat " + thermostatId); + "No bridge initialized for thermostat " + thermostatId); return null; } NikoHomeControlBridgeHandler nhcBridgeHandler = (NikoHomeControlBridgeHandler) nhcBridge.getHandler(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcAction.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcAction.java index 8425d7588fe49..dbe5d5a3471f8 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcAction.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcAction.java @@ -158,7 +158,7 @@ protected void updateState() { protected void updateState(int state) { NhcActionEvent eventHandler = this.eventHandler; if (eventHandler != null) { - logger.debug("Niko Home Control: update channel state for {} with {}", id, state); + logger.debug("update channel state for {} with {}", id, state); eventHandler.actionEvent(state); } } @@ -167,7 +167,7 @@ protected void updateState(int state) { * Method called when action is removed from the Niko Home Control Controller. */ public void actionRemoved() { - logger.warn("Niko Home Control: action removed {}, {}", id, name); + logger.debug("action removed {}, {}", id, name); NhcActionEvent eventHandler = this.eventHandler; if (eventHandler != null) { eventHandler.actionRemoved(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcActionEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcActionEvent.java index 4f8495c8e3234..a58b1f422e16a 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcActionEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcActionEvent.java @@ -32,6 +32,12 @@ public interface NhcActionEvent { */ public void actionEvent(int state); + /** + * Called to indicate the action has been initialized. + * + */ + public void actionInitialized(); + /** * Called to indicate the action has been removed from the Niko Home Control controller. * diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcControllerEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcControllerEvent.java index a3eea885a1c0d..ead01aeddaf45 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcControllerEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcControllerEvent.java @@ -67,8 +67,9 @@ public default String getToken() { /** * Called to indicate the connection with the Niko Home Control Controller is offline. * + * @param message */ - public void controllerOffline(); + public void controllerOffline(String message); /** * Called to indicate the connection with the Niko Home Control Controller is online. diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeter.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeter.java index c37daa8741636..c6dad086b0896 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeter.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeter.java @@ -56,7 +56,7 @@ protected NhcEnergyMeter(String id, String name, NikoHomeControlCommunication nh public void updateState(int power) { NhcEnergyMeterEvent handler = eventHandler; if (handler != null) { - logger.debug("Niko Home Control: update channel for {}", id); + logger.debug("update channel for {}", id); handler.energyMeterEvent(power); } } @@ -65,7 +65,7 @@ public void updateState(int power) { * Method called when energyMeters meter is removed from the Niko Home Control Controller. */ public void energyMeterRemoved() { - logger.warn("Niko Home Control: action removed {}, {}", id, name); + logger.debug("action removed {}, {}", id, name); NhcEnergyMeterEvent eventHandler = this.eventHandler; if (eventHandler != null) { eventHandler.energyMeterRemoved(); @@ -117,7 +117,7 @@ public void setPower(@Nullable Integer power) { this.power = power; NhcEnergyMeterEvent handler = eventHandler; if (handler != null) { - logger.debug("Niko Home Control: update power channel for {} with {}", id, power); + logger.debug("update power channel for {} with {}", id, power); handler.energyMeterEvent(power); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeterEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeterEvent.java index 8beae434f1a9c..f7dee8b807273 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeterEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcEnergyMeterEvent.java @@ -28,14 +28,20 @@ public interface NhcEnergyMeterEvent { /** - * This method is called when an energyMeters meter event is received from the Niko Home Control controller. + * This method is called when an energyMeter event is received from the Niko Home Control controller. * * @param power current power consumption/production in W (positive for consumption), null for an empty reading */ public void energyMeterEvent(@Nullable Integer power); /** - * Called to indicate the energyMeters meter has been removed from the Niko Home Control controller. + * Called to indicate the energyMeter has been initialized. + * + */ + public void energyMeterInitialized(); + + /** + * Called to indicate the energyMeter has been removed from the Niko Home Control controller. * */ public void energyMeterRemoved(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostat.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostat.java index c25124f8895ea..0d4d5a866f6ac 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostat.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostat.java @@ -115,7 +115,7 @@ public void updateState(int mode) { * Method called when thermostat is removed from the Niko Home Control Controller. */ public void thermostatRemoved() { - logger.warn("Niko Home Control: action removed {}, {}", id, name); + logger.debug("action removed {}, {}", id, name); NhcThermostatEvent eventHandler = this.eventHandler; if (eventHandler != null) { eventHandler.thermostatRemoved(); @@ -125,7 +125,7 @@ public void thermostatRemoved() { private void updateChannels() { NhcThermostatEvent handler = eventHandler; if (handler != null) { - logger.debug("Niko Home Control: update channels for {}", id); + logger.debug("update channels for {}", id); handler.thermostatEvent(measured, setpoint, mode, overrule, demand); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostatEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostatEvent.java index 6eb1fa5e69460..7bdf1c803d1cc 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostatEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcThermostatEvent.java @@ -37,6 +37,12 @@ public interface NhcThermostatEvent { */ public void thermostatEvent(int measured, int setpoint, int mode, int overrule, int demand); + /** + * Called to indicate the thermostat has been initialized. + * + */ + public void thermostatInitialized(); + /** * Called to indicate the thermostat has been removed from the Niko Home Control controller. * diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java index e2a4f5a221f35..853e9c9cb4f17 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java @@ -65,7 +65,7 @@ protected NikoHomeControlCommunication(NhcControllerEvent handler) { public synchronized void restartCommunication() { stopCommunication(); - logger.debug("Niko Home Control: restart communication from thread {}", Thread.currentThread().getId()); + logger.debug("restart communication from thread {}", Thread.currentThread().getId()); startCommunication(); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlDiscover.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlDiscover.java index 29de7bae862a4..708e399929f54 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlDiscover.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlDiscover.java @@ -70,7 +70,7 @@ public NikoHomeControlDiscover(String broadcast) throws IOException { datagramSocket.send(discoveryPacket); while (true) { datagramSocket.receive(packet); - logger.trace("Niko Home Control: bridge discovery response {}", + logger.trace("bridge discovery response {}", HexUtils.bytesToHex(Arrays.copyOf(packet.getData(), packet.getLength()))); if (isNhc(packet)) { break; @@ -79,7 +79,7 @@ public NikoHomeControlDiscover(String broadcast) throws IOException { addr = packet.getAddress(); setNhcBridgeId(packet); setIsNhcII(packet); - logger.debug("Niko Home Control: IP address is {}, unique ID is {}", addr, nhcBridgeId); + logger.debug("IP address is {}, unique ID is {}", addr, nhcBridgeId); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcAction1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcAction1.java index a88c23c47f1b8..784f5d79fc654 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcAction1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcAction1.java @@ -74,7 +74,7 @@ public void setState(int newState) { if (getType() == ActionType.ROLLERSHUTTER) { if (filterEvent) { filterEvent = false; - logger.debug("Niko Home Control: filtered event {} for {}", newState, id); + logger.debug("filtered event {} for {}", newState, id); return; } @@ -88,7 +88,7 @@ public void setState(int newState) { } } if (waitForEvent) { - logger.debug("Niko Home Control: received requested rollershutter {} position event {}", id, newState); + logger.debug("received requested rollershutter {} position event {}", id, newState); executeRollershutterTask(); } else { state = newState; @@ -103,7 +103,7 @@ public void setState(int newState) { */ @Override public void execute(String command) { - logger.debug("Niko Home Control: execute action {} of type {} for {}", command, type, id); + logger.debug("execute action {} of type {} for {}", command, type, id); String value = ""; switch (getType()) { @@ -162,7 +162,7 @@ private void executeRollershutter(String command) { } else if (command.equals(NHCSTOP)) { executeRollershutterStop(); } else { - int newValue = 100 - Integer.parseInt(command); + int newValue = Integer.parseInt(command); if (logger.isTraceEnabled()) { logger.trace("handleRollerShutterCommand: rollershutter {} percent command, current {}, new {}", id, currentValue, newValue); @@ -174,9 +174,9 @@ private void executeRollershutter(String command) { scheduleRollershutterStop(currentValue, newValue); } if (newValue < currentValue) { - executeRollershutterDown(); - } else if (newValue > currentValue) { executeRollershutterUp(); + } else if (newValue > currentValue) { + executeRollershutterDown(); } } }; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageBase1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageBase1.java index a80ae058a060a..8ae35740f30e7 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageBase1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageBase1.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol.nhc1; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link NhcMessageBase1} used as base class for output from gson for cmd or event feedback from Niko Home * Control. This class only contains the common base fields required for the deserializer @@ -21,10 +23,11 @@ * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault abstract class NhcMessageBase1 { - private String cmd; - private String event; + private String cmd = ""; + private String event = ""; String getCmd() { return cmd; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageCmd1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageCmd1.java index d9700913bd9c4..c0bac1dff6d7f 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageCmd1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageCmd1.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol.nhc1; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link NhcMessageCmd1} used as input to gson to send commands to Niko Home Control. Extends * {@link NhcMessageBase1}. @@ -21,6 +23,7 @@ * @author Mark Herwege - Initial Contribution */ @SuppressWarnings("unused") +@NonNullByDefault class NhcMessageCmd1 extends NhcMessageBase1 { private int id; @@ -29,7 +32,7 @@ class NhcMessageCmd1 extends NhcMessageBase1 { private int value3; private int mode; private int overrule; - private String overruletime; + private String overruletime = ""; NhcMessageCmd1(String cmd) { super.setCmd(cmd); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageListMap1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageListMap1.java index 016405dcc3142..a8f9418c15472 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageListMap1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageListMap1.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link NhcMessageListMap1} used as output from gson for cmd or event feedback from Niko Home Control where the * data part is enclosed by [] and contains a list of json strings. Extends {@link NhcMessageBase1}. @@ -25,6 +27,7 @@ * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault class NhcMessageListMap1 extends NhcMessageBase1 { private List> data = new ArrayList<>(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageMap1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageMap1.java index 12cfb12d5f4c9..a2ed5497f5ff4 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageMap1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMessageMap1.java @@ -15,6 +15,8 @@ import java.util.HashMap; import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link NhcMessageMap1} used as output from gson for cmd or event feedback from Niko Home Control where the * data part is a simple json string. Extends {@link NhcMessageBase1}. @@ -23,6 +25,7 @@ * * @author Mark Herwege - Initial Contribution */ +@NonNullByDefault class NhcMessageMap1 extends NhcMessageBase1 { private Map data = new HashMap<>(); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcThermostat1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcThermostat1.java index c9c85339a5f23..697ad9c34b79b 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcThermostat1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcThermostat1.java @@ -42,7 +42,7 @@ public class NhcThermostat1 extends NhcThermostat { */ @Override public void executeMode(int mode) { - logger.debug("Niko Home Control: execute thermostat mode {} for {}", mode, id); + logger.debug("execute thermostat mode {} for {}", mode, id); nhcComm.executeThermostat(id, Integer.toString(mode)); } @@ -55,8 +55,7 @@ public void executeMode(int mode) { */ @Override public void executeOverrule(int overrule, int overruletime) { - logger.debug("Niko Home Control: execute thermostat overrule {} during {} min for {}", overrule, overruletime, - id); + logger.debug("execute thermostat overrule {} during {} min for {}", overrule, overruletime, id); nhcComm.executeThermostat(id, overrule, overruletime); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java index 0eb1adb653aa0..a2c8b7bd40908 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java @@ -20,6 +20,7 @@ import java.net.Socket; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; @@ -94,7 +95,7 @@ public synchronized void startCommunication() { Thread.sleep(1000); } if (nhcEventsRunning) { - logger.debug("Niko Home Control: starting but previous connection still active after 5000ms"); + logger.debug("starting but previous connection still active after 5000ms"); throw new IOException(); } @@ -105,7 +106,7 @@ public synchronized void startCommunication() { nhcSocket = socket; nhcOut = new PrintWriter(socket.getOutputStream(), true); nhcIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.debug("Niko Home Control: connected via local port {}", socket.getLocalPort()); + logger.debug("connected via local port {}", socket.getLocalPort()); // initialize all info in local fields initialize(); @@ -115,9 +116,8 @@ public synchronized void startCommunication() { (new Thread(this::runNhcEvents)).start(); } catch (IOException | InterruptedException e) { - logger.warn("Niko Home Control: error initializing communication"); stopCommunication(); - handler.controllerOffline(); + handler.controllerOffline("Error initializing communication"); } } @@ -139,7 +139,7 @@ public synchronized void stopCommunication() { } nhcSocket = null; - logger.debug("Niko Home Control: communication stopped"); + logger.debug("communication stopped"); } @Override @@ -158,7 +158,7 @@ public boolean communicationActive() { private void runNhcEvents() { String nhcMessage; - logger.debug("Niko Home Control: listening for events"); + logger.debug("listening for events"); listenerStopped = false; nhcEventsRunning = true; @@ -170,7 +170,7 @@ private void runNhcEvents() { if (!listenerStopped) { nhcEventsRunning = false; // this is a socket error, not a communication stop triggered from outside this runnable - logger.warn("Niko Home Control: IO error in listener"); + logger.debug("IO error in listener"); // the IO has stopped working, so we need to close cleanly and try to restart restartCommunication(); return; @@ -181,7 +181,7 @@ private void runNhcEvents() { nhcEventsRunning = false; // this is a stop from outside the runnable, so just log it and stop - logger.debug("Niko Home Control: event listener thread stopped"); + logger.debug("event listener thread stopped"); } /** @@ -218,17 +218,16 @@ private void sendAndReadMessage(String command) throws IOException { @SuppressWarnings("null") private synchronized void sendMessage(Object nhcMessage) { String json = gsonOut.toJson(nhcMessage); - logger.debug("Niko Home Control: send json {}", json); + logger.debug("send json {}", json); nhcOut.println(json); if (nhcOut.checkError()) { - logger.warn("Niko Home Control: error sending message, trying to restart communication"); + logger.debug("error sending message, trying to restart communication"); restartCommunication(); // retry sending after restart - logger.debug("Niko Home Control: resend json {}", json); + logger.debug("resend json {}", json); nhcOut.println(json); if (nhcOut.checkError()) { - logger.warn("Niko Home Control: error resending message"); - handler.controllerOffline(); + handler.controllerOffline("Error resending message"); } } } @@ -239,11 +238,14 @@ private synchronized void sendMessage(Object nhcMessage) { * @param nhcMessage message read from Niko Home Control. */ private void readMessage(@Nullable String nhcMessage) { - logger.debug("Niko Home Control: received json {}", nhcMessage); + logger.debug("received json {}", nhcMessage); try { NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class); + if (nhcMessageGson == null) { + return; + } String cmd = nhcMessageGson.getCmd(); String event = nhcMessageGson.getEvent(); @@ -268,10 +270,10 @@ private void readMessage(@Nullable String nhcMessage) { } else if ("getalarms".equals(event)) { eventGetAlarms(((NhcMessageMap1) nhcMessageGson).getData()); } else { - logger.debug("Niko Home Control: not acted on json {}", nhcMessage); + logger.debug("not acted on json {}", nhcMessage); } } catch (JsonParseException e) { - logger.debug("Niko Home Control: not acted on unsupported json {}", nhcMessage); + logger.debug("not acted on unsupported json {}", nhcMessage); } } @@ -283,7 +285,7 @@ private void setIfPresent(Map data, String key, Consumer } private synchronized void cmdSystemInfo(Map data) { - logger.debug("Niko Home Control: systeminfo"); + logger.debug("systeminfo"); setIfPresent(data, "swversion", systemInfo::setSwVersion); setIfPresent(data, "api", systemInfo::setApi); @@ -311,17 +313,17 @@ private void cmdStartEvents(Map data) { if (errorCodeString != null) { int errorCode = Integer.parseInt(errorCodeString); if (errorCode == 0) { - logger.debug("Niko Home Control: start events success"); + logger.debug("start events success"); } else { - logger.warn("Niko Home Control: error code {} returned on start events", errorCode); + logger.debug("error code {} returned on start events", errorCode); } } else { - logger.warn("Niko Home Control: could not determine error code returned on start events"); + logger.debug("could not determine error code returned on start events"); } } private void cmdListLocations(List> data) { - logger.debug("Niko Home Control: list locations"); + logger.debug("list locations"); locations.clear(); @@ -338,7 +340,7 @@ private void cmdListLocations(List> data) { } private void cmdListActions(List> data) { - logger.debug("Niko Home Control: list actions"); + logger.debug("list actions"); for (Map action : data) { String id = action.get("id"); @@ -360,7 +362,7 @@ private void cmdListActions(List> data) { logger.debug("name not found in action {}", action); continue; } - String type = action.get("type"); + String type = Optional.ofNullable(action.get("type")).orElse(""); ActionType actionType = ActionType.GENERIC; switch (type) { case "0": @@ -377,7 +379,7 @@ private void cmdListActions(List> data) { actionType = ActionType.ROLLERSHUTTER; break; default: - logger.debug("Niko Home Control: unknown action type {} for action {}", type, id); + logger.debug("unknown action type {} for action {}", type, id); continue; } String locationId = action.get("location"); @@ -395,14 +397,18 @@ private void cmdListActions(List> data) { // Action object already exists, so only update state. // If we would re-instantiate action, we would lose pointer back from action to thing handler that was // set in thing handler initialize(). - actions.get(id).setState(state); + NhcAction nhcAction = actions.get(id); + if (nhcAction != null) { + nhcAction.setState(state); + } } } } private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException { - if (str == null) + if (str == null) { throw new IllegalArgumentException("String is null"); + } try { return Integer.parseInt(str); } catch (NumberFormatException e) { @@ -411,7 +417,7 @@ private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentExceptio } private void cmdListThermostat(List> data) { - logger.debug("Niko Home Control: list thermostats"); + logger.debug("list thermostats"); for (Map thermostat : data) { try { @@ -442,8 +448,11 @@ private void cmdListThermostat(List> data) { String name = thermostat.get("name"); String locationId = thermostat.get("location"); String location = ""; - if (!locationId.isEmpty()) { - location = locations.get(locationId).getName(); + if (!((locationId == null) || locationId.isEmpty())) { + NhcLocation1 nhcLocation = locations.get(locationId); + if (nhcLocation != null) { + location = nhcLocation.getName(); + } } if (name != null) { return new NhcThermostat1(i, name, location, this); @@ -463,12 +472,12 @@ private void cmdExecuteActions(Map data) { try { int errorCode = parseIntOrThrow(data.get("error")); if (errorCode == 0) { - logger.debug("Niko Home Control: execute action success"); + logger.debug("execute action success"); } else { - logger.warn("Niko Home Control: error code {} returned on command execution", errorCode); + logger.debug("error code {} returned on command execution", errorCode); } } catch (IllegalArgumentException e) { - logger.warn("Niko Home Control: no error code returned on command execution"); + logger.debug("no error code returned on command execution"); } } @@ -476,12 +485,12 @@ private void cmdExecuteThermostat(Map data) { try { int errorCode = parseIntOrThrow(data.get("error")); if (errorCode == 0) { - logger.debug("Niko Home Control: execute thermostats success"); + logger.debug("execute thermostats success"); } else { - logger.warn("Niko Home Control: error code {} returned on command execution", errorCode); + logger.debug("error code {} returned on command execution", errorCode); } } catch (IllegalArgumentException e) { - logger.warn("Niko Home Control: no error code returned on command execution"); + logger.debug("no error code returned on command execution"); } } @@ -489,13 +498,13 @@ private void eventListActions(List> data) { for (Map action : data) { String id = action.get("id"); if (id == null || !actions.containsKey(id)) { - logger.warn("Niko Home Control: action in controller not known {}", id); + logger.warn("action in controller not known {}", id); return; } String stateString = action.get("value1"); if (stateString != null) { int state = Integer.parseInt(stateString); - logger.debug("Niko Home Control: event execute action {} with state {}", id, state); + logger.debug("event execute action {} with state {}", id, state); NhcAction action1 = actions.get(id); if (action1 != null) { action1.setState(state); @@ -509,7 +518,7 @@ private void eventListThermostat(List> data) { try { String id = thermostat.get("id"); if (!thermostats.containsKey(id)) { - logger.warn("Niko Home Control: thermostat in controller not known {}", id); + logger.warn("thermostat in controller not known {}", id); return; } @@ -549,15 +558,15 @@ private void eventGetAlarms(Map data) { } switch (data.getOrDefault("type", "")) { case "0": - logger.debug("Niko Home Control: alarm - {}", alarmText); + logger.debug("alarm - {}", alarmText); handler.alarmEvent(alarmText); break; case "1": - logger.debug("Niko Home Control: notice - {}", alarmText); + logger.debug("notice - {}", alarmText); handler.noticeEvent(alarmText); break; default: - logger.debug("Niko Home Control: unexpected message type {}", data.get("type")); + logger.debug("unexpected message type {}", data.get("type")); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlMessageDeserializer1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlMessageDeserializer1.java index 3429512631bc0..df5b4fd9816b2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlMessageDeserializer1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlMessageDeserializer1.java @@ -19,6 +19,9 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; @@ -33,16 +36,17 @@ * @author Mark Herwege - Initial Contribution * */ +@NonNullByDefault class NikoHomeControlMessageDeserializer1 implements JsonDeserializer { @Override - public NhcMessageBase1 deserialize(final JsonElement json, final Type typeOfT, + public @Nullable NhcMessageBase1 deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { final JsonObject jsonObject = json.getAsJsonObject(); try { - String cmd = null; - String event = null; + String cmd = ""; + String event = ""; if (jsonObject.has("cmd")) { cmd = jsonObject.get("cmd").getAsString(); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcAction2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcAction2.java index 3b66d90f45b27..1155ac5b6f843 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcAction2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcAction2.java @@ -115,7 +115,7 @@ public void setState(int state) { */ @Override public void execute(String command) { - logger.debug("Niko Home Control: execute action {} of type {} for {}", command, type, id); + logger.debug("execute action {} of type {} for {}", command, type, id); nhcComm.executeAction(id, command); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java index 86ff112c0adf7..6ad1a686db3c1 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java @@ -100,7 +100,7 @@ private TrustManager[] getTrustManagers() throws CertificateException { tmFactory.init(keyStore); return tmFactory.getTrustManagers(); } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException e) { - logger.warn("Niko Home Control: error with SSL context creation: {} ", e.getMessage()); + logger.debug("error with SSL context creation: {} ", e.getMessage()); throw new CertificateException("SSL context creation exception", e); } finally { ResourceBundle.clearCache(); @@ -121,14 +121,14 @@ synchronized void startConnection(String cocoAddress, int port, String profile, if (future != null) { try { future.get(5000, TimeUnit.MILLISECONDS); - logger.debug("Niko Home Control: finished stopping connection"); + logger.debug("finished stopping connection"); } catch (InterruptedException | ExecutionException | TimeoutException ignore) { - logger.debug("Niko Home Control: error stopping connection"); + logger.debug("error stopping connection"); } stoppedFuture = null; } - logger.debug("Niko Home Control: starting connection..."); + logger.debug("starting connection..."); this.cocoAddress = cocoAddress; this.port = port; this.profile = profile; @@ -142,17 +142,17 @@ synchronized void startConnection(String cocoAddress, int port, String profile, subscribedFuture = connection.subscribe("#", messageSubscriber); } } else { - logger.debug("Niko Home Control: error connecting"); + logger.debug("error connecting"); throw new MqttException("Connection execution exception"); } } catch (InterruptedException e) { - logger.debug("Niko Home Control: connection interrupted exception"); + logger.debug("connection interrupted exception"); throw new MqttException("Connection interrupted exception"); } catch (ExecutionException e) { - logger.debug("Niko Home Control: connection execution exception", e.getCause()); + logger.debug("connection execution exception", e.getCause()); throw new MqttException("Connection execution exception"); } catch (TimeoutException e) { - logger.debug("Niko Home Control: connection timeout exception"); + logger.debug("connection timeout exception"); throw new MqttException("Connection timeout exception"); } } @@ -169,7 +169,7 @@ private MqttBrokerConnection createMqttConnection() throws MqttException { * Stop the MQTT connection. */ void stopConnection() { - logger.debug("Niko Home Control: stopping connection..."); + logger.debug("stopping connection..."); MqttBrokerConnection connection = mqttConnection; if (connection != null) { connection.removeConnectionObserver(connectionObserver); @@ -203,7 +203,7 @@ private boolean isConnected() { try { if ((future != null) && future.get(5000, TimeUnit.MILLISECONDS)) { MqttConnectionState state = connection.connectionState(); - logger.debug("Niko Home Control: connection state {} for {}", state, connection.getClientId()); + logger.debug("connection state {} for {}", state, connection.getClientId()); return state == MqttConnectionState.CONNECTED; } } catch (InterruptedException | ExecutionException | TimeoutException e) { @@ -223,25 +223,25 @@ private boolean isConnected() { void connectionPublish(String topic, String payload) throws MqttException { MqttBrokerConnection connection = mqttConnection; if (connection == null) { - logger.debug("Niko Home Control: cannot publish, no connection"); + logger.debug("cannot publish, no connection"); throw new MqttException("No connection exception"); } if (isConnected()) { - logger.debug("Niko Home Control: publish {}, {}", topic, payload); + logger.debug("publish {}, {}", topic, payload); connection.publish(topic, payload.getBytes(), connection.getQos(), false); } else { - logger.debug("Niko Home Control: cannot publish, not subscribed to connection messages"); + logger.debug("cannot publish, not subscribed to connection messages"); } } @Override public void onSuccess(String topic) { - logger.debug("Niko Home Control: publish succeeded {}", topic); + logger.debug("publish succeeded {}", topic); } @Override public void onFailure(String topic, Throwable error) { - logger.debug("Niko Home Control: publish failed {}, {}", topic, error.getMessage(), error); + logger.debug("publish failed {}, {}", topic, error.getMessage(), error); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcThermostat2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcThermostat2.java index 4f05e4421359d..06b0936727759 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcThermostat2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcThermostat2.java @@ -45,7 +45,7 @@ protected NhcThermostat2(String id, String name, String model, String technology @Override public void executeMode(int mode) { - logger.debug("Niko Home Control: execute thermostat mode {} for {}", mode, id); + logger.debug("execute thermostat mode {} for {}", mode, id); String program = THERMOSTATMODES[mode]; nhcComm.executeThermostat(id, program); @@ -53,8 +53,7 @@ public void executeMode(int mode) { @Override public void executeOverrule(int overrule, int overruletime) { - logger.debug("Niko Home Control: execute thermostat overrule {} during {} min for {}", overrule, overruletime, - id); + logger.debug("execute thermostat overrule {} during {} min for {}", overrule, overruletime, id); nhcComm.executeThermostat(id, overrule, overruletime); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 916ae9c20ecaa..866e18283ddb2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -33,7 +33,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nikohomecontrol.internal.protocol.*; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcEnergyMeter; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat; +import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam; @@ -104,19 +108,19 @@ public synchronized void startCommunication() { InetAddress addr = handler.getAddr(); if (addr == null) { - logger.warn("Niko Home Control: IP address cannot be empty"); + logger.warn("IP address cannot be empty"); stopCommunication(); return; } String addrString = addr.getHostAddress(); int port = handler.getPort(); - logger.debug("Niko Home Control: initializing for mqtt connection to CoCo on {}:{}", addrString, port); + logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port); profile = handler.getProfile(); String token = handler.getToken(); if (token.isEmpty()) { - logger.warn("Niko Home Control: JWT token cannot be empty"); + logger.warn("JWT token cannot be empty"); stopCommunication(); return; } @@ -125,7 +129,7 @@ public synchronized void startCommunication() { mqttConnection.startConnection(addrString, port, profile, token); initialize(); } catch (MqttException e) { - logger.warn("Niko Home Control: error in mqtt communication"); + logger.debug("error in mqtt communication"); stopCommunication(); } } @@ -150,7 +154,7 @@ public boolean communicationActive() { // Wait until we received all devices info to confirm we are active. return started.get(5000, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("Niko Home Control: exception waiting for connection start"); + logger.debug("exception waiting for connection start"); return false; } } @@ -176,10 +180,10 @@ private void initialize() throws MqttException { mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message)); } - private void connectionLost() { - logger.debug("Niko Home Control: connection lost"); + private void connectionLost(String message) { + logger.debug("connection lost"); stopCommunication(); - handler.controllerOffline(); + handler.controllerOffline(message); } private void systemEvt(String response) { @@ -189,13 +193,13 @@ private void systemEvt(String response) { List systemInfo = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - List messageParams = message.params; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo; systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if timeInfo not present in response, this should not happen in a timeInfo response } @@ -214,12 +218,12 @@ private void systeminfoPublishRsp(String response) { List systemInfo = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - List messageParams = message.params; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if systemInfo not present in response, this should not happen in a systemInfo response } @@ -234,12 +238,12 @@ private void servicesListRsp(String response) { List serviceList = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - List messageParams = message.params; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if services not present in response, this should not happen in a services response } @@ -255,12 +259,12 @@ private void devicesListRsp(String response) { List deviceList = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - List messageParams = message.params; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if devices not present in response, this should not happen in a devices response } @@ -274,7 +278,7 @@ private void devicesListRsp(String response) { } // Once a devices list response is received, we know the communication is fully started. - logger.debug("Niko Home Control: Communication start complete."); + logger.debug("Communication start complete."); handler.controllerOnline(); CompletableFuture future = communicationStarted; if (future != null) { @@ -289,13 +293,13 @@ private void devicesEvt(String response) { String method = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - method = message.method; - List messageParams = message.params; + method = (message != null) ? message.method : null; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if devices not present in response, this should not happen in a devices event } @@ -308,9 +312,6 @@ private void devicesEvt(String response) { return; } else if ("devices.added".equals(method)) { deviceList.forEach(this::addDevice); - } else if ("devices.changed".equals(method)) { - deviceList.forEach(this::removeDevice); - deviceList.forEach(this::addDevice); } deviceList.forEach(this::updateState); @@ -322,17 +323,17 @@ private void notificationEvt(String response) { List notificationList = null; try { NhcMessage2 message = gson.fromJson(response, messageType); - List messageParams = message.params; + List messageParams = (message != null) ? message.params : null; if (messageParams != null) { notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst() .get().notifications; } } catch (JsonSyntaxException e) { - logger.debug("Niko Home Control: unexpected json {}", response); + logger.debug("unexpected json {}", response); } catch (NoSuchElementException ignore) { // Ignore if notifications not present in response, this should not happen in a notifications event } - logger.debug("Niko Home Control: notifications {}", notificationList); + logger.debug("notifications {}", notificationList); if (notificationList == null) { return; } @@ -348,7 +349,7 @@ private void notificationEvt(String response) { handler.noticeEvent(alarmText); break; default: - logger.debug("Niko Home Control: unexpected message type {}", notification.type); + logger.debug("unexpected message type {}", notification.type); } } } @@ -363,7 +364,7 @@ private void addDevice(NhcDevice2 device) { if ("action".equals(device.type)) { if (!actions.containsKey(device.uuid)) { - logger.debug("Niko Home Control: adding action device {}, {}", device.uuid, device.name); + logger.debug("adding action device {}, {}", device.uuid, device.name); ActionType actionType; switch (device.model) { @@ -394,8 +395,7 @@ private void addDevice(NhcDevice2 device) { break; default: actionType = ActionType.GENERIC; - logger.debug("Niko Home Control: device type {} not recognised, default to GENERIC action", - device.type); + logger.debug("device type {} not recognised, default to GENERIC action", device.type); } NhcAction2 nhcAction = new NhcAction2(device.uuid, device.name, device.model, device.technology, @@ -404,7 +404,7 @@ private void addDevice(NhcDevice2 device) { } } else if ("thermostat".equals(device.type)) { if (!thermostats.containsKey(device.uuid)) { - logger.debug("Niko Home Control: adding thermostat device {}, {}", device.uuid, device.name); + logger.debug("adding thermostat device {}, {}", device.uuid, device.name); NhcThermostat2 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.model, device.technology, location, this); @@ -412,26 +412,28 @@ private void addDevice(NhcDevice2 device) { } } else if ("centralmeter".equals(device.type)) { if (!energyMeters.containsKey(device.uuid)) { - logger.debug("Niko Home Control: adding centralmeter device {}, {}", device.uuid, device.name); + logger.debug("adding centralmeter device {}, {}", device.uuid, device.name); NhcEnergyMeter2 nhcEnergyMeter = new NhcEnergyMeter2(device.uuid, device.name, device.model, device.technology, this, scheduler); energyMeters.put(device.uuid, nhcEnergyMeter); } } else { - logger.debug("Niko Home Control: device type {} not supported for {}, {}", device.type, device.uuid, - device.name); + logger.debug("device type {} not supported for {}, {}", device.type, device.uuid, device.name); } } private void removeDevice(NhcDevice2 device) { - if (actions.containsKey(device.uuid)) { - actions.get(device.uuid).actionRemoved(); + NhcAction action = actions.get(device.uuid); + NhcThermostat thermostat = thermostats.get(device.uuid); + NhcEnergyMeter energyMeter = energyMeters.get(device.uuid); + if (action != null) { + action.actionRemoved(); actions.remove(device.uuid); - } else if (thermostats.containsKey(device.uuid)) { - thermostats.get(device.uuid).thermostatRemoved(); + } else if (thermostat != null) { + thermostat.thermostatRemoved(); thermostats.remove(device.uuid); - } else if (energyMeters.containsKey(device.uuid)) { - energyMeters.get(device.uuid).energyMeterRemoved(); + } else if (energyMeter != null) { + energyMeter.energyMeterRemoved(); energyMeters.remove(device.uuid); } } @@ -481,10 +483,10 @@ private void updateLightState(NhcAction2 action, List devicePropert if (booleanState != null) { if (NHCON.equals(booleanState)) { action.setBooleanState(true); - logger.debug("Niko Home Control: setting action {} internally to ON", action.getId()); + logger.debug("setting action {} internally to ON", action.getId()); } else if (NHCOFF.equals(booleanState)) { action.setBooleanState(false); - logger.debug("Niko Home Control: setting action {} internally to OFF", action.getId()); + logger.debug("setting action {} internally to OFF", action.getId()); } } @@ -492,8 +494,7 @@ private void updateLightState(NhcAction2 action, List devicePropert String brightness = dimmerProperty.get().brightness; if (brightness != null) { action.setState(Integer.parseInt(brightness)); - logger.debug("Niko Home Control: setting action {} internally to {}", action.getId(), - dimmerProperty.get().brightness); + logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness); } } } @@ -502,9 +503,9 @@ private void updateRollershutterState(NhcAction2 action, List devic deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> { try { action.setState(Integer.parseInt(position)); - logger.debug("Niko Home Control: setting action {} internally to {}", action.getId(), position); + logger.debug("setting action {} internally to {}", action.getId(), position); } catch (NumberFormatException e) { - logger.trace("Niko Home Control: received empty rollershutter {} position info", action.getId()); + logger.trace("received empty rollershutter {} position info", action.getId()); } }); } @@ -577,12 +578,10 @@ private void updateEnergyMeterState(NhcEnergyMeter2 energyMeter, List { try { energyMeter.setPower(Integer.parseInt(electricalPower)); - logger.trace("Niko Home Control: setting energy meter {} power to {}", energyMeter.getId(), - electricalPower); + logger.trace("setting energy meter {} power to {}", energyMeter.getId(), electricalPower); } catch (NumberFormatException e) { energyMeter.setPower(null); - logger.trace("Niko Home Control: received empty energy meter {} power reading", - energyMeter.getId()); + logger.trace("received empty energy meter {} power reading", energyMeter.getId()); } }); } @@ -607,6 +606,9 @@ public void executeAction(String actionId, String value) { device.properties = deviceProperties; NhcAction2 action = (NhcAction2) actions.get(actionId); + if (action == null) { + return; + } switch (action.getType()) { case GENERIC: @@ -639,7 +641,7 @@ public void executeAction(String actionId, String value) { } else if (NHCDOWN.equals(value)) { property.position = "0"; } else { - int position = 100 - Integer.parseInt(value); + int position = Integer.parseInt(value); property.position = String.valueOf(position); } break; @@ -745,12 +747,18 @@ public void startEnergyMeter(String energyMeterId) { String topic = profile + "/control/devices/cmd"; String gsonMessage = gson.toJson(message); - ((NhcEnergyMeter2) energyMeters.get(energyMeterId)).startEnergyMeter(topic, gsonMessage); + NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId); + if (energyMeter != null) { + energyMeter.startEnergyMeter(topic, gsonMessage); + } } @Override public void stopEnergyMeter(String energyMeterId) { - ((NhcEnergyMeter2) energyMeters.get(energyMeterId)).stopEnergyMeter(); + NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId); + if (energyMeter != null) { + energyMeter.stopEnergyMeter(); + } } /** @@ -768,19 +776,26 @@ private void sendDeviceMessage(String topic, String gsonMessage) { mqttConnection.connectionPublish(topic, gsonMessage); } catch (MqttException e) { - logger.warn("Niko Home Control: sending command failed, trying to restart communication"); + String message = e.getMessage(); + message = (message != null) ? message : "Communication error"; + + logger.debug("sending command failed, trying to restart communication"); restartCommunication(); // retry sending after restart try { if (communicationActive()) { mqttConnection.connectionPublish(topic, gsonMessage); } else { - logger.warn("Niko Home Control: failed to restart communication"); - connectionLost(); + logger.debug("failed to restart communication"); } } catch (MqttException e1) { - logger.warn("Niko Home Control: error resending device command"); - connectionLost(); + message = e1.getMessage(); + message = (message != null) ? message : "Communication error"; + + logger.debug("error resending device command"); + } + if (!communicationActive()) { + connectionLost(message); } } } @@ -791,24 +806,24 @@ public void processMessage(String topic, byte[] payload) { if ((profile + "/system/evt").equals(topic)) { systemEvt(message); } else if ((profile + "/system/rsp").equals(topic)) { - logger.debug("Niko Home Control: received topic {}, payload {}", topic, message); + logger.debug("received topic {}, payload {}", topic, message); systeminfoPublishRsp(message); } else if ((profile + "/notification/evt").equals(topic)) { - logger.debug("Niko Home Control: received topic {}, payload {}", topic, message); + logger.debug("received topic {}, payload {}", topic, message); notificationEvt(message); } else if ((profile + "/control/devices/evt").equals(topic)) { - logger.trace("Niko Home Control: received topic {}, payload {}", topic, message); + logger.trace("received topic {}, payload {}", topic, message); devicesEvt(message); } else if ((profile + "/control/devices/rsp").equals(topic)) { - logger.debug("Niko Home Control: received topic {}, payload {}", topic, message); + logger.debug("received topic {}, payload {}", topic, message); devicesListRsp(message); } else if ((profile + "/authentication/rsp").equals(topic)) { - logger.debug("Niko Home Control: received topic {}, payload {}", topic, message); + logger.debug("received topic {}, payload {}", topic, message); servicesListRsp(message); } else if ((profile + "/control/devices.error").equals(topic)) { - logger.warn("Niko Home Control: received error {}", message); + logger.warn("received error {}", message); } else { - logger.trace("Niko Home Control: not acted on received message topic {}, payload {}", topic, message); + logger.trace("not acted on received message topic {}, payload {}", topic, message); } } @@ -845,10 +860,14 @@ public String getServices() { public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) { if (error != null) { logger.debug("Connection state: {}", state, error); - restartCommunication(); + String message = error.getMessage(); + message = (message != null) ? message : "Error communicating with the controller"; + if (!MqttConnectionState.CONNECTING.equals(state)) { + // This is a connection loss, try to restart + restartCommunication(); + } if (!communicationActive()) { - logger.warn("Niko Home Control: failed to restart communication"); - connectionLost(); + connectionLost(message); } } else { logger.trace("Connection state: {}", state); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index c2b637da355d9..6382d8656b414 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -127,7 +127,7 @@ Niko Home Control action ID false - + Step value used for increase/decrease of dimmer brightness, default 10% 10 @@ -151,6 +151,12 @@ Niko Home Control action ID false + + + Invert rollershutter direction + false + true + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java index 87cec1ad18bc2..608663ebd84f8 100644 --- a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java @@ -16,6 +16,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.TooManyListenersException; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -62,6 +63,7 @@ public class SDS011Handler extends BaseThingHandler { private @Nullable ScheduledFuture dataReadJob; private @Nullable ScheduledFuture connectionMonitor; + private @Nullable Future initJob; private @Nullable ScheduledFuture retryInitJob; private ZonedDateTime lastCommunication = ZonedDateTime.now(); @@ -115,10 +117,10 @@ public void initialize() { if (config.reporting) { timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval); - scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); + initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); } else { timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval); - scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); + initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); } } @@ -130,66 +132,45 @@ private void initializeCommunicator(WorkMode mode, Duration interval) { return; } - boolean initSuccessful = false; - int retryInit = 3; - int retryCount = 0; - // sometimes the device is a little difficult and needs multiple configuration attempts - while (!initSuccessful && retryCount < retryInit) { - logger.trace("Trying to initialize device attempt={}", retryCount); - initSuccessful = doInit(localCommunicator, mode, interval); - retryCount++; - } - - if (initSuccessful) { - lastCommunication = ZonedDateTime.now(); - updateStatus(ThingStatus.ONLINE); + logger.trace("Trying to initialize device"); + doInit(localCommunicator, mode, interval); - if (mode == WorkMode.POLLING) { - dataReadJob = scheduler.scheduleWithFixedDelay(() -> { - try { - localCommunicator.requestSensorData(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Cannot query data from device"); - } - }, 2, config.pollingInterval, TimeUnit.SECONDS); - } else { - // start a job that reads the port until data arrives - int reportingReadStartDelay = 10; - int startReadBeforeDataArrives = 5; - long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay - - startReadBeforeDataArrives; - logger.trace("Scheduling job to receive reported values"); - dataReadJob = scheduler.scheduleWithFixedDelay(() -> { - try { - localCommunicator.readSensorData(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Cannot query data from device, because: " + e.getMessage()); - } - }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS); - } + lastCommunication = ZonedDateTime.now(); - Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive - .plus(CONNECTION_MONITOR_START_DELAY_OFFSET); - connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, - connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), - TimeUnit.SECONDS); + if (mode == WorkMode.POLLING) { + dataReadJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.requestSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device"); + } + }, 2, config.pollingInterval, TimeUnit.SECONDS); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Commands and replies from the device don't seem to match"); - logger.debug( - "Could not configure sensor -> setting Thing to OFFLINE, disposing the handler and reschedule initialize in {} seconds", - RETRY_INIT_DELAY); - doDispose(false); - retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS); + // start a job that reads the port until data arrives + int reportingReadStartDelay = 10; + int startReadBeforeDataArrives = 5; + long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay + - startReadBeforeDataArrives; + logger.trace("Scheduling job to receive reported values"); + dataReadJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.readSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device, because: " + e.getMessage()); + } + }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS); } + + Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET); + connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, + connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS); } - private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) { - boolean initSuccessful = false; + private void doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) { try { - initSuccessful = localCommunicator.initialize(mode, interval); + localCommunicator.initialize(mode, interval); } catch (final IOException ex) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!"); } catch (PortInUseException e) { @@ -201,7 +182,6 @@ private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Dura updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Cannot set serial port parameters"); } - return initSuccessful; } private boolean validateConfiguration() { @@ -243,6 +223,12 @@ private void doDispose(boolean sendDeviceToSleep) { this.connectionMonitor = null; } + Future localInitJob = this.initJob; + if (localInitJob != null) { + localInitJob.cancel(true); + this.initJob = null; + } + ScheduledFuture localRetryOpenPortJob = this.retryInitJob; if (localRetryOpenPortJob != null) { localRetryOpenPortJob.cancel(true); diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java index 3e3c11e32980d..8ea0299026d98 100644 --- a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java @@ -29,12 +29,8 @@ import org.openhab.binding.novafinedust.internal.SDS011Handler; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPortIdentifier; @@ -52,7 +48,7 @@ @NonNullByDefault public class SDS011Communicator { - private static final int MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY = 20; + private static final int MAX_READ_UNTIL_SENSOR_DATA = 6; // at least 6 because we send 5 configuration commands private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class); @@ -82,9 +78,8 @@ public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portI * @throws IOException * @throws UnsupportedCommOperationException */ - public boolean initialize(WorkMode mode, Duration interval) + public void initialize(WorkMode mode, Duration interval) throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException { - boolean initSuccessful = true; logger.trace("Initializing with mode={}, interval={}", mode, interval); @@ -102,30 +97,28 @@ public boolean initialize(WorkMode mode, Duration interval) logger.trace("Input and Outputstream opened for the port"); // wake up the device - initSuccessful &= sendSleep(false); - logger.trace("Wake up call done, initSuccessful={}", initSuccessful); - initSuccessful &= getFirmware(); - logger.trace("Firmware requested, initSuccessful={}", initSuccessful); + sendSleep(false); + logger.trace("Wake up call done"); + getFirmware(); + logger.trace("Firmware requested"); if (mode == WorkMode.POLLING) { - initSuccessful &= setMode(WorkMode.POLLING); - logger.trace("Polling mode set, initSuccessful={}", initSuccessful); - initSuccessful &= setWorkingPeriod((byte) 0); - logger.trace("Working period for polling set, initSuccessful={}", initSuccessful); + setMode(WorkMode.POLLING); + logger.trace("Polling mode set"); + setWorkingPeriod((byte) 0); + logger.trace("Working period for polling set"); } else { // reporting - initSuccessful &= setWorkingPeriod((byte) interval.toMinutes()); - logger.trace("Working period for reporting set, initSuccessful={}", initSuccessful); - initSuccessful &= setMode(WorkMode.REPORTING); - logger.trace("Reporting mode set, initSuccessful={}", initSuccessful); + setWorkingPeriod((byte) interval.toMinutes()); + logger.trace("Working period for reporting set"); + setMode(WorkMode.REPORTING); + logger.trace("Reporting mode set"); } this.serialPort = localSerialPort; - - return initSuccessful; } - private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException { + private void sendCommand(CommandMessage message) throws IOException { byte[] commandData = message.getBytes(); if (logger.isDebugEnabled()) { logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData)); @@ -139,23 +132,12 @@ public boolean initialize(WorkMode mode, Duration interval) } try { - // Give the sensor some time to handle the command + // Give the sensor some time to handle the command before doing something else with it Thread.sleep(500); } catch (InterruptedException e) { - logger.warn("Problem while waiting for reading a reply to our command."); + logger.warn("Interrupted while waiting after sending command={}", message); Thread.currentThread().interrupt(); } - SensorReply reply = readReply(); - // in case there is still another reporting active, we want to discard the sensor data and read the reply to our - // command again, this might happen more often in case the sensor has buffered some data - for (int i = 0; i < MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY; i++) { - if (reply instanceof SensorMeasuredDataReply) { - reply = readReply(); - } else { - break; - } - } - return reply; } private void write(byte[] commandData) throws IOException { @@ -166,21 +148,13 @@ private void write(byte[] commandData) throws IOException { } } - private boolean setWorkingPeriod(byte period) throws IOException { + private void setWorkingPeriod(byte period) throws IOException { CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period }); logger.debug("Sending work period: {}", period); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to setWorkingPeriod command: {}", reply); - if (reply instanceof WorkingPeriodReply) { - WorkingPeriodReply wpReply = (WorkingPeriodReply) reply; - if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) { - return true; - } - } - return false; + sendCommand(m); } - private boolean setMode(WorkMode workMode) throws IOException { + private void setMode(WorkMode workMode) throws IOException { byte haveToRequestData = 0; if (workMode == WorkMode.POLLING) { haveToRequestData = 1; @@ -188,18 +162,10 @@ private boolean setMode(WorkMode workMode) throws IOException { CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData }); logger.debug("Sending mode: {}", workMode); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to setMode command: {}", reply); - if (reply instanceof ModeReply) { - ModeReply mr = (ModeReply) reply; - if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) { - return true; - } - } - return false; + sendCommand(m); } - private boolean sendSleep(boolean doSleep) throws IOException { + private void sendSleep(boolean doSleep) throws IOException { byte payload = (byte) 1; if (doSleep) { payload = (byte) 0; @@ -207,38 +173,21 @@ private boolean sendSleep(boolean doSleep) throws IOException { CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload }); logger.debug("Sending doSleep: {}", doSleep); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to sendSleep command: {}", reply); + sendCommand(m); + // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying + // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration + // commands if (!doSleep) { // sometimes the sensor does not wakeup on the first attempt, thus we try again - for (int i = 0; reply == null && i < 3; i++) { - reply = sendCommand(m); - logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply); - } + sendCommand(m); } - - if (reply instanceof SleepReply) { - SleepReply sr = (SleepReply) reply; - if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) { - return true; - } - } - return false; } - private boolean getFirmware() throws IOException { + private void getFirmware() throws IOException { CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {}); logger.debug("Sending get firmware request"); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to getFirmware command: {}", reply); - - if (reply instanceof SensorFirmwareReply) { - SensorFirmwareReply fwReply = (SensorFirmwareReply) reply; - thingHandler.setFirmware(fwReply.getFirmware()); - return true; - } - return false; + sendCommand(m); } /** @@ -256,7 +205,7 @@ public void requestSensorData() throws IOException { try { Thread.sleep(200); // give the device some time to handle the command } catch (InterruptedException e) { - logger.warn("Interrupted while waiting before reading a reply to our rquest data command."); + logger.warn("Interrupted while waiting before reading a reply to our request data command."); Thread.currentThread().interrupt(); } readSensorData(); @@ -271,7 +220,7 @@ public void requestSensorData() throws IOException { if (localInpuStream != null) { logger.trace("Reading for reply until first byte is found"); while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) { - logger.debug("Trying to find first reply byte now..."); + // logger.trace("Trying to find first reply byte now..."); } readBuffer[0] = (byte) b; int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1); @@ -286,16 +235,26 @@ public void requestSensorData() throws IOException { public void readSensorData() throws IOException { logger.trace("readSensorData() called"); + + boolean foundSensorData = doRead(); + for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) { + foundSensorData = doRead(); + } + } + + private boolean doRead() throws IOException { SensorReply reply = readReply(); - logger.trace("readSensorData(): Read reply={}", reply); + logger.trace("doRead(): Read reply={}", reply); if (reply instanceof SensorMeasuredDataReply) { SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply; logger.trace("We received sensor data"); if (sensorData.isValidData()) { logger.trace("Sensor data is valid => updating channels"); thingHandler.updateChannels(sensorData); + return true; } } + return false; } /** diff --git a/bundles/org.openhab.binding.nuki/README.md b/bundles/org.openhab.binding.nuki/README.md index 8fbb20bd94c68..19bd16cf68448 100644 --- a/bundles/org.openhab.binding.nuki/README.md +++ b/bundles/org.openhab.binding.nuki/README.md @@ -93,7 +93,7 @@ sitemap nuki label="Nuki Smart Lock" { Switch item=Frontdoor_Lock } Frame label="Channel State used for lock actions" { - Switch item=Frontdoor_State mappings=[2="Unlock", 7="Unlatch", 1002="LnGo", 1007="LnGoU", 4="Lock"] + Switch item=Frontdoor_LockState mappings=[2="Unlock", 7="Unlatch", 1002="LnGo", 1007="LnGoU", 4="Lock"] } Frame label="Channel State" { Text item=Frontdoor_LockState label="Lock State [MAP(nukilockstates.map):%s]" diff --git a/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/NetworkOceanicThingHandler.java b/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/NetworkOceanicThingHandler.java index f968b1618b013..8b1f9b39700f1 100644 --- a/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/NetworkOceanicThingHandler.java +++ b/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/NetworkOceanicThingHandler.java @@ -21,7 +21,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.oceanic.internal.NetworkOceanicBindingConfiguration; diff --git a/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/SerialOceanicThingHandler.java b/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/SerialOceanicThingHandler.java index bb87024df73f3..f9d54b8035826 100644 --- a/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/SerialOceanicThingHandler.java +++ b/bundles/org.openhab.binding.oceanic/src/main/java/org/openhab/binding/oceanic/internal/handler/SerialOceanicThingHandler.java @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.Enumeration; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.oceanic.internal.SerialOceanicBindingConfiguration; import org.openhab.binding.oceanic.internal.Throttler; import org.openhab.core.thing.Thing; diff --git a/bundles/org.openhab.binding.oceanic/src/main/resources/OH-INF/thing/network.xml b/bundles/org.openhab.binding.oceanic/src/main/resources/OH-INF/thing/network.xml index 9fc10fc31e81a..49e731f472955 100644 --- a/bundles/org.openhab.binding.oceanic/src/main/resources/OH-INF/thing/network.xml +++ b/bundles/org.openhab.binding.oceanic/src/main/resources/OH-INF/thing/network.xml @@ -59,7 +59,6 @@ Port number of the network proxy - false diff --git a/bundles/org.openhab.binding.omnikinverter/src/main/java/org/openhab/binding/omnikinverter/internal/OmnikInverter.java b/bundles/org.openhab.binding.omnikinverter/src/main/java/org/openhab/binding/omnikinverter/internal/OmnikInverter.java index fc3bc89caa2ba..9ef1d649965cd 100644 --- a/bundles/org.openhab.binding.omnikinverter/src/main/java/org/openhab/binding/omnikinverter/internal/OmnikInverter.java +++ b/bundles/org.openhab.binding.omnikinverter/src/main/java/org/openhab/binding/omnikinverter/internal/OmnikInverter.java @@ -16,7 +16,7 @@ import java.net.Socket; import java.nio.ByteBuffer; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; /** diff --git a/bundles/org.openhab.binding.omnilink/pom.xml b/bundles/org.openhab.binding.omnilink/pom.xml index ee0e2767fb4ae..7b68c65c9417f 100644 --- a/bundles/org.openhab.binding.omnilink/pom.xml +++ b/bundles/org.openhab.binding.omnilink/pom.xml @@ -18,7 +18,7 @@ com.github.digitaldan jomnilink - 1.4.0 + 1.4.2 compile diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java index 5ac9cf644dfd1..a17159463b54b 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java @@ -13,6 +13,7 @@ package org.openhab.binding.omnilink.internal; import java.util.Arrays; +import java.util.Optional; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -34,8 +35,7 @@ public enum SystemType { this.modelNumbers = Set.of(modelNumbers); } - public static SystemType getType(int modelNumber) { - return Arrays.stream(values()).filter(s -> s.modelNumbers.contains(modelNumber)).findFirst() - .orElseThrow(IllegalArgumentException::new); + public static Optional getType(int modelNumber) { + return Arrays.stream(values()).filter(s -> s.modelNumbers.contains(modelNumber)).findFirst(); } } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/TemperatureFormat.java similarity index 92% rename from bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java rename to bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/TemperatureFormat.java index 8379e0dfb2ef6..58f02afefec98 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/TemperatureFormat.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.omnilink.internal.handler; +package org.openhab.binding.omnilink.internal; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -33,7 +33,7 @@ public float omniToFormat(int omniNumber) { } @Override - public int formatToOmni(int celsius) { + public int formatToOmni(float celsius) { return MessageUtils.CToOmni(celsius); } }, @@ -44,7 +44,7 @@ public float omniToFormat(int omniNumber) { } @Override - public int formatToOmni(int fahrenheit) { + public int formatToOmni(float fahrenheit) { return MessageUtils.FtoOmni(fahrenheit); } }; @@ -69,7 +69,7 @@ private TemperatureFormat(int formatNumber) { * @param format Number in the current format. * @return Omni formatted number. */ - public abstract int formatToOmni(int format); + public abstract int formatToOmni(float format); /** * Get the number which identifies this format as defined by the omniprotocol. diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java index 7cf1dd8a2a65f..f44c8e12b6eba 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java @@ -17,7 +17,7 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.omnilink.internal.handler.BridgeOfflineException; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.binding.omnilink.internal.handler.OmnilinkBridgeHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java index 86ceadf7c6986..e796ad66112c4 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java @@ -21,11 +21,12 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.SystemType; -import org.openhab.binding.omnilink.internal.handler.BridgeOfflineException; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.binding.omnilink.internal.handler.OmnilinkBridgeHandler; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; @@ -62,7 +63,7 @@ public class OmnilinkDiscoveryService extends AbstractDiscoveryService private final Logger logger = LoggerFactory.getLogger(OmnilinkDiscoveryService.class); private static final int DISCOVER_TIMEOUT_SECONDS = 30; private @Nullable OmnilinkBridgeHandler bridgeHandler; - private @Nullable SystemType systemType; + private Optional systemType = Optional.empty(); private @Nullable List areas; /** @@ -389,7 +390,6 @@ private void discoverThermostats() { int thingNumber = areaProperties.getNumber(); String thingName = areaProperties.getName(); String thingID = Integer.toString(thingNumber); - ThingUID thingUID = null; /* * It seems that for simple OmniLink Controller configurations there @@ -405,27 +405,24 @@ private void discoverThermostats() { Map properties = Map.of(THING_PROPERTIES_NAME, thingName); - final SystemType systemType = this.systemType; - if (systemType != null) { - switch (systemType) { + final String name = thingName; + systemType.ifPresentOrElse(t -> { + ThingUID thingUID = null; + switch (t) { case LUMINA: thingUID = new ThingUID(THING_TYPE_LUMINA_AREA, bridgeUID, thingID); break; - case OMNI: - thingUID = new ThingUID(THING_TYPE_OMNI_AREA, bridgeUID, thingID); - break; default: - throw new IllegalStateException("Unknown System Type"); + thingUID = new ThingUID(THING_TYPE_OMNI_AREA, bridgeUID, thingID); } - } - - if (thingUID != null) { DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties) .withProperty(THING_PROPERTIES_NUMBER, thingID) - .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID) - .withLabel(thingName).build(); + .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID).withLabel(name) + .build(); thingDiscovered(discoveryResult); - } + }, () -> { + logger.warn("Unknown System Type"); + }); areas.add(areaProperties); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/exceptions/BridgeOfflineException.java similarity index 85% rename from bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java rename to bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/exceptions/BridgeOfflineException.java index 891d4c4a6994b..7b35721280d4c 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/exceptions/BridgeOfflineException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.omnilink.internal.handler; +package org.openhab.binding.omnilink.internal.exceptions; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -21,9 +21,8 @@ * @author Craig Hamilton - Initial contribution */ @NonNullByDefault +@SuppressWarnings("serial") public class BridgeOfflineException extends Exception { - private static final long serialVersionUID = -9081729691518514097L; - public BridgeOfflineException(Exception e) { super(e); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java index d0e6db0e43a09..1201d30b9b16c 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.omnilink.internal.AreaAlarm; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; @@ -94,7 +95,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java index f17f716a1cd32..587b253dc6b38 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.binding.BaseThingHandler; diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java index 01e5290a623be..e9a8707475daf 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java @@ -77,7 +77,7 @@ private void updateHandlerStatus() { if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) { updateStatus(ThingStatus.ONLINE); retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); } } } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java index 9846edf7d12da..e5501cc60be10 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java index d6b7be705051f..6a51b68f34f27 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java @@ -22,6 +22,7 @@ import org.openhab.binding.omnilink.internal.AudioPlayer; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.NextPreviousType; import org.openhab.core.library.types.OnOffType; @@ -37,6 +38,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioZoneProperties; import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAudioZoneStatus; @@ -90,14 +92,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } switch (channelUID.getId()) { case CHANNEL_AUDIO_ZONE_POWER: if (command instanceof OnOffType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_ON_MUTE.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_ON_AND_MUTE, OnOffType.OFF.equals(command) ? 0 : 1, thingID); } else { logger.debug("Invalid command: {}, must be OnOffType", command); @@ -105,7 +107,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_AUDIO_ZONE_MUTE: if (command instanceof OnOffType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_ON_MUTE.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_ON_AND_MUTE, OnOffType.OFF.equals(command) ? 2 : 3, thingID); } else { logger.debug("Invalid command: {}, must be OnOffType", command); @@ -113,16 +115,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_AUDIO_ZONE_VOLUME: if (command instanceof PercentType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_VOLUME.getNumber(), - ((PercentType) command).intValue(), thingID); + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_VOLUME, ((PercentType) command).intValue(), + thingID); } else { logger.debug("Invalid command: {}, must be PercentType", command); } break; case CHANNEL_AUDIO_ZONE_SOURCE: if (command instanceof DecimalType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(), - ((DecimalType) command).intValue(), thingID); + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_SOURCE, ((DecimalType) command).intValue(), + thingID); } else { logger.debug("Invalid command: {}, must be DecimalType", command); } @@ -149,7 +151,7 @@ private void handlePlayPauseCommand(ChannelUID channelUID, PlayPauseType command Optional audioPlayer = bridgeHandler.getAudioPlayer(); if (audioPlayer.isPresent()) { AudioPlayer player = audioPlayer.get(); - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_SOURCE, PlayPauseType.PLAY.equals(command) ? player.getPlayCommand() : player.getPauseCommand(), thingID); } else { @@ -168,7 +170,7 @@ private void handleNextPreviousCommand(ChannelUID channelUID, NextPreviousType c Optional audioPlayer = bridgeHandler.getAudioPlayer(); if (audioPlayer.isPresent()) { AudioPlayer player = audioPlayer.get(); - sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_AUDIO_ZONE_SET_SOURCE, NextPreviousType.NEXT.equals(command) ? player.getNextCommand() : player.getPreviousCommand(), thingID); } else { diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java index a288fa00a88f4..64e47498bd31c 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; import com.digitaldan.jomnilinkII.MessageTypes.properties.ButtonProperties; @@ -97,7 +98,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_BUTTON_PRESS: if (command instanceof OnOffType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_BUTTON.getNumber(), 0, thingID); + sendOmnilinkCommand(CommandMessage.CMD_BUTTON, 0, thingID); updateChannels(); } else { logger.debug("Invalid command: {}, must be OnOffType", command); diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java index 70340edb48bac..0afb46af4173f 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java @@ -27,6 +27,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; + /** * The {@link ConsoleHandler} defines some methods that are used to * interface with an OmniLink Console. This by extension also defines the @@ -67,7 +69,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER: if (command instanceof StringType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_ENABLE_DISABLE_BEEPER.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_ENABLE_DISABLE_BEEPER, ((StringType) command).equals(StringType.valueOf("OFF")) ? 0 : 1, thingID); } else { logger.debug("Invalid command: {}, must be StringType", command); @@ -75,8 +77,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_CONSOLE_BEEP: if (command instanceof DecimalType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_BEEP.getNumber(), ((DecimalType) command).intValue(), - thingID); + sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_BEEP, ((DecimalType) command).intValue(), thingID); } else { logger.debug("Invalid command: {}, must be DecimalType", command); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java index 6536175ca710a..4e865439ead0b 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java @@ -22,8 +22,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.omnilink.internal.TemperatureFormat; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; @@ -36,6 +38,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties; @@ -101,7 +104,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -112,13 +115,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_AUX_LOW_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(), - TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_HEAT_POINT, + TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).floatValue()), thingID); break; case CHANNEL_AUX_HIGH_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(), - TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_COOL_POINT, + TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).floatValue()), thingID); break; default: diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java index 92a20ec8e6d51..6c2bfa8f60b93 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AccessControlReaderProperties; import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAccessControlReaderLockStatus; @@ -85,15 +87,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } switch (channelUID.getId()) { case CHANNEL_LOCK_SWITCH: if (command instanceof OnOffType) { - sendOmnilinkCommand(OnOffType.OFF.equals(command) ? OmniLinkCmd.CMD_UNLOCK_DOOR.getNumber() - : OmniLinkCmd.CMD_LOCK_DOOR.getNumber(), 0, thingID); + sendOmnilinkCommand(OnOffType.OFF.equals(command) ? CommandMessage.CMD_UNLOCK_DOOR + : CommandMessage.CMD_LOCK_DOOR, 0, thingID); } else { logger.debug("Invalid command {}, must be OnOffType", command); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java index e560e0939e435..b7a626ddc5c2e 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java @@ -23,6 +23,8 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; + /** * The {@link LuminaAreaHandler} defines some methods that are used to * interface with an OmniLink Lumina Area. This by extension also defines the @@ -44,17 +46,17 @@ public LuminaAreaHandler(Thing thing) { protected int getMode(ChannelUID channelUID) { switch (channelUID.getId()) { case CHANNEL_AREA_SECURITY_MODE_HOME: - return OmniLinkCmd.CMD_SECURITY_LUMINA_HOME_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_HOME_MODE; case CHANNEL_AREA_SECURITY_MODE_SLEEP: - return OmniLinkCmd.CMD_SECURITY_LUMINA_SLEEP_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_SLEEP_MODE; case CHANNEL_AREA_SECURITY_MODE_AWAY: - return OmniLinkCmd.CMD_SECURITY_LUMINA_AWAY_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_AWAY_MODE; case CHANNEL_AREA_SECURITY_MODE_VACATION: - return OmniLinkCmd.CMD_SECURITY_LUMINA_VACATION_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_VACATION_MODE; case CHANNEL_AREA_SECURITY_MODE_PARTY: - return OmniLinkCmd.CMD_SECURITY_LUMINA_PARTY_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_PARTY_MODE; case CHANNEL_AREA_SECURITY_MODE_SPECIAL: - return OmniLinkCmd.CMD_SECURITY_LUMINA_SPECIAL_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_LUMINA_SPECIAL_MODE; default: throw new IllegalStateException("Unknown channel for area thing " + channelUID); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java index 7424d8d981e20..e16a198b17874 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java @@ -23,6 +23,8 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; + /** * The {@link OmniAreaHandler} defines some methods that are used to * interface with an OmniLink OmniPro Area. This by extension also defines the @@ -45,19 +47,19 @@ public OmniAreaHandler(Thing thing) { protected int getMode(ChannelUID channelUID) { switch (channelUID.getId()) { case CHANNEL_AREA_SECURITY_MODE_DISARM: - return OmniLinkCmd.CMD_SECURITY_OMNI_DISARM.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_DISARM; case CHANNEL_AREA_SECURITY_MODE_DAY: - return OmniLinkCmd.CMD_SECURITY_OMNI_DAY_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_DAY_MODE; case CHANNEL_AREA_SECURITY_MODE_NIGHT: - return OmniLinkCmd.CMD_SECURITY_OMNI_NIGHT_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_NIGHT_MODE; case CHANNEL_AREA_SECURITY_MODE_AWAY: - return OmniLinkCmd.CMD_SECURITY_OMNI_AWAY_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_AWAY_MODE; case CHANNEL_AREA_SECURITY_MODE_VACATION: - return OmniLinkCmd.CMD_SECURITY_OMNI_VACATION_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_VACATION_MODE; case CHANNEL_AREA_SECURITY_MODE_DAY_INSTANT: - return OmniLinkCmd.CMD_SECURITY_OMNI_DAY_INSTANCE_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_DAY_INSTANT_MODE; case CHANNEL_AREA_SECURITY_MODE_NIGHT_DELAYED: - return OmniLinkCmd.CMD_SECURITY_OMNI_NIGHT_DELAYED_MODE.getNumber(); + return CommandMessage.CMD_SECURITY_OMNI_NIGHT_DELAYED_MODE; default: throw new IllegalStateException("Unknown channel for area thing " + channelUID); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java deleted file mode 100644 index 20e3f7cba1fe0..0000000000000 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.omnilink.internal.handler; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * OmniLink commands - * - * @author Dan Cunningham - Initial contribution - * @author Ethan Dye - openHAB3 rewrite - * @since 1.5.0 - */ -@NonNullByDefault -public enum OmniLinkCmd { - CMD_UNIT_OFF(0), - CMD_UNIT_ON(1), - CMD_UNIT_AREA_ALL_OFF(2), - CMD_UNIT_AREA_ALL_ON(3), - CMD_UNIT_PERCENT(9), - CMD_UNIT_LO9_LEVEL_HIGH7(101), - CMD_UNIT_DECREMENT_COUNTER(10), - CMD_UNIT_INCREMENT_COUNTER(11), - CMD_UNIT_SET_COUNTER(12), - CMD_UNIT_LO9_RAMP_HIGH7(13), - CMD_UNIT_LIGHTOLIER(14), - CMD_UNIT_UPB_REQ_STATUS(15), - CMD_UNIT_UNIT_DIM_STEP_1(17), - CMD_UNIT_UNIT_DIM_STEP_2(18), - CMD_UNIT_UNIT_DIM_STEP_3(19), - CMD_UNIT_UNIT_DIM_STEP_4(20), - CMD_UNIT_UNIT_DIM_STEP_5(21), - CMD_UNIT_UNIT_DIM_STEP_6(22), - CMD_UNIT_UNIT_DIM_STEP_7(23), - CMD_UNIT_UNIT_DIM_STEP_8(24), - CMD_UNIT_UNIT_DIM_STEP_9(25), - CMD_UNIT_UNIT_BRIGHTEN_STEP_1(33), - CMD_UNIT_UNIT_BRIGHTEN_STEP_2(34), - CMD_UNIT_UNIT_BRIGHTEN_STEP_3(35), - CMD_UNIT_UNIT_BRIGHTEN_STEP_4(36), - CMD_UNIT_UNIT_BRIGHTEN_STEP_5(37), - CMD_UNIT_UNIT_BRIGHTEN_STEP_6(38), - CMD_UNIT_UNIT_BRIGHTEN_STEP_7(39), - CMD_UNIT_UNIT_BRIGHTEN_STEP_8(40), - CMD_UNIT_UNIT_BRIGHTEN_STEP_9(41), - CMD_UNIT_UPB_LO9_BLINK_HIGH7(26), - CMD_UNIT_UPB_STOP_BLINK(27), - CMD_UNIT_UPB_LINK_OFF(28), - CMD_UNIT_UPB_LINK_ON(29), - CMD_UNIT_UPB_LINK_SET(30), - CMD_UNIT_CENTRALITE_SCENE_OFF(42), - CMD_UNIT_CENTRALITE_SCENE_ON(43), - CMD_UNIT_UPB_LED_OFF(44), - CMD_UNIT_UPB_LED_ON(45), - CMD_UNIT_RADIORA_PHANTOM_BUTTON_OFF(46), - CMD_UNIT_RADIORA_PHANTM_BUTTON_ON(46), - CMD_UNIT_LEVITON_SCENE_OFF(60), - CMD_UNIT_LEVITON_SCENE_ON(61), - CMD_UNIT_LEVITON_SCENE_SET(62), - - CMD_SECURITY_OMNI_DISARM(48), - CMD_SECURITY_OMNI_DAY_MODE(49), - CMD_SECURITY_OMNI_NIGHT_MODE(50), - CMD_SECURITY_OMNI_AWAY_MODE(51), - CMD_SECURITY_OMNI_VACATION_MODE(52), - CMD_SECURITY_OMNI_DAY_INSTANCE_MODE(53), - CMD_SECURITY_OMNI_NIGHT_DELAYED_MODE(54), - CMD_SECURITY_BYPASS_ZONE(4), - CMD_SECURITY_RESTORE_ZONE(5), - CMD_SECURITY_RESTORE_ALL_ZONES(6), - CMD_SECURITY_LUMINA_HOME_MODE(49), - CMD_SECURITY_LUMINA_SLEEP_MODE(50), - CMD_SECURITY_LUMINA_AWAY_MODE(51), - CMD_SECURITY_LUMINA_VACATION_MODE(52), - CMD_SECURITY_LUMINA_PARTY_MODE(53), - CMD_SECURITY_LUMINA_SPECIAL_MODE(54), - - CMD_BUTTON(7), - - CMD_ENERGY_SAVER_OFF(64), - CMD_ENERGY_SAVER_ON(65), - - CMD_THERMO_SET_HEAT_LOW_POINT(66), - CMD_THERMO_SET_COOL_HIGH_POINT(67), - CMD_THERMO_SET_SYSTEM_MODE(68), - CMD_THERMO_SET_FAN_MODE(69), - CMD_THERMO_SET_HOLD_MODE(70), - CMD_THERMO_RAISE_LOWER_HEAT(71), - CMD_THERMO_RAISE_LOWER_COOL(72), - CMD_THERMO_SET_HUMDIFY_POINT(73), - CMD_THERMO_SET_DEHUMIDIFY_POINT(74), - - CMD_MESSAGE_SHOW_MESSAGE_WITH_BEEP_AND_LED(80), - CMD_MESSAGE_SHOW_MESSAGE_WITH_BEEP_OR_LED(86), - CMD_MESSAGE_LOG_MESSAGE(81), - CMD_MESSAGE_CLEAR_MESSAGE(82), - CMD_MESSAGE_SAY_MESSAGE(83), - CMD_MESSAGE_PHONE_AND_SAY_MESSAGE(84), - CMD_MESSAGE_SEND_MESSAGE_TO_SERIAL_PORT(85), - - CMD_CONSOLE_ENABLE_DISABLE_BEEPER(102), - CMD_CONSOLE_BEEP(103), - - CMD_LOCK_DOOR(105), - CMD_UNLOCK_DOOR(106), - - CMD_AUDIO_ZONE_SET_ON_MUTE(112), - CMD_AUDIO_ZONE_SET_VOLUME(113), - CMD_AUDIO_ZONE_SET_SOURCE(114), - CMD_AUDIO_ZONE_SELECT_KEY(115), - - SENSOR_UNIT_POWER(1001), - SENSOR_UNIT_LEVEL(1002), - SENSOR_UNIT_DISPLAY(1003), - SENSOR_THERMO_HEAT_POINTC(2001), - SENSOR_THERMO_HEAT_POINTF(2002), - SENSOR_THERMO_COOL_POINTC(2003), - SENSOR_THERMO_COOL_POINTF(2004), - SENSOR_THERMO_SYSTEM_MODE(2005), - SENSOR_THERMO_FAN_MODE(2006), - SENSOR_THERMO_HOLD_MODE(2007), - SENSOR_THERMO_TEMPC(2006), - SENSOR_THERMO_TEMPF(2007), - SENSOR_ZONE_STATUS_CURRENT(3001), - SENSOR_ZONE_STATUS_LATCHED(3002), - SENSOR_ZONE_STATUS_ARMING(3003), - SENSOR_AREA_STATUS_MODE(4001), - SENSOR_AREA_STATUS_ALARM(4002), - SENSOR_AREA_STATUS_EXIT_DELAY(4003), - SENSOR_AREA_STATUS_ENTRY_DELAY(4003), - SENSOR_AREA_EXIT_TIMER(4004), - SENSOR_AREA_ENTRY_TIMER(4005), - SENSOR_AUX_STATUS(5001), - SENSOR_AUX_CURRENTC(5002), - SENSOR_AUX_CURRENTF(5003), - SENSOR_AUX_LOWC(5004), - SENSOR_AUX_LOWF(5005), - SENSOR_AUX_HIGHC(5006), - SENSOR_AUX_HIGHF(5007), - SENSOR_AUDIOZONE_POWER(6001), - SENSOR_AUDIOZONE_SOURCE(6002), - SENSOR_AUDIOZONE_VOLUME(6003), - SENSOR_AUDIOZONE_MUTE(6004), - SENSOR_AUDIOZONE_TEXT(6005), - SENSOR_AUDIOZONE_TEXT_FIELD1(6006), - SENSOR_AUDIOZONE_TEXT_FIELD2(6007), - SENSOR_AUDIOZONE_TEXT_FIELD3(6008), - SENSOR_AUDIOSOURCE_TEXT(7001), - SENSOR_AUDIOSOURCE_TEXT_FIELD1(7002), - SENSOR_AUDIOSOURCE_TEXT_FIELD2(7003), - SENSOR_AUDIOSOURCE_TEXT_FIELD3(7004); - - private int number; - - OmniLinkCmd(int number) { - this.number = number; - } - - public int getNumber() { - return number; - } - - public static @Nullable OmniLinkCmd getCommand(String name) { - for (OmniLinkCmd command : OmniLinkCmd.values()) { - if (name.equals(command.toString())) { - return command; - } - } - return null; - } - - public static @Nullable OmniLinkCmd getCommand(int ordinal) { - OmniLinkCmd[] values = OmniLinkCmd.values(); - if (ordinal < values.length) { - return values[ordinal]; - } else { - return null; - } - } - - public static String toList() { - StringBuilder sb = new StringBuilder(); - for (OmniLinkCmd command : OmniLinkCmd.values()) { - sb.append(command.toString()).append(","); - } - return sb.toString(); - } -} diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java index 52232bab26e03..a0991bb9a5621 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java @@ -28,8 +28,10 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.AudioPlayer; import org.openhab.binding.omnilink.internal.SystemType; +import org.openhab.binding.omnilink.internal.TemperatureFormat; import org.openhab.binding.omnilink.internal.config.OmnilinkBridgeConfig; import org.openhab.binding.omnilink.internal.discovery.OmnilinkDiscoveryService; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; @@ -50,6 +52,7 @@ import com.digitaldan.jomnilinkII.Connection; import com.digitaldan.jomnilinkII.DisconnectListener; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.EventLogData; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation; @@ -92,7 +95,7 @@ public class OmnilinkBridgeHandler extends BaseBridgeHandler implements Notifica private @Nullable ScheduledFuture eventPollingJob; private final int autoReconnectPeriod = 60; private Optional audioPlayer = Optional.empty(); - private @Nullable SystemType systemType = null; + private Optional systemType = Optional.empty(); private final Gson gson = new Gson(); private int eventLogNumber = 0; @@ -194,7 +197,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { case CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER: if (command instanceof StringType) { try { - sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_ENABLE_DISABLE_BEEPER.getNumber(), + sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_ENABLE_DISABLE_BEEPER, ((StringType) command).equals(StringType.valueOf("OFF")) ? 0 : 1, 0); updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF); } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException @@ -208,8 +211,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { case CHANNEL_CONSOLE_BEEP: if (command instanceof DecimalType) { try { - sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_BEEP.getNumber(), - ((DecimalType) command).intValue(), 0); + sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_BEEP, ((DecimalType) command).intValue(), 0); updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF); } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) { @@ -306,28 +308,23 @@ public void objectStatusNotification(@Nullable ObjectStatus objectStatus) { theThing.map(Thing::getHandler) .ifPresent(theHandler -> ((ZoneHandler) theHandler).handleStatus(zoneStatus)); } else if (status instanceof ExtendedAreaStatus) { - final SystemType systemType = this.systemType; ExtendedAreaStatus areaStatus = (ExtendedAreaStatus) status; int areaNumber = areaStatus.getNumber(); - if (systemType != null) { - logger.debug("Received status update for Area: {}, status: {}", areaNumber, areaStatus); - Optional theThing; - switch (systemType) { - case OMNI: - theThing = getChildThing(THING_TYPE_OMNI_AREA, areaNumber); - break; + logger.debug("Received status update for Area: {}, status: {}", areaNumber, areaStatus); + systemType.ifPresent(t -> { + Optional theThing = Optional.empty(); + switch (t) { case LUMINA: theThing = getChildThing(THING_TYPE_LUMINA_AREA, areaNumber); break; - default: - theThing = Optional.empty(); + case OMNI: + theThing = getChildThing(THING_TYPE_OMNI_AREA, areaNumber); + break; } theThing.map(Thing::getHandler) .ifPresent(theHandler -> ((AbstractAreaHandler) theHandler).handleStatus(areaStatus)); - } else { - logger.debug("Received null System Type!"); - } + }); } else if (status instanceof ExtendedAccessControlReaderLockStatus) { ExtendedAccessControlReaderLockStatus lockStatus = (ExtendedAccessControlReaderLockStatus) status; int lockNumber = lockStatus.getNumber(); diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java index ddc4e1221560c..969527e91ac64 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java @@ -22,8 +22,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.omnilink.internal.TemperatureFormat; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; @@ -37,6 +39,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties; @@ -104,7 +107,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -125,13 +128,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_AUX_LOW_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(), - temperatureFormat.get().formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_HEAT_POINT, + temperatureFormat.get().formatToOmni(((QuantityType) command).floatValue()), thingID); break; case CHANNEL_AUX_HIGH_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(), - temperatureFormat.get().formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_COOL_POINT, + temperatureFormat.get().formatToOmni(((QuantityType) command).floatValue()), thingID); break; default: diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java index ee61ee2403231..df84bc9e0e1c1 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java @@ -24,8 +24,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.omnilink.internal.TemperatureFormat; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.QuantityType; @@ -42,6 +44,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; import com.digitaldan.jomnilinkII.MessageTypes.properties.ThermostatProperties; @@ -123,7 +126,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -144,35 +147,35 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_THERMO_SYSTEM_MODE: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_SYSTEM_MODE.getNumber(), - ((DecimalType) command).intValue(), thingID); + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_SYSTEM_MODE, ((DecimalType) command).intValue(), + thingID); break; case CHANNEL_THERMO_FAN_MODE: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_FAN_MODE.getNumber(), ((DecimalType) command).intValue(), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_FAN_MODE, ((DecimalType) command).intValue(), thingID); break; case CHANNEL_THERMO_HOLD_STATUS: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HOLD_MODE.getNumber(), - ((DecimalType) command).intValue(), thingID); + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_HOLD_MODE, ((DecimalType) command).intValue(), + thingID); break; case CHANNEL_THERMO_HEAT_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(), - temperatureFormat.get().formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_HEAT_POINT, + temperatureFormat.get().formatToOmni(((QuantityType) command).floatValue()), thingID); break; case CHANNEL_THERMO_COOL_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(), - temperatureFormat.get().formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_COOL_POINT, + temperatureFormat.get().formatToOmni(((QuantityType) command).floatValue()), thingID); break; case CHANNEL_THERMO_HUMIDIFY_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HUMDIFY_POINT.getNumber(), - TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_HUMDIFY_POINT, + TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).floatValue()), thingID); break; case CHANNEL_THERMO_DEHUMIDIFY_SETPOINT: - sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_DEHUMIDIFY_POINT.getNumber(), - TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).intValue()), + sendOmnilinkCommand(CommandMessage.CMD_THERMO_SET_DEHUMIDIFY_POINT, + TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType) command).floatValue()), thingID); break; default: diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java index 5370daefab440..379fc3c109d02 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; import com.digitaldan.jomnilinkII.MessageTypes.properties.UnitProperties; @@ -98,7 +100,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -144,15 +146,14 @@ private void handleUnitDuration(ChannelUID channelUID, DecimalType command) { return; } - sendOmnilinkCommand( - channelID.startsWith("on") ? OmniLinkCmd.CMD_UNIT_ON.getNumber() : OmniLinkCmd.CMD_UNIT_OFF.getNumber(), + sendOmnilinkCommand(channelID.startsWith("on") ? CommandMessage.CMD_UNIT_ON : CommandMessage.CMD_UNIT_OFF, duration, thingID); } protected void handleOnOff(ChannelUID channelUID, OnOffType command) { logger.debug("handleOnOff called for channel: {}, command: {}", channelUID, command); - sendOmnilinkCommand(OnOffType.ON.equals(command) ? OmniLinkCmd.CMD_UNIT_ON.getNumber() - : OmniLinkCmd.CMD_UNIT_OFF.getNumber(), 0, thingID); + sendOmnilinkCommand(OnOffType.ON.equals(command) ? CommandMessage.CMD_UNIT_ON : CommandMessage.CMD_UNIT_OFF, 0, + thingID); } @Override diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java index 572de0b4ce6ea..8d07e9228716d 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest; import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests; +import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.StringType; @@ -38,6 +39,7 @@ import org.slf4j.LoggerFactory; import com.digitaldan.jomnilinkII.Message; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus; import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation; import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties; @@ -104,7 +106,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -115,10 +117,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_ZONE_BYPASS: - mode = OmniLinkCmd.CMD_SECURITY_BYPASS_ZONE.getNumber(); + mode = CommandMessage.CMD_SECURITY_BYPASS_ZONE; break; case CHANNEL_ZONE_RESTORE: - mode = OmniLinkCmd.CMD_SECURITY_RESTORE_ZONE.getNumber(); + mode = CommandMessage.CMD_SECURITY_RESTORE_ZONE; break; default: mode = -1; diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java index 632197f0852fd..c4a6ecdbc6cc7 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java @@ -16,7 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd; import org.openhab.binding.omnilink.internal.handler.UnitHandler; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; @@ -27,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; + /** * The {@link DimmableUnitHandler} defines some methods that are used to * interface with an OmniLink Dimmable Unit. This by extension also defines the @@ -79,15 +80,13 @@ private void handlePercent(ChannelUID channelUID, PercentType command) { } else if (lightLevel == 100) { super.handleOnOff(channelUID, OnOffType.ON); } else { - sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_PERCENT.getNumber(), lightLevel, thingID); + sendOmnilinkCommand(CommandMessage.CMD_UNIT_PERCENT, lightLevel, thingID); } } private void handleIncreaseDecrease(ChannelUID channelUID, IncreaseDecreaseType command) { logger.debug("handleIncreaseDecrease called for channel: {}, command: {}", channelUID, command); - sendOmnilinkCommand( - IncreaseDecreaseType.INCREASE.equals(command) ? OmniLinkCmd.CMD_UNIT_UNIT_BRIGHTEN_STEP_1.getNumber() - : OmniLinkCmd.CMD_UNIT_UNIT_DIM_STEP_1.getNumber(), - 0, thingID); + sendOmnilinkCommand(IncreaseDecreaseType.INCREASE.equals(command) ? CommandMessage.CMD_UNIT_UPB_BRIGHTEN_STEP_1 + : CommandMessage.CMD_UNIT_UPB_DIM_STEP_1, 0, thingID); } } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java index 56874e767ff5b..87b8199035027 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java @@ -16,7 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd; import org.openhab.binding.omnilink.internal.handler.UnitHandler; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -29,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus; /** @@ -55,15 +55,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } switch (channelUID.getId()) { case CHANNEL_FLAG_VALUE: if (command instanceof DecimalType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_SET_COUNTER.getNumber(), - ((DecimalType) command).intValue(), thingID); + sendOmnilinkCommand(CommandMessage.CMD_UNIT_SET_COUNTER, ((DecimalType) command).intValue(), + thingID); } else { logger.debug("Invalid command: {}, must be DecimalType", command); } diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java index f59344f34ef5a..e92458527fa88 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java @@ -16,7 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd; import org.openhab.binding.omnilink.internal.handler.UnitHandler; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -29,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus; /** @@ -55,7 +55,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE, - ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!")); + ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null status update!")); return; } @@ -113,8 +113,8 @@ private void handleRoomScene(ChannelUID channelUID, OnOffType command) { } int roomNum = (thingID + 7) / 8; int param2 = ((roomNum * 6) - 3) + linkNum; - sendOmnilinkCommand(OnOffType.ON.equals(command) ? OmniLinkCmd.CMD_UNIT_UPB_LINK_ON.getNumber() - : OmniLinkCmd.CMD_UNIT_UPB_LINK_OFF.getNumber(), 0, param2); + sendOmnilinkCommand(OnOffType.ON.equals(command) ? CommandMessage.CMD_UNIT_UPB_LINK_ON + : CommandMessage.CMD_UNIT_UPB_LINK_OFF, 0, param2); } private void handleRoomState(ChannelUID channelUID, DecimalType command) { @@ -125,18 +125,18 @@ private void handleRoomState(ChannelUID channelUID, DecimalType command) { switch (cmdValue) { case 0: - cmd = OmniLinkCmd.CMD_UNIT_OFF.getNumber(); + cmd = CommandMessage.CMD_UNIT_OFF; param2 = thingID; break; case 1: - cmd = OmniLinkCmd.CMD_UNIT_ON.getNumber(); + cmd = CommandMessage.CMD_UNIT_ON; param2 = thingID; break; case 2: case 3: case 4: case 5: - cmd = OmniLinkCmd.CMD_UNIT_UPB_LINK_ON.getNumber(); + cmd = CommandMessage.CMD_UNIT_UPB_LINK_ON; /* * A little magic with the link #'s: 0 and 1 are off and on, respectively. * So A ends up being 2, but OmniLink Protocol expects an offset of 0. That diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java index 2fda1ced90bcb..c128b12cec3ca 100644 --- a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java +++ b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java @@ -16,7 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd; import org.openhab.binding.omnilink.internal.handler.units.DimmableUnitHandler; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; @@ -27,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage; + /** * The {@link UpbUnitHandler} defines some methods that are used to * interface with an OmniLink UPB Unit. This by extension also defines the @@ -57,7 +58,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_UPB_STATUS: if (command instanceof StringType) { - sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_UPB_REQ_STATUS.getNumber(), 0, thingID); + sendOmnilinkCommand(CommandMessage.CMD_UNIT_UPB_REQ_STATUS, 0, thingID); updateState(CHANNEL_UPB_STATUS, UnDefType.UNDEF); } else { logger.debug("Invalid command: {}, must be StringType", command); diff --git a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ApiConfiguration.java b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ApiConfiguration.java index 0425bac2a543b..5309241591068 100644 --- a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ApiConfiguration.java +++ b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ApiConfiguration.java @@ -14,7 +14,7 @@ import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * The {@link ApiConfiguration} defines the model for a API bridge configuration. diff --git a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ChannelConfig.java b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ChannelConfig.java index e5e5491086d50..d1158c6da96ef 100644 --- a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ChannelConfig.java +++ b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/ChannelConfig.java @@ -14,7 +14,7 @@ import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.CHANNEL_CONFIG_OFFSET; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * The {@link ChannelConfig} defines the model for a channel configuration. diff --git a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/RouteConfiguration.java b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/RouteConfiguration.java index 965a5585492db..a4be8d16ba3b3 100644 --- a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/RouteConfiguration.java +++ b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/RouteConfiguration.java @@ -14,7 +14,7 @@ import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.ROUTE_CONFIG_ROUTE_ID; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * The {@link RouteConfiguration} defines the model for a route stop configuration. diff --git a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/StopConfiguration.java b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/StopConfiguration.java index b799b823f8e90..488c00d005c20 100644 --- a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/StopConfiguration.java +++ b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/config/StopConfiguration.java @@ -14,7 +14,7 @@ import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * The {@link StopConfiguration} defines the model for a stop bridge configuration. diff --git a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/config/config.xml index 994537637ab67..4b7b222725bad 100644 --- a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/config/config.xml @@ -4,33 +4,29 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + Sensor ID in format: xx.xxxxxxxxxxxx or a full path including hubs/branches - true - + Time in seconds after which the thing is refreshed 300 s - false - + Sensor ID in format: xx.xxxxxxxxxxxx or a full path including hubs/branches - true - + Time in seconds after which the thing is refreshed 300 s - false - + Overrides detected sensor type @@ -41,7 +37,6 @@ true - false true diff --git a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bae09x.xml b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bae09x.xml index 38973babbbab5..79e2d72a2c58b 100644 --- a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bae09x.xml +++ b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bae09x.xml @@ -13,10 +13,9 @@ 1 - + Sensor ID in format: xx.xxxxxxxxxxxx) - true diff --git a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bridge.xml index 0c0f85075e227..b0f5501b146b5 100644 --- a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/bridge.xml @@ -7,17 +7,15 @@ An owserver instance - + network_address Network address of the host running the owserver - true - + Listening port of the owserver 4304 - false @@ -27,17 +25,15 @@ Allows direct access to the OWFS - + full path to the OWFS-node (e.g. statistics/errors/CRC8_errors) - true - + Time in seconds after which the channel is refreshed 300 s - false @@ -47,17 +43,15 @@ Allows direct access to the OWFS - + full path to the OWFS-node (e.g. statistics/errors/CRC8_errors) - true - + Time in seconds after which the channel is refreshed 300 s - false diff --git a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/common.xml b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/common.xml index 482c37f3eb520..762237d3599ac 100644 --- a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/common.xml +++ b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/common.xml @@ -23,11 +23,10 @@ temperature value of this sensor - + filters all 85°C readings (POR-value), may suppress valid readings if enabled false - false @@ -37,13 +36,12 @@ temperature value of this sensor - + filters all 85°C readings (POR-value), may suppress valid readings if enabled false - false - + @@ -53,7 +51,6 @@ 10 true - false @@ -77,7 +74,7 @@ relative humidity (0-100%) - + true @@ -87,7 +84,6 @@ /humidity - false diff --git a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/multisensor.xml b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/multisensor.xml index 3af91defdcf0e..7d65617e271ad 100644 --- a/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/multisensor.xml +++ b/bundles/org.openhab.binding.onewire/src/main/resources/OH-INF/thing/multisensor.xml @@ -18,19 +18,17 @@ 1-wire multisensor (DS2438-based) - + Sensor ID of the DS2438 sensor in format: xx.xxxxxxxxxxxx or a full path including hubs/branches - true - + Time in seconds after which the thing is refreshed 300 s - false - + @@ -38,7 +36,6 @@ DS2438 true - false @@ -49,26 +46,23 @@ 1-wire multisensor (DS2438-based) - + Sensor ID of the DS2438 sensor in format: xx.xxxxxxxxxxxx or a full path including hubs/branches - true - + Time in seconds after which the thing is refreshed 300 s - false - + Time in seconds after which the digital I/Os are refreshed 10 s - false - + @@ -76,7 +70,6 @@ DS2438 true - false @@ -90,17 +83,15 @@ 1 - + Sensor ID in format: xx.xxxxxxxxxxxx) - true - + Time in seconds after which the thing is refreshed 300 s - false diff --git a/bundles/org.openhab.binding.onewiregpio/src/main/java/org/openhab/binding/onewiregpio/internal/handler/OneWireGPIOHandler.java b/bundles/org.openhab.binding.onewiregpio/src/main/java/org/openhab/binding/onewiregpio/internal/handler/OneWireGPIOHandler.java index bbc8548c9a8bb..7ee519735d57c 100644 --- a/bundles/org.openhab.binding.onewiregpio/src/main/java/org/openhab/binding/onewiregpio/internal/handler/OneWireGPIOHandler.java +++ b/bundles/org.openhab.binding.onewiregpio/src/main/java/org/openhab/binding/onewiregpio/internal/handler/OneWireGPIOHandler.java @@ -26,7 +26,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.onewiregpio.internal.OneWireGPIOBindingConstants; import org.openhab.binding.onewiregpio.internal.OneWireGpioConfiguration; import org.openhab.core.library.types.QuantityType; @@ -94,7 +93,7 @@ public void initialize() { * When invalid parameter is found, default value is assigned. */ private boolean checkConfiguration() { - if (StringUtils.isEmpty(gpioBusFile)) { + if (gpioBusFile == null || gpioBusFile.isEmpty()) { logger.debug("GPIO_BUS_FILE not set. Please check configuration, and set proper path to w1_slave file."); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "The path to the w1_slave sensor data file is missing."); diff --git a/bundles/org.openhab.binding.onewiregpio/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.onewiregpio/src/main/resources/OH-INF/thing/thing-types.xml index 2f4278b99dbe9..25c418b627e20 100644 --- a/bundles/org.openhab.binding.onewiregpio/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.onewiregpio/src/main/resources/OH-INF/thing/thing-types.xml @@ -26,7 +26,6 @@ Sensor precision after floating point. - false 3 diff --git a/bundles/org.openhab.binding.onkyo/src/main/java/org/openhab/binding/onkyo/internal/discovery/OnkyoUpnpDiscoveryParticipant.java b/bundles/org.openhab.binding.onkyo/src/main/java/org/openhab/binding/onkyo/internal/discovery/OnkyoUpnpDiscoveryParticipant.java index 280b930351085..a77136443604b 100644 --- a/bundles/org.openhab.binding.onkyo/src/main/java/org/openhab/binding/onkyo/internal/discovery/OnkyoUpnpDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.onkyo/src/main/java/org/openhab/binding/onkyo/internal/discovery/OnkyoUpnpDiscoveryParticipant.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.Set; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.jupnp.model.meta.RemoteDevice; @@ -64,7 +64,7 @@ public OnkyoUpnpDiscoveryParticipant() { protected void activate(ComponentContext componentContext) { if (componentContext.getProperties() != null) { String autoDiscoveryPropertyValue = (String) componentContext.getProperties().get("enableAutoDiscovery"); - if (StringUtils.isNotEmpty(autoDiscoveryPropertyValue)) { + if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isEmpty()) { isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue); } } @@ -81,8 +81,8 @@ public Set getSupportedThingTypeUIDs() { DiscoveryResult result = null; ThingUID thingUid = getThingUID(device); if (thingUid != null) { - String label = StringUtils.isEmpty(device.getDetails().getFriendlyName()) ? device.getDisplayString() - : device.getDetails().getFriendlyName(); + String friendlyName = device.getDetails().getFriendlyName(); + String label = friendlyName == null || friendlyName.isEmpty() ? device.getDisplayString() : friendlyName; Map properties = new HashMap<>(2, 1); properties.put(HOST_PARAMETER, device.getIdentity().getDescriptorURL().getHost()); properties.put(UDN_PARAMETER, device.getIdentity().getUdn().getIdentifierString()); @@ -97,13 +97,12 @@ public Set getSupportedThingTypeUIDs() { public @Nullable ThingUID getThingUID(RemoteDevice device) { ThingUID result = null; if (isAutoDiscoveryEnabled) { - if (StringUtils.containsIgnoreCase(device.getDetails().getManufacturerDetails().getManufacturer(), - MANUFACTURER)) { - logger.debug("Manufacturer matched: search: {}, device value: {}.", MANUFACTURER, - device.getDetails().getManufacturerDetails().getManufacturer()); - if (StringUtils.containsIgnoreCase(device.getType().getType(), UPNP_DEVICE_TYPE)) { - logger.debug("Device type matched: search: {}, device value: {}.", UPNP_DEVICE_TYPE, - device.getType().getType()); + String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer(); + if (manufacturer != null && manufacturer.toLowerCase().contains(MANUFACTURER.toLowerCase())) { + logger.debug("Manufacturer matched: search: {}, device value: {}.", MANUFACTURER, manufacturer); + String type = device.getType().getType(); + if (type != null && type.toLowerCase().contains(UPNP_DEVICE_TYPE.toLowerCase())) { + logger.debug("Device type matched: search: {}, device value: {}.", UPNP_DEVICE_TYPE, type); String deviceModel = device.getDetails().getModelDetails() != null ? device.getDetails().getModelDetails().getModelName() @@ -143,7 +142,7 @@ private ThingTypeUID findThingType(@Nullable String deviceModel) { * @return */ private boolean isSupportedDeviceModel(final @Nullable String deviceModel) { - return StringUtils.isNotBlank(deviceModel) && Arrays.stream(OnkyoModel.values()) + return deviceModel != null && !deviceModel.isBlank() && Arrays.stream(OnkyoModel.values()) .anyMatch(model -> StringUtils.startsWithIgnoreCase(deviceModel, model.getId())); } } diff --git a/bundles/org.openhab.binding.onkyo/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.onkyo/src/main/resources/OH-INF/binding/binding.xml index e65c540aa1f7a..9c486c685ec37 100644 --- a/bundles/org.openhab.binding.onkyo/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.onkyo/src/main/resources/OH-INF/binding/binding.xml @@ -10,7 +10,6 @@ url to use for playing notification sounds, e.g. http://192.168.0.2:8080 - false diff --git a/bundles/org.openhab.binding.opengarage/src/main/java/org/openhab/binding/opengarage/internal/api/ControllerVariables.java b/bundles/org.openhab.binding.opengarage/src/main/java/org/openhab/binding/opengarage/internal/api/ControllerVariables.java index e048a7e5a1f04..14c7d1e9f3109 100644 --- a/bundles/org.openhab.binding.opengarage/src/main/java/org/openhab/binding/opengarage/internal/api/ControllerVariables.java +++ b/bundles/org.openhab.binding.opengarage/src/main/java/org/openhab/binding/opengarage/internal/api/ControllerVariables.java @@ -43,7 +43,7 @@ private ControllerVariables() { public static ControllerVariables parse(String response) { LOGGER.debug("Parsing string: \"{}\"", response); /* parse json string */ - JsonObject jsonObject = new JsonParser().parse(response).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject(); ControllerVariables info = new ControllerVariables(); info.dist = jsonObject.get("dist").getAsInt(); info.door = jsonObject.get("door").getAsInt(); diff --git a/bundles/org.openhab.binding.opensprinkler/src/main/java/org/openhab/binding/opensprinkler/internal/util/Parse.java b/bundles/org.openhab.binding.opensprinkler/src/main/java/org/openhab/binding/opensprinkler/internal/util/Parse.java index dccc437a5487a..6fffab23fe03e 100644 --- a/bundles/org.openhab.binding.opensprinkler/src/main/java/org/openhab/binding/opensprinkler/internal/util/Parse.java +++ b/bundles/org.openhab.binding.opensprinkler/src/main/java/org/openhab/binding/opensprinkler/internal/util/Parse.java @@ -27,8 +27,6 @@ * @author Chris Graham - Initial contribution */ public class Parse { - private static JsonParser jsonParser = new JsonParser(); - /** * Parses an integer from a JSON string given its key name. * @@ -37,7 +35,7 @@ public class Parse { * @return int value of the objects data. */ public static int jsonInt(String jsonData, String keyName) { - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); return jobject.get(keyName).getAsInt(); } @@ -50,7 +48,7 @@ public static int jsonInt(String jsonData, String keyName) { * @return String value of the objects data. */ public static String jsonString(String jsonData, String keyName) { - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); return jobject.get(keyName).getAsString(); } @@ -64,7 +62,7 @@ public static String jsonString(String jsonData, String keyName) { * @return int value of the objects data. */ public static int jsonIntAtArrayIndex(String jsonData, String keyName, int index) { - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); JsonArray jarray = jobject.get(keyName).getAsJsonArray(); return jarray.get(index).getAsInt(); @@ -79,7 +77,7 @@ public static int jsonIntAtArrayIndex(String jsonData, String keyName, int index * @return String value of the objects data. */ public static String jsonStringAtArrayIndex(String jsonData, String keyName, int index) { - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); JsonArray jarray = jobject.get(keyName).getAsJsonArray(); return jarray.get(index).getAsString(); @@ -95,7 +93,7 @@ public static String jsonStringAtArrayIndex(String jsonData, String keyName, int public static List jsonIntArray(String jsonData, String keyName) { List returnList = new ArrayList<>(); - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); JsonArray jarray = jobject.get(keyName).getAsJsonArray(); @@ -116,7 +114,7 @@ public static List jsonIntArray(String jsonData, String keyName) { public static List jsonStringArray(String jsonData, String keyName) { List returnList = new ArrayList<>(); - JsonElement jelement = jsonParser.parse(jsonData); + JsonElement jelement = JsonParser.parseString(jsonData); JsonObject jobject = jelement.getAsJsonObject(); JsonArray jarray = jobject.get(keyName).getAsJsonArray(); diff --git a/bundles/org.openhab.binding.openthermgateway/README.md b/bundles/org.openhab.binding.openthermgateway/README.md index 3d829137c0bd8..006cd104d367a 100644 --- a/bundles/org.openhab.binding.openthermgateway/README.md +++ b/bundles/org.openhab.binding.openthermgateway/README.md @@ -71,7 +71,16 @@ The OpenTherm Gateway binding supports the following channels: | airpressfault | Switch | Air pressure fault | R | | waterovtemp | Switch | Water over-temperature fault | R | | oemfaultcode | Switch | OEM fault code | R | -| diag | Switch | Diagr / wstics indication | R | +| diag | Switch | Diagnostics indication | R | +| unsuccessfulburnerstarts | Number:Dimensionless | Unsuccessful burner starts | R | +| burnerstarts | Number:Dimensionless | Burner starts | R | +| chpumpstarts | Number:Dimensionless | Central heating pump starts | R | +| dhwpvstarts | Number:Dimensionless | Domestic hot water pump/valve starts | R | +| dhwburnerstarts | Number:Dimensionless | Domestic hot water burner starts | R | +| burnerhours | Number:Dimensionless | Burner hours | R | +| chpumphours | Number:Dimensionless | Central heating pump hours | R | +| dhwpvhours | Number:Dimensionless | Domestic hot water pump/valve hours | R | +| dhwburnerhours | Number:Dimensionless | Domestic hot water burner hours | R | | sendcommand | Text | Channel to send commands to the OpenTherm Gateway device | W | ## Full Example @@ -123,6 +132,15 @@ Switch AirPressFault "Air pressure fault" { channel="openthermgateway:o Switch WaterOvTemp "Water over-temperature fault" { channel="openthermgateway:otgw:1:waterovtemp" } Number OemFaultCode "OEM fault code" { channel="openthermgateway:otgw:1:oemfaultcode" } Switch Diagnostics "Diagnostics indication" { channel="openthermgateway:otgw:1:diag" } +Number:Dimensionless UnsuccessfulBurnerStarts "Unsuccessful burner starts" { channel="openthermgateway:otgw:1:unsuccessfulburnerstarts" } +Number:Dimensionless BurnerStarts "Burner starts" { channel="openthermgateway:otgw:1:burnerstarts" } +Number:Dimensionless CentralHeatingPumpStarts "Central heating pump starts" { channel="openthermgateway:otgw:1:chpumpstarts" } +Number:Dimensionless DomesticHotWaterPumpValveStarts "Domestic hot water pump/valve starts" { channel="openthermgateway:otgw:1:dhwpvstarts" } +Number:Dimensionless DomesticHotWaterBurnerStarts "Domestic hot water burner starts" { channel="openthermgateway:otgw:1:dhwburnerstarts" } +Number:Time BurnerHours "Burner hours" { channel="openthermgateway:otgw:1:burnerhours" } +Number:Time CentralHeatingPumpHours "Central heating pump hours" { channel="openthermgateway:otgw:1:chpumphours" } +Number:Time DomesticHotWaterPumpValveHours "Domestic hot water pump/valve hours" { channel="openthermgateway:otgw:1:dhwpvhours" } +Number:Time DomesticHotWaterBurnerHours "Domestic hot water burner hours" { channel="openthermgateway:otgw:1:dhwburnerhours" } Text SendCommand "Send command channel" { channel="openthermgateway:otgw:1:sendcommand" } ``` @@ -163,6 +181,15 @@ sitemap demo label="Main Menu" { Switch item="waterOvTemp" icon="" label="Water over-temperature fault" Text item="OemFaultCode" icon="" label="OEM fault code" Switch item="Diagnostics" icon="" label="Diagnostics indication" + Text item="UnsuccessfulBurnerStarts" icon="" label="Unsuccessful burner starts" + Text item="BurnerStarts" icon="" label="Burner starts" + Text item="CentralHeatingPumpStarts" icon="" label="Central heating pump starts" + Text item="DomesticHotWaterPumpValveStarts" icon="" label="Domestic hot water pump/valve starts" + Text item="DomesticHotWaterBurnerStarts" icon="" label="Domestic hot water burner starts" + Text item="BurnerHours" icon="" label="Burner hours" + Text item="CentralHeatingPumpHours" icon="" label="Central heating pump hours" + Text item="DomesticHotWaterPumpValveHours" icon="" label="Domestic hot water pump/valve hours" + Text item="DomesticHotWaterBurnerHours" icon="" label="Domestic hot water burner hours" } } ``` diff --git a/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/OpenThermGatewayBindingConstants.java b/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/OpenThermGatewayBindingConstants.java index 1ed28ae4d4695..c45bc3dfed481 100644 --- a/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/OpenThermGatewayBindingConstants.java +++ b/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/OpenThermGatewayBindingConstants.java @@ -72,6 +72,15 @@ public class OpenThermGatewayBindingConstants { public static final String CHANNEL_WATER_OVER_TEMP = "waterovtemp"; public static final String CHANNEL_OEM_FAULTCODE = "oemfaultcode"; public static final String CHANNEL_DIAGNOSTICS_INDICATION = "diag"; + public static final String CHANNEL_UNSUCCESSFUL_BURNER_STARTS = "unsuccessfulburnerstarts"; + public static final String CHANNEL_BURNER_STARTS = "burnerstarts"; + public static final String CHANNEL_CH_PUMP_STARTS = "chpumpstarts"; + public static final String CHANNEL_DHW_PV_STARTS = "dhwpvstarts"; + public static final String CHANNEL_DHW_BURNER_STARTS = "dhwburnerstarts"; + public static final String CHANNEL_BURNER_HOURS = "burnerhours"; + public static final String CHANNEL_CH_PUMP_HOURS = "chpumphours"; + public static final String CHANNEL_DHW_PV_HOURS = "dhwpvhours"; + public static final String CHANNEL_DHW_BURNER_HOURS = "dhwburnerhours"; public static final Set SUPPORTED_CHANNEL_IDS = Set.of(CHANNEL_ROOM_TEMPERATURE, CHANNEL_ROOM_SETPOINT, CHANNEL_FLOW_TEMPERATURE, CHANNEL_RETURN_TEMPERATURE, CHANNEL_OUTSIDE_TEMPERATURE, @@ -86,5 +95,7 @@ public class OpenThermGatewayBindingConstants { CHANNEL_DOMESTIC_HOT_WATER_SETPOINT, CHANNEL_FLAME, CHANNEL_RELATIVE_MODULATION_LEVEL, CHANNEL_MAXIMUM_MODULATION_LEVEL, CHANNEL_FAULT, CHANNEL_SERVICEREQUEST, CHANNEL_REMOTE_RESET, CHANNEL_LOW_WATER_PRESSURE, CHANNEL_GAS_FLAME_FAULT, CHANNEL_AIR_PRESSURE_FAULT, CHANNEL_WATER_OVER_TEMP, - CHANNEL_OEM_FAULTCODE, CHANNEL_DIAGNOSTICS_INDICATION); + CHANNEL_OEM_FAULTCODE, CHANNEL_DIAGNOSTICS_INDICATION, CHANNEL_UNSUCCESSFUL_BURNER_STARTS, + CHANNEL_BURNER_STARTS, CHANNEL_CH_PUMP_STARTS, CHANNEL_DHW_PV_STARTS, CHANNEL_DHW_BURNER_STARTS, + CHANNEL_BURNER_HOURS, CHANNEL_CH_PUMP_HOURS, CHANNEL_DHW_PV_HOURS, CHANNEL_DHW_BURNER_HOURS); } diff --git a/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/internal/DataItemGroup.java b/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/internal/DataItemGroup.java index 8932d63864aa9..2fbcf5afe6339 100644 --- a/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/internal/DataItemGroup.java +++ b/bundles/org.openhab.binding.openthermgateway/src/main/java/org/openhab/binding/openthermgateway/internal/DataItemGroup.java @@ -172,16 +172,22 @@ private static Map createDataItemGroups() { new DataItem(100, Msg.READ, ByteType.HIGHBYTE, DataType.FLAGS, 5, "rof5"), new DataItem(100, Msg.READ, ByteType.HIGHBYTE, DataType.FLAGS, 6, "rof6"), new DataItem(100, Msg.READ, ByteType.HIGHBYTE, DataType.FLAGS, 7, "rof7") }); + g.put(113, new DataItem[] { + new DataItem(113, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "unsuccessfulburnerstarts") }); g.put(115, new DataItem[] { new DataItem(115, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "oemdiagcode") }); g.put(116, new DataItem[] { new DataItem(116, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "burnerstarts") }); g.put(117, new DataItem[] { new DataItem(117, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "chpumpstarts") }); g.put(118, new DataItem[] { new DataItem(118, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwpvstarts") }); g.put(119, new DataItem[] { new DataItem(119, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwburnerstarts") }); - g.put(120, new DataItem[] { new DataItem(120, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "burnerhours") }); - g.put(121, new DataItem[] { new DataItem(121, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "chpumphours") }); - g.put(122, new DataItem[] { new DataItem(122, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwpvhours") }); - g.put(123, new DataItem[] { new DataItem(123, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwburnerhours") }); + g.put(120, new DataItem[] { + new DataItem(120, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "burnerhours", Units.HOUR) }); + g.put(121, new DataItem[] { + new DataItem(121, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "chpumphours", Units.HOUR) }); + g.put(122, new DataItem[] { + new DataItem(122, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwpvhours", Units.HOUR) }); + g.put(123, new DataItem[] { + new DataItem(123, Msg.READ, ByteType.BOTH, DataType.UINT16, 0, "dhwburnerhours", Units.HOUR) }); g.put(124, new DataItem[] { new DataItem(124, Msg.WRITE, ByteType.BOTH, DataType.FLOAT, 0, "masterotversion") }); g.put(125, new DataItem[] { new DataItem(125, Msg.READ, ByteType.BOTH, DataType.FLOAT, 0, "slaveotversion") }); diff --git a/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/channels.xml index 43a9182ac4d89..5d9700ca3b0b3 100644 --- a/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/channels.xml @@ -286,6 +286,69 @@ + + Number:Dimensionless + + Unsuccessful burner starts + + + + + Number:Dimensionless + + Burner starts + + + + + Number:Dimensionless + + Central heating pump starts + + + + + Number:Dimensionless + + Domestic hot water pump/valve starts + + + + + Number:Dimensionless + + Domestic hot water burner starts + + + + + Number:Time + + Burner hours + + + + + Number:Time + + Central heating pump hours + + + + + Number:Time + + Domestic hot water pump/valve hours + + + + + Number:Time + + Domestic hot water burner hours + + + String diff --git a/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/otgw.xml b/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/otgw.xml index 6d29d1981c154..373527a1c784c 100644 --- a/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/otgw.xml +++ b/bundles/org.openhab.binding.openthermgateway/src/main/resources/OH-INF/thing/otgw.xml @@ -44,10 +44,19 @@ + + + + + + + + + - 1.2.0 + 1.3.0 diff --git a/bundles/org.openhab.binding.openweathermap/README.md b/bundles/org.openhab.binding.openweathermap/README.md index 59688934a34f1..00e49e47ba43e 100644 --- a/bundles/org.openhab.binding.openweathermap/README.md +++ b/bundles/org.openhab.binding.openweathermap/README.md @@ -10,7 +10,7 @@ This binding integrates the [OpenWeatherMap weather API](https://openweathermap. ## Supported Things -There are five supported things. +There are six supported things. ### OpenWeatherMap Account @@ -29,10 +29,22 @@ If the request fails, all daily forecast channel groups will be removed from the ### Current UV Index And Forecast +::: tip Note +The product will retire on 1st April 2021, please find UV data in the One Call API. +One Call API includes current, hourly forecast for 7 days and 5 days historical UV data. +::: + The third thing `uvindex` supports the [current UV Index](https://openweathermap.org/api/uvi#current) and [forecasted UV Index](https://openweathermap.org/api/uvi#forecast) for a specific location. It requires coordinates of the location of your interest. You can add as much `uvindex` things for different locations to your setup as you like to observe. +### Current And Forecasted Air Pollution + +Another thing is the `air-pollution` which provides the [current air pollution](https://openweathermap.org/api/air-pollution) and [forecasted air pollution](https://openweathermap.org/api/air-pollution#forecast) for a specific location. +It requires coordinates of the location of your interest. +Air pollution forecast is available for 5 days with hourly granularity. +You can add as much `air-pollution` things for different locations to your setup as you like to observe. + ### One Call API Weather and Forecast The thing `onecall` supports the [current and forecast weather data](https://openweathermap.org/api/one-call-api#how) for a specific location using the One Call API. @@ -48,7 +60,7 @@ For every day in history you have to create a different thing. ## Discovery -If a system location is set, a "Local Weather And Forecast" (`weather-and-forecast`) thing and "Local UV Index" (`uvindex`) thing will be automatically discovered for this location. +If a system location is set, a "Local Weather And Forecast" (`weather-and-forecast`) thing will be automatically discovered for this location. Once the system location will be changed, the background discovery updates the configuration of both things accordingly. ## Thing Configuration @@ -73,21 +85,31 @@ Once the parameters `forecastHours` or `forecastDays` will be changed, the avail ### Current UV Index And Forecast -| Parameter | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------| -| location | Location of weather in geographical coordinates (latitude/longitude/altitude). **Mandatory** | +| Parameter | Description | +|----------------|----------------------------------------------------------------------------------------------------------------------------------| +| location | Location of weather in geographical coordinates (latitude/longitude/altitude). **Mandatory** | | forecastDays | Number of days for UV Index forecast (including todays forecast). Optional, the default value is 6 (min="1", max="8", step="1"). | Once the parameter `forecastDays` will be changed, the available channel groups on the thing will be created or removed accordingly. -### One Call API Weather and Forecast +### Current Air Pollution And Forecast | Parameter | Description | |----------------|--------------------------------------------------------------------------------------------------------------------------------| | location | Location of weather in geographical coordinates (latitude/longitude/altitude). **Mandatory** | +| forecastHours | Number of hours for air pollution forecast. Optional, the default value is 0 (min="0", max="120", step="1"). | + +Once the parameter `forecastHours` will be changed, the available channel groups on the thing will be created or removed accordingly. + +### One Call API Weather and Forecast + +| Parameter | Description | +|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| location | Location of weather in geographical coordinates (latitude/longitude/altitude). **Mandatory** | | forecastMinutes| Number of minutes for minutely precipitation forecast. Optional, the default value is 0, so by default **no** minutely forecast data is fetched. (min="0", max="60"). | -| forecastHours | Number of hours for hourly forecast. Optional, the default value is 24 (min="0", max="48"). | -| forecastDays | Number of days for daily forecast (including todays forecast). Optional, the default value is 6 (min="0", max="8"). | +| forecastHours | Number of hours for hourly forecast. Optional, the default value is 24 (min="0", max="48"). | +| forecastDays | Number of days for daily forecast (including todays forecast). Optional, the default value is 6 (min="0", max="8"). | +| numberOfAlerts | Number of alerts to be shown. Optional, the default value is 0 (min="0", max="5"). | ### One Call API History Data @@ -133,7 +155,6 @@ These channels are not supported in the One Call API | current | visibility | Number:Length | Current visibility. | | current | uvindex | Number | Current UV Index. Only available in the One Call API | - **Attention**: Rain item is showing "1h" in the case when data are received from weather stations directly. The fact is that some METAR stations do not have precipitation indicators or do not measure precipitation conditions due to some other technical reasons. In this case, we use model data. @@ -181,36 +202,48 @@ See above for a description of the available channels. ### Daily Forecast -| Channel Group ID | Channel ID | Item Type | Description | -|------------------------------------------------------------------|----------------------|----------------------|----------------------------------------------------------------------------| -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | time-stamp | DateTime | Date of data forecasted. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | condition | String | Forecast weather condition. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | condition-id | String | Id of the forecasted weather condition. **Advanced** | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | icon | Image | Icon representing the forecasted weather condition. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | icon-id | String | Id of the icon representing the forecasted weather condition. **Advanced** | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | apparent-temperature | Number:Temperature | Forecasted apparent temperature. Not available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | min-temperature | Number:Temperature | Minimum forecasted temperature of a day. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | max-temperature | Number:Temperature | Maximum forecasted temperature of a day. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | pressure | Number:Pressure | Forecasted barometric pressure. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | humidity | Number:Dimensionless | Forecasted atmospheric humidity. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | wind-speed | Number:Speed | Forecasted wind speed. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | wind-direction | Number:Angle | Forecasted wind direction. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | gust-speed | Number:Speed | Forecasted gust speed. **Advanced** | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | cloudiness | Number:Dimensionless | Forecasted cloudiness. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | rain | Number:Length | Expected rain volume of a day. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | snow | Number:Length | Expected snow volume of a day. | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | dew-point | Number:Temperature | Expected dew-point. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | uvindex | Number | Forecasted Midday UV Index. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | precip-probability | Number:Dimensionless | Precipitation probability. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | morning-temperature | Number:Temperature | Expected morning temperature. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | day-temperature | Number:Temperature | Expected day-temperature. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | evening-temperature | Number:Temperature | Expected evening-temperature. Only available in the One Call API | -| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | night-temperature | Number:Temperature | Expected night-temperature. Only available in the One Call API | +| Channel Group ID | Channel ID | Item Type | Description | +|------------------------------------------------------------------|----------------------|----------------------|-----------------------------------------------------------------------------------| +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | time-stamp | DateTime | Date of data forecasted. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | sunrise | DateTime | Time of sunrise for the given day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | sunset | DateTime | Time of sunset for the given day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | condition | String | Forecast weather condition. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | condition-id | String | Id of the forecasted weather condition. **Advanced** | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | icon | Image | Icon representing the forecasted weather condition. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | icon-id | String | Id of the icon representing the forecasted weather condition. **Advanced** | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | apparent-temperature | Number:Temperature | Forecasted apparent temperature. Not available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | min-temperature | Number:Temperature | Minimum forecasted temperature of a day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | max-temperature | Number:Temperature | Maximum forecasted temperature of a day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | pressure | Number:Pressure | Forecasted barometric pressure. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | humidity | Number:Dimensionless | Forecasted atmospheric humidity. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | wind-speed | Number:Speed | Forecasted wind speed. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | wind-direction | Number:Angle | Forecasted wind direction. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | gust-speed | Number:Speed | Forecasted gust speed. **Advanced** | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | cloudiness | Number:Dimensionless | Forecasted cloudiness. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | rain | Number:Length | Expected rain volume of a day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay16 | snow | Number:Length | Expected snow volume of a day. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | dew-point | Number:Temperature | Expected dew-point. Only available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | uvindex | Number | Forecasted Midday UV Index. Only available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | precip-probability | Number:Dimensionless | Precipitation probability. | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | morning-temperature | Number:Temperature | Expected morning temperature. Only available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | day-temperature | Number:Temperature | Expected day-temperature. Only available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | evening-temperature | Number:Temperature | Expected evening-temperature. Only available in the One Call API | +| forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | night-temperature | Number:Temperature | Expected night-temperature. Only available in the One Call API | | forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | apparent-morning | Number:Temperature | Expected apparent temperature in the morning. Only available in the One Call API | | forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | apparent-day | Number:Temperature | Expected apparent temperature in the day. Only available in the One Call API | | forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | apparent-evening | Number:Temperature | Expected apparent temperature in the evening. Only available in the One Call API | | forecastToday, forecastTomorrow, forecastDay2, ... forecastDay7 | apparent-night | Number:Temperature | Expected apparent temperature in the night. Only available in the One Call API | - + +### One Call API Weather Warnings + +| Channel Group ID | Channel ID | Item Type | Description | +|-----------------------|-------------|-----------|-----------------------------------------------------| +| alerts1, alerts2, ... | event | String | Type of the warning, e.g. FROST. | +| alerts1, alerts2, ... | description | String | A detailed description of the alert. | +| alerts1, alerts2, ... | onset | DateTime | Start Date and Time for which the warning is valid. | +| alerts1, alerts2, ... | expires | DateTime | End Date and Time for which the warning is valid. | +| alerts1, alerts2, ... | source | String | The source of the alert. **Advanced** | + ### UV Index | Channel Group ID | Channel ID | Item Type | Description | @@ -220,6 +253,21 @@ See above for a description of the available channels. The `uvindex` channel is also available in the current data and the daily forecast of the One Call API. +### Air Pollution + +| Channel Group ID | Channel ID | Item Type | Description | +|-----------------------------------------------------------------|------------------------|----------------|--------------------------------------------------------------------------| +| current, forecastHours01, forecastHours02, ... forecastHours120 | time-stamp | DateTime | Date of data observation / forecast. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | airQualityIndex | Number | Current or forecasted air quality index. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | particulateMatter2dot5 | Number:Density | Current or forecasted density of particles less than 2.5 µm in diameter. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | particulateMatter10 | Number:Density | Current or forecasted density of particles less than 10 µm in diameter. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | carbonMonoxide | Number:Density | Current or forecasted concentration of carbon monoxide. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | nitrogenMonoxide | Number:Density | Current or forecasted concentration of nitrogen monoxide. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | nitrogenDioxide | Number:Density | Current or forecasted concentration of nitrogen dioxide. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | ozone | Number:Density | Current or forecasted concentration of ozone. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | sulphurDioxide | Number:Density | Current or forecasted concentration of sulphur dioxide. | +| current, forecastHours01, forecastHours02, ... forecastHours120 | ammonia | Number:Density | Current or forecasted concentration of ammonia. | + ## Full Example ### Things @@ -230,11 +278,10 @@ demo.things Bridge openweathermap:weather-api:api "OpenWeatherMap Account" [apikey="AAA", refreshInterval=30, language="de"] { Thing weather-and-forecast local "Local Weather And Forecast" [location="XXX,YYY", forecastHours=0, forecastDays=7] Thing weather-and-forecast miami "Weather And Forecast In Miami" [location="25.782403,-80.264563", forecastHours=24, forecastDays=0] - Thing uvindex local "Local UV Index" [location="XXX,YYY", forecastDays=7] } ``` -#### One Call API version +#### One Call API Version ```java Bridge openweathermap:weather-api:api "OpenWeatherMap Account" [apikey="Add your API key", refreshInterval=60, language="de"] { @@ -306,16 +353,9 @@ String miamiHourlyForecast06Condition "Condition in Miami for hours 3 to 6 [%s]" Image miamiHourlyForecast06ConditionIcon "Icon" { channel="openweathermap:weather-and-forecast:api:miami:forecastHours06#icon" } Number:Temperature miamiHourlyForecast06Temperature "Temperature in Miami for hours 3 to 6 [%.1f %unit%]" { channel="openweathermap:weather-and-forecast:api:miami:forecastHours06#temperature" } ... - -DateTime localCurrentUVIndexTimestamp "Timestamp of last measurement [%1$tY-%1$tm-%1$td]" + + + location + + Location of weather in geographical coordinates (latitude/longitude/altitude). + + + + Number of hours for air pollution forecast. + 0 + + + location @@ -122,6 +135,11 @@ Number of minutes for minutely precipitation forecast. 0 + + + Number of alerts to be shown. + 0 + diff --git a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap.properties b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap.properties index 4c3d9f123fa7d..bb260043e59b6 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap.properties +++ b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap.properties @@ -13,6 +13,7 @@ offline.conf-error-not-supported-uvindex-number-of-days = The 'forecastDays' par offline.conf-error-not-supported-onecall-number-of-minutes = The 'forecastMinutes' parameter must be between 0 and 60. offline.conf-error-not-supported-onecall-number-of-hours = The 'forecastHours' parameter must be between 0 and 48. offline.conf-error-not-supported-onecall-number-of-days = The 'forecastDays' parameter must be between 0 and 7. +offline.conf-error-not-supported-onecall-number-of-alerts = The 'numberOfAlerts' parameter must be greater than or equals to 0. # discovery result discovery.openweathermap.weather-and-forecast.api.local.label = Local Weather And Forecast diff --git a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap_de.properties b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap_de.properties index 524d7457cc7c3..d00377ff118cf 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap_de.properties +++ b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/i18n/openweathermap_de.properties @@ -69,6 +69,9 @@ thing-type.openweathermap.weather-and-forecast.description = Erm thing-type.openweathermap.uvindex.label = UV-Index thing-type.openweathermap.uvindex.description = Erm�glicht die Anzeige des aktuellen UV-Index. +thing-type.openweathermap.air-pollution.label = Luftqualit�t +thing-type.openweathermap.air-pollution.description = Erm�glicht die Anzeige der aktuellen Luftqualit�t. + # thing types config thing-type.config.openweathermap.weather-and-forecast.location.label = Ort der Wetterdaten thing-type.config.openweathermap.weather-and-forecast.location.description = Ort der Wetterdaten in geographischen Koordinaten (Breitengrad/L�ngengrad/H�he). @@ -85,6 +88,27 @@ thing-type.config.openweathermap.uvindex.location.description = Ort der Wetterda thing-type.config.openweathermap.uvindex.forecastDays.label = Tage thing-type.config.openweathermap.uvindex.forecastDays.description = Anzahl der Tage f�r die UV-Index Vorhersage. +thing-type.config.openweathermap.air-pollution.location.label = Ort der Wetterdaten +thing-type.config.openweathermap.air-pollution.location.description = Ort der Wetterdaten in geographischen Koordinaten (Breitengrad/L�ngengrad/H�he). + +thing-type.config.openweathermap.air-pollution.forecastHours.label = Stunden +thing-type.config.openweathermap.air-pollution.forecastHours.description = Anzahl der Stunden f�r die Vorhersage der Luftqualit�t. + +thing-type.config.openweathermap.onecall.location.label = Ort der Wetterdaten +thing-type.config.openweathermap.onecall.location.description = Ort der Wetterdaten in geographischen Koordinaten (Breitengrad/L�ngengrad/H�he). + +thing-type.config.openweathermap.onecall.forecastMinutes.label = Minuten +thing-type.config.openweathermap.onecall.forecastMinutes.description = Anzahl der Minuten f�r die Vorhersage von Niederschlag. + +thing-type.config.openweathermap.onecall.forecastHours.label = Stunden +thing-type.config.openweathermap.onecall.forecastHours.description = Anzahl der Stunden f�r die Wettervorhersage. + +thing-type.config.openweathermap.onecall.forecastDays.label = Tage +thing-type.config.openweathermap.onecall.forecastDays.description = Anzahl der Tage f�r die Wettervorhersage, inklusive aktueller Tag. + +thing-type.config.openweathermap.onecall.numberOfAlerts.label = Wetterwarnungen +thing-type.config.openweathermap.onecall.numberOfAlerts.description = Anzahl der Wetterwarnungen. + # channel group types channel-group-type.openweathermap.station.label = Wetterstation channel-group-type.openweathermap.station.description = Fasst Daten �ber die Wetterstation oder den Ort zusammen. @@ -104,6 +128,18 @@ channel-group-type.openweathermap.uvindex.description = Fasst aktuelle UV-Index channel-group-type.openweathermap.uvindexForecast.label = UV-Index Vorhersage channel-group-type.openweathermap.uvindexForecast.description = Fasst Daten der UV-Index Vorhersage zusammen. +channel-group-type.openweathermap.airPollution.label = Aktuelle Luftqualit�t +channel-group-type.openweathermap.airPollution.description = Fasst Daten �ber die aktuelle Luftqualit�t zusammen. + +channel-group-type.openweathermap.airPollutionForecast.label = Vorhersage der Luftqualit�t +channel-group-type.openweathermap.airPollutionForecast.description = Fasst Daten �ber die vorhergesagte Luftqualit�t zusammen. + +channel-group-type.openweathermap.oneCallCurrent.label = Aktuelles Wetter +channel-group-type.openweathermap.oneCallCurrent.description = Fasst aktuelle Wetterdaten der One Call API zusammen. + +channel-group-type.openweathermap.oneCallAlerts.label = Wetterwarnungen +channel-group-type.openweathermap.oneCallAlerts.description = Fasst Daten von Wetterwarnungen zusammen. + # channel groups thing-type.openweathermap.weather-and-forecast.group.forecastHours03.label = Wettervorhersage f�r 3 Stunden thing-type.openweathermap.weather-and-forecast.group.forecastHours03.description = Fasst Daten der Wettervorhersage in den n�chsten drei Stunden zusammen. @@ -162,6 +198,24 @@ thing-type.openweathermap.uvindex.group.forecastDay4.description = Fasst Daten d thing-type.openweathermap.uvindex.group.forecastDay5.label = UV-Index f�r 5 Tage thing-type.openweathermap.uvindex.group.forecastDay5.description = Fasst Daten der UV-Index Vorhersage in f�nf Tagen zusammen. +thing-type.openweathermap.onecall.group.forecastToday.label = Wettervorhersage f�r heute +thing-type.openweathermap.onecall.group.forecastToday.description = Fasst Daten der heutigen Wettervorhersage der One Call API zusammen. + +thing-type.openweathermap.onecall.group.forecastTomorrow.label = Wettervorhersage f�r morgen +thing-type.openweathermap.onecall.group.forecastTomorrow.description = Fasst Daten der morgigen Wettervorhersage der One Call API zusammen. + +thing-type.openweathermap.onecall.group.forecastDay2.label = Wettervorhersage f�r �bermorgen +thing-type.openweathermap.onecall.group.forecastDay2.description = Fasst Daten der �bermorgigen Wettervorhersage der One Call API zusammen. + +thing-type.openweathermap.onecall.group.forecastDay3.label = Wettervorhersage f�r 3 Tage +thing-type.openweathermap.onecall.group.forecastDay3.description = Fasst Daten der Wettervorhersage in drei Tagen der One Call API zusammen. + +thing-type.openweathermap.onecall.group.forecastDay4.label = Wettervorhersage f�r 4 Tage +thing-type.openweathermap.onecall.group.forecastDay4.description = Fasst Daten der Wettervorhersage in vier Tagen der One Call API zusammen. + +thing-type.openweathermap.onecall.group.forecastDay5.label = Wettervorhersage f�r 5 Tage +thing-type.openweathermap.onecall.group.forecastDay5.description = Fasst Daten der Wettervorhersage in f�nf Tagen der One Call API zusammen. + # channel types channel-type.openweathermap.station-id.label = Station-ID channel-type.openweathermap.station-id.description = Zeigt die ID der Wetterstation oder des Ortes an. @@ -184,6 +238,14 @@ channel-type.openweathermap.daily-forecast-time-stamp.label = Vorhersage Datum channel-type.openweathermap.daily-forecast-time-stamp.description = Zeigt das Datum der Vorhersage an. channel-type.openweathermap.daily-forecast-time-stamp.state.pattern = %1$td.%1$tm.%1$tY +channel-type.openweathermap.sunrise.label = Sonnenaufgang +channel-type.openweathermap.sunrise.description = Zeigt den Zeitpunkt des Sonnenaufgangs an. +channel-type.openweathermap.sunrise.state.pattern = %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS + +channel-type.openweathermap.sunset.label = Sonnenuntergang +channel-type.openweathermap.sunset.description = Zeigt den Zeitpunkt des Sonnenuntergangs an. +channel-type.openweathermap.sunset.state.pattern = %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS + channel-type.openweathermap.condition.label = Wetterlage channel-type.openweathermap.condition.description = Zeigt die aktuelle Wetterlage an. @@ -253,12 +315,96 @@ channel-type.openweathermap.snow.description = Zeigt den kumulierten Schnee der channel-type.openweathermap.forecasted-snow.label = Vorhergesagter Schnee channel-type.openweathermap.forecasted-snow.description = Zeigt die vorhergesagte Schneemenge an. +channel-type.openweathermap.precip-probability.label = Vorhergesagte Niederschlagswahrscheinlichkeit +channel-type.openweathermap.precip-probability.description = Zeigt die vorhergesagte Niederschlagswahrscheinlichkeit an. + channel-type.openweathermap.uvindex.label = UV-Index channel-type.openweathermap.uvindex.description = Zeigt den aktuellen UV-Index an. channel-type.openweathermap.forecasted-uvindex.label = Vorhergesagter UV-Index channel-type.openweathermap.forecasted-uvindex.description = Zeigt den vorhergesagten UV-Index an. +channel-type.openweathermap.air-quality-index.label = Luftqualit�tsindex +channel-type.openweathermap.air-quality-index.description = Zeigt den aktuellen Luftqualit�tsindex an. +channel-type.openweathermap.air-quality-index.state.option.1 = Gut +channel-type.openweathermap.air-quality-index.state.option.2 = Befriedigend +channel-type.openweathermap.air-quality-index.state.option.3 = Moderat +channel-type.openweathermap.air-quality-index.state.option.4 = Ungesund +channel-type.openweathermap.air-quality-index.state.option.5 = �u�erst Ungesund + +channel-type.openweathermap.forecasted-air-quality-index.label = Vorhergesagter Luftqualit�tsindex +channel-type.openweathermap.forecasted-air-quality-index.description = Zeigt den vorhergesagten Luftqualit�tsindex an. +channel-type.openweathermap.forecasted-air-quality-index.state.option.1 = Gut +channel-type.openweathermap.forecasted-air-quality-index.state.option.2 = Befriedigend +channel-type.openweathermap.forecasted-air-quality-index.state.option.3 = Moderat +channel-type.openweathermap.forecasted-air-quality-index.state.option.4 = Ungesund +channel-type.openweathermap.forecasted-air-quality-index.state.option.5 = �u�erst Ungesund + +channel-type.openweathermap.particulate-matter-2dot5.label = Feinstaub - PM2.5 +channel-type.openweathermap.particulate-matter-2dot5.description = Aktuelle Dichte von Teilchen mit Partikelgr��e unter 2,5 �m. + +channel-type.openweathermap.forecasted-particulate-matter-2dot5.label = Vorhergesagter Feinstaub - PM2.5 +channel-type.openweathermap.forecasted-particulate-matter-2dot5.description = Vorhergesagte Dichte von Teilchen mit Partikelgr��e unter 2,5 �m. + +channel-type.openweathermap.particulate-matter-10.label = Feinstaub - PM10 +channel-type.openweathermap.particulate-matter-10.description = Aktuelle Dichte von Teilchen mit Partikelgr��e unter 10 �m. + +channel-type.openweathermap.forecasted-particulate-matter-10.label = Vorhergesagter Feinstaub - PM10 +channel-type.openweathermap.forecasted-particulate-matter-10.description = Vorhergesagte Dichte von Teilchen mit Partikelgr��e unter 10 �m. + +channel-type.openweathermap.carbon-monoxide.label = Kohlenmonoxid +channel-type.openweathermap.carbon-monoxide.description = Aktuelle Konzentration an Kohlenmonoxid. + +channel-type.openweathermap.forecasted-carbon-monoxide.label = Vorhergesagter Kohlenmonoxid +channel-type.openweathermap.forecasted-carbon-monoxide.description = Vorhergesagte Konzentration an Kohlenmonoxid. + +channel-type.openweathermap.nitrogen-monoxide.label = Stickstoffmonoxid +channel-type.openweathermap.nitrogen-monoxide.description = Aktuelle Konzentration an Stickstoffmonoxid. + +channel-type.openweathermap.forecasted-nitrogen-monoxide.label = Vorhergesagter Stickstoffmonoxid +channel-type.openweathermap.forecasted-nitrogen-monoxide.description = Vorhergesagte Konzentration an Stickstoffmonoxid. + +channel-type.openweathermap.nitrogen-dioxide.label = Stickstoffdioxid +channel-type.openweathermap.nitrogen-dioxide.description = Aktuelle Konzentration an Stickstoffdioxid. + +channel-type.openweathermap.forecasted-nitrogen-dioxide.label = Vorhergesagter Stickstoffdioxid +channel-type.openweathermap.forecasted-nitrogen-dioxide.description = Vorhergesagte Konzentration an Stickstoffdioxid. + +channel-type.openweathermap.ozone.label = Ozon +channel-type.openweathermap.ozone.description = Aktuelle Konzentration an Ozon. + +channel-type.openweathermap.forecasted-ozone.label = Vorhergesagter Ozon +channel-type.openweathermap.forecasted-ozone.description = Vorhergesagte Konzentration an Ozon. + +channel-type.openweathermap.sulphur-dioxide.label = Schwefeldioxid +channel-type.openweathermap.sulphur-dioxide.description = Aktuelle Konzentration an Schwefeldioxid. + +channel-type.openweathermap.forecasted-sulphur-dioxide.label = Vorhergesagter Schwefeldioxid +channel-type.openweathermap.forecasted-sulphur-dioxide.description = Vorhergesagte Konzentration an Schwefeldioxid. + +channel-type.openweathermap.ammonia.label = Ammoniak +channel-type.openweathermap.ammonia.description = Aktuelle Konzentration an Ammoniak. + +channel-type.openweathermap.forecasted-ammonia.label = Vorhergesagter Ammoniak +channel-type.openweathermap.forecasted-ammonia.description = Vorhergesagte Konzentration an Ammoniak. + +channel-type.openweathermap.alert-event.label = Art +channel-type.openweathermap.alert-event.description = Art der Warnung, z.B. FROST. + +channel-type.openweathermap.alert-description.label = Beschreibung +channel-type.openweathermap.alert-description.description = Zeigt die Beschreibung der Wetterwarnung an. + +channel-type.openweathermap.alert-onset.label = G�ltig ab +channel-type.openweathermap.alert-onset.description = Datum und Uhrzeit, ab dem die Warnung g�ltig ist. +channel-type.openweathermap.alert-onset.state.pattern = %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS + +channel-type.openweathermap.alert-expires.label = G�ltig bis +channel-type.openweathermap.alert-expires.description = Datum und Uhrzeit, bis zu dem die Warnung g�ltig ist. +channel-type.openweathermap.alert-expires.state.pattern = %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS + +channel-type.openweathermap.alert-source.label = Quelle +channel-type.openweathermap.alert-source.description = Zeigt die Quelle der Wetterwarnung an. + # thing status offline.conf-error-missing-apikey = Der Parameter 'API Schl�ssel' muss konfiguriert werden. offline.conf-error-invalid-apikey = Ung�ltiger 'API Schl�ssel'. Mehr Infos unter https://openweathermap.org/faq#error401. @@ -270,6 +416,7 @@ offline.conf-error-parsing-location = Der Parameter 'Ort' kann nicht in Latitude offline.conf-error-not-supported-number-of-hours = Der Parameter 'forecastHours' muss zwischen 0 und 120 liegen - Schrittweite: 3. offline.conf-error-not-supported-number-of-days = Der Parameter 'forecastDays' muss zwischen 0 und 16 liegen. offline.conf-error-not-supported-uvindex-number-of-days = Der Parameter 'forecastDays' muss zwischen 1 und 8 liegen. +offline.conf-error-not-supported-air-pollution-number-of-hours = Der Parameter 'forecastHours' muss zwischen 0 und 120 liegen - Schrittweite: 1. offline.conf-error-not-supported-onecall-number-of-minutes = Der Parameter 'forecastMinutes' muss zwischen 0 and 60 liegen. offline.conf-error-not-supported-onecall-number-of-hours = Der Parameter 'forecastHours' muss zwischen 0 und 48 liegen. @@ -278,3 +425,4 @@ offline.conf-error-not-supported-onecall-number-of-days = Der Parameter 'forecas # discovery result discovery.openweathermap.weather-and-forecast.api.local.label = Lokales Wetter und Wettervorhersage discovery.openweathermap.uvindex.api.local.label = Lokaler UV-Index +discovery.openweathermap.air-pollution.api.local.label = Lokale Luftqualit�t diff --git a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/channel-types.xml index 9bbf626368efe..87a3d5cb03728 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/channel-types.xml +++ b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/channel-types.xml @@ -84,6 +84,9 @@ + + + @@ -105,6 +108,40 @@ + + + This is the current air pollution. + + + + + + + + + + + + + + + + + This is the forecasted air pollution. + + + + + + + + + + + + + + Current weather data from the One Call API. @@ -253,6 +290,17 @@ + + + Weather warnings issued for the requested location. + + + + + + + + @@ -576,6 +624,148 @@ + + Number + + Current air quality index. + + + + + + + + + + + + + Number + + Forecasted air quality index. + + + + + + + + + + + + + Number:Density + + Current density of particles less than 2.5 µm in diameter. + + + + + Number:Density + + Forecasted density of particles less than 2.5 µm in diameter. + + + + + Number:Density + + Current density of particles less than 10 µm in diameter. + + + + + Number:Density + + Forecasted density of particles less than 10 µm in diameter. + + + + + Number:Density + + Current concentration of carbon monoxide. + + + + + Number:Density + + Forecasted concentration of carbon monoxide. + + + + + Number:Density + + Current concentration of nitrogen monoxide. + + + + + Number:Density + + Forecasted concentration of nitrogen monoxide. + + + + + Number:Density + + Current concentration of nitrogen dioxide. + + + + + Number:Density + + Forecasted concentration of nitrogen dioxide. + + + + + Number:Density + + Current concentration of ozone. + + + + + Number:Density + + Forecasted concentration of ozone. + + + + + Number:Density + + Current concentration of sulphur dioxide. + + + + + Number:Density + + Forecasted concentration of sulphur dioxide. + + + + + Number:Density + + Current concentration of ammonia. + + + + + Number:Density + + Forecasted concentration of ammonia. + + + Number:Length @@ -585,9 +775,46 @@ Number:Dimensionless - + Forecasted precipitation probability. + Rain + + String + + Type of the warning, e.g. FROST. + + + + + String + + A detailed description of the alert. + + + + + DateTime + + Start Date and Time for which the warning is valid. + Time + + + + + DateTime + + End Date and Time for which the warning is valid. + Time + + + + + String + + Source of the alert. + + diff --git a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/thing-types.xml index 848132a6865a1..2fab59a36fc61 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.openweathermap/src/main/resources/OH-INF/thing/thing-types.xml @@ -116,6 +116,23 @@ + + + + + + + Provides air pollution data from the OpenWeatherMap API. + + + + + + location + + + + @@ -216,7 +233,6 @@ - This is the weather forecast for today from the one call API. diff --git a/bundles/org.openhab.binding.openwebnet/README.md b/bundles/org.openhab.binding.openwebnet/README.md index e805668238866..4d1e7b5dd1438 100644 --- a/bundles/org.openhab.binding.openwebnet/README.md +++ b/bundles/org.openhab.binding.openwebnet/README.md @@ -8,6 +8,8 @@ The binding supports: - auto discovery of BUS/SCS IP and ZigBee USB gateways; auto discovery of devices - commands from openHAB and feedback (events) from BUS/SCS and wireless network + +![MyHOMEServer1 Gateway](doc/MyHOMEServer1_gateway.jpg) ![F454 Gateway](doc/F454_gateway.png) ![ZigBee USB Gateway](doc/USB_gateway.jpg) @@ -40,6 +42,7 @@ The following Things and OpenWebNet `WHOs` are supported: | Gateway Management | `13` | `bus_gateway` | Any IP gateway supporting OpenWebNet protocol should work (e.g. F454 / MyHOMEServer1 / MH202 / F455 / MH200N, ...) | Successfully tested: F454, MyHOMEServer1, MyHOME_Screen10, F455, F452, F453AV, MH201, MH202, MH200N. Some connection stability issues/gateway resets reported with MH202 | | Lighting | `1` | `bus_on_off_switch`, `bus_dimmer` | BUS switches and dimmers | Successfully tested: F411/2, F411/4, F411U2, F422, F429. Some discovery issues reported with F429 (DALI Dimmers) | | Automation | `2` | `bus_automation` | BUS roller shutters, with position feedback and auto-calibration | Successfully tested: LN4672M2 | +| Energy Management | `18` | `bus_energy_meter` | Energy Management | Successfully tested: F520, F521 | ### For ZigBee (Radio) @@ -129,7 +132,7 @@ Devices support some of the following channels: | `switch` or `switch_01`/`02` for ZigBee | Switch | To switch the device `ON` and `OFF` | R/W | | `brightness` | Dimmer | To adjust the brightness value (Percent, `ON`, `OFF`) | R/W | | `shutter` | Rollershutter | To activate roller shutters (`UP`, `DOWN`, `STOP`, Percent - [see Shutter position](#shutter-position)) | R/W | - +| `power` | Number:Power | The current active power usage from Energy Meter | R | ### Notes on channels #### `shutter` position @@ -154,6 +157,8 @@ Bridge openwebnet:bus_gateway:mybridge "MyHOMEServer1" [ host="192.168.1.35", pa bus_on_off_switch LR_switch "Living Room Light" [ where="51" ] bus_dimmer LR_dimmer "Living Room Dimmer" [ where="0311#4#01" ] bus_automation LR_shutter "Living Room Shutter" [ where="93", shutterRun="10050"] + bus_energy_meter CENTRAL_Ta "Energy Meter Ta" [ where="51" ] + bus_energy_meter CENTRAL_Tb "Energy Meter Tb" [ where="52" ] } ``` @@ -176,6 +181,9 @@ Example items linked to BUS devices: Switch iLR_switch "Light" (gLivingRoom) [ "Lighting" ] { channel="openwebnet:bus_on_off_switch:mybridge:LR_switch:switch" } Dimmer iLR_dimmer "Dimmer [%.0f %%]" (gLivingRoom) [ "Lighting" ] { channel="openwebnet:bus_dimmer:mybridge:LR_dimmer:brightness" } Rollershutter iLR_shutter "Shutter [%.0f %%]" (gShutters, gLivingRoom) [ "Blinds" ] { channel="openwebnet:bus_automation:mybridge:LR_shutter:shutter" } +Number:Power iCENTRAL_Ta "Power [%.0f %unit%]" { channel="openwebnet:bus_energy_meter:mybridge:CENTRAL_Ta:power" } +Number:Power iCENTRAL_Tb "Power [%.0f %unit%]" { channel="openwebnet:bus_energy_meter:mybridge:CENTRAL_Tb:power" } + ``` Example items linked to OpenWebNet ZigBee devices: @@ -198,6 +206,12 @@ sitemap openwebnet label="OpenWebNet Binding Example Sitemap" Default item=iLR_dimmer icon="light" Default item=iLR_shutter } + + Frame label="Energy Meters" icon="energy" + { + Default item=iCENTRAL_Ta label="General" icon="energy" valuecolor=[>3000="red"] + Default item=iCENTRAL_Tb label="Ground Floor" icon="energy" valuecolor=[>3000="red"] + } } ``` @@ -216,5 +230,6 @@ Special thanks for helping on testing this binding go to: [@gilberto.cocchi](https://community.openhab.org/u/gilberto.cocchi/), [@llegovich](https://community.openhab.org/u/llegovich), [@gabriele.daltoe](https://community.openhab.org/u/gabriele.daltoe), -[@feodor](https://community.openhab.org/u/feodor) +[@feodor](https://community.openhab.org/u/feodor), +[@aconte80](https://community.openhab.org/u/aconte80) and many others at the fantastic openHAB community! diff --git a/bundles/org.openhab.binding.openwebnet/doc/F454_gateway.png b/bundles/org.openhab.binding.openwebnet/doc/F454_gateway.png index 05df8e331de92..3f03176edc4ad 100644 Binary files a/bundles/org.openhab.binding.openwebnet/doc/F454_gateway.png and b/bundles/org.openhab.binding.openwebnet/doc/F454_gateway.png differ diff --git a/bundles/org.openhab.binding.openwebnet/doc/MyHOMEServer1_gateway.jpg b/bundles/org.openhab.binding.openwebnet/doc/MyHOMEServer1_gateway.jpg new file mode 100644 index 0000000000000..6a99f76020e81 Binary files /dev/null and b/bundles/org.openhab.binding.openwebnet/doc/MyHOMEServer1_gateway.jpg differ diff --git a/bundles/org.openhab.binding.openwebnet/pom.xml b/bundles/org.openhab.binding.openwebnet/pom.xml index 2765abe37f1fd..9b703d9762b6d 100644 --- a/bundles/org.openhab.binding.openwebnet/pom.xml +++ b/bundles/org.openhab.binding.openwebnet/pom.xml @@ -21,9 +21,9 @@ - com.github.openwebnet4j + io.github.openwebnet4j openwebnet4j - 0.3.4 + 0.4.1 compile diff --git a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/OpenWebNetBindingConstants.java b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/OpenWebNetBindingConstants.java index 5eaf8dd2082a4..33fa4def8a9e5 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/OpenWebNetBindingConstants.java +++ b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/OpenWebNetBindingConstants.java @@ -26,6 +26,7 @@ * The {@link OpenWebNetBindingConstants} class defines common constants, which are used across the whole binding. * * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management */ @NonNullByDefault @@ -52,6 +53,8 @@ public class OpenWebNetBindingConstants { public static final String THING_LABEL_BUS_DIMMER = "Dimmer"; public static final ThingTypeUID THING_TYPE_BUS_AUTOMATION = new ThingTypeUID(BINDING_ID, "bus_automation"); public static final String THING_LABEL_BUS_AUTOMATION = "Automation"; + public static final ThingTypeUID THING_TYPE_BUS_ENERGY_METER = new ThingTypeUID(BINDING_ID, "bus_energy_meter"); + public static final String THING_LABEL_BUS_ENERGY_METER = "Energy Meter"; // ZIGBEE public static final ThingTypeUID THING_TYPE_ZB_ON_OFF_SWITCH = new ThingTypeUID(BINDING_ID, "zb_on_off_switch"); @@ -76,9 +79,14 @@ public class OpenWebNetBindingConstants { public static final Set AUTOMATION_SUPPORTED_THING_TYPES = new HashSet<>( Arrays.asList(THING_TYPE_ZB_AUTOMATION, THING_TYPE_BUS_AUTOMATION)); + // ## Energy Management + public static final Set ENERGY_MANAGEMENT_SUPPORTED_THING_TYPES = new HashSet<>( + Arrays.asList(THING_TYPE_BUS_ENERGY_METER)); + // ## Groups public static final Set DEVICE_SUPPORTED_THING_TYPES = Stream - .of(LIGHTING_SUPPORTED_THING_TYPES, AUTOMATION_SUPPORTED_THING_TYPES, GENERIC_SUPPORTED_THING_TYPES) + .of(LIGHTING_SUPPORTED_THING_TYPES, AUTOMATION_SUPPORTED_THING_TYPES, + ENERGY_MANAGEMENT_SUPPORTED_THING_TYPES, GENERIC_SUPPORTED_THING_TYPES) .flatMap(Collection::stream).collect(Collectors.toCollection(HashSet::new)); public static final Set BRIDGE_SUPPORTED_THING_TYPES = new HashSet<>( @@ -94,9 +102,13 @@ public class OpenWebNetBindingConstants { public static final String CHANNEL_SWITCH_01 = "switch_01"; public static final String CHANNEL_SWITCH_02 = "switch_02"; public static final String CHANNEL_BRIGHTNESS = "brightness"; + // automation public static final String CHANNEL_SHUTTER = "shutter"; + // energy management + public static final String CHANNEL_POWER = "power"; + // devices config properties public static final String CONFIG_PROPERTY_WHERE = "where"; public static final String CONFIG_PROPERTY_SHUTTER_RUN = "shutterRun"; diff --git a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetBridgeHandler.java b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetBridgeHandler.java index d906d59079163..46366d60073a0 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetBridgeHandler.java +++ b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetBridgeHandler.java @@ -12,7 +12,9 @@ */ package org.openhab.binding.openwebnet.handler; -import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.*; +import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.PROPERTY_FIRMWARE_VERSION; +import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.PROPERTY_SERIAL_NO; +import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.THING_TYPE_ZB_GATEWAY; import java.util.Collection; import java.util.Collections; @@ -45,6 +47,7 @@ import org.openwebnet4j.communication.OWNException; import org.openwebnet4j.message.Automation; import org.openwebnet4j.message.BaseOpenMessage; +import org.openwebnet4j.message.EnergyManagement; import org.openwebnet4j.message.FrameException; import org.openwebnet4j.message.GatewayMgmt; import org.openwebnet4j.message.Lighting; @@ -60,6 +63,7 @@ * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events. * * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management */ @NonNullByDefault public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener { @@ -286,7 +290,11 @@ private void discoverByActivation(BaseOpenMessage baseMsg) { logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg); return; } - if (baseMsg instanceof Lighting || baseMsg instanceof Automation) { // we support these types only + if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement) { // we + // support + // these + // types + // only BaseOpenMessage bmsg = baseMsg; if (baseMsg instanceof Lighting) { What what = baseMsg.getWhat(); @@ -386,7 +394,7 @@ public void onEventMessage(@Nullable OpenMessage msg) { BaseOpenMessage baseMsg = (BaseOpenMessage) msg; // let's try to get the Thing associated with this message... - if (baseMsg instanceof Lighting || baseMsg instanceof Automation) { + if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement) { String ownId = ownIdFromMessage(baseMsg); logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId); OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId); diff --git a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetEnergyHandler.java b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetEnergyHandler.java new file mode 100644 index 0000000000000..f73a98b20b7bd --- /dev/null +++ b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/handler/OpenWebNetEnergyHandler.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2021 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.openwebnet.handler; + +import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.CHANNEL_POWER; + +import java.util.Set; + +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.openwebnet.OpenWebNetBindingConstants; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; +import org.openwebnet4j.communication.OWNException; +import org.openwebnet4j.message.BaseOpenMessage; +import org.openwebnet4j.message.EnergyManagement; +import org.openwebnet4j.message.FrameException; +import org.openwebnet4j.message.Where; +import org.openwebnet4j.message.WhereEnergyManagement; +import org.openwebnet4j.message.Who; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OpenWebNetEnergyHandler} is responsible for handling commands/messages for a Energy Management OpenWebNet + * device. It extends the abstract {@link OpenWebNetThingHandler}. + * + * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management + */ +@NonNullByDefault +public class OpenWebNetEnergyHandler extends OpenWebNetThingHandler { + + private final Logger logger = LoggerFactory.getLogger(OpenWebNetEnergyHandler.class); + + public final static Set SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.ENERGY_MANAGEMENT_SUPPORTED_THING_TYPES; + + public OpenWebNetEnergyHandler(Thing thing) { + super(thing); + } + + @Override + protected Where buildBusWhere(String wStr) throws IllegalArgumentException { + return new WhereEnergyManagement(wStr); + } + + @Override + protected void requestChannelState(ChannelUID channel) { + logger.debug("requestChannelState() thingUID={} channel={}", thing.getUID(), channel.getId()); + try { + bridgeHandler.gateway.send(EnergyManagement.requestActivePower(deviceWhere.value())); + } catch (OWNException e) { + logger.warn("requestChannelState() OWNException thingUID={} channel={}: {}", thing.getUID(), + channel.getId(), e.getMessage()); + } + } + + @Override + protected void handleChannelCommand(ChannelUID channel, Command command) { + logger.warn("handleChannelCommand() Read only channel, unsupported command {}", command); + } + + @Override + protected String ownIdPrefix() { + return Who.ENERGY_MANAGEMENT.value().toString(); + } + + @Override + protected void handleMessage(BaseOpenMessage msg) { + super.handleMessage(msg); + + if (msg.isCommand()) { + logger.warn("handleMessage() Ignoring unsupported command for thing {}. Frame={}", getThing().getUID(), + msg); + return; + } else { + updateActivePower(msg); + } + } + + /** + * Updates energy power state based on a EnergyManagement message received from the OWN network + * + * @param msg the EnergyManagement message received + * @throws FrameException + */ + private void updateActivePower(BaseOpenMessage msg) { + Integer activePower; + try { + activePower = Integer.parseInt(msg.getDimValues()[0]); + updateState(CHANNEL_POWER, new QuantityType(activePower, Units.WATT)); + } catch (FrameException e) { + logger.warn("FrameException on frame {}: {}", msg, e.getMessage()); + updateState(CHANNEL_POWER, UnDefType.UNDEF); + } + } +} diff --git a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/OpenWebNetHandlerFactory.java b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/OpenWebNetHandlerFactory.java index 7598e2a491bc0..8e1058f6a1304 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/OpenWebNetHandlerFactory.java +++ b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/OpenWebNetHandlerFactory.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.openwebnet.handler.OpenWebNetAutomationHandler; import org.openhab.binding.openwebnet.handler.OpenWebNetBridgeHandler; +import org.openhab.binding.openwebnet.handler.OpenWebNetEnergyHandler; import org.openhab.binding.openwebnet.handler.OpenWebNetGenericHandler; import org.openhab.binding.openwebnet.handler.OpenWebNetLightingHandler; import org.openhab.core.thing.Bridge; @@ -34,6 +35,7 @@ * The {@link OpenWebNetHandlerFactory} is responsible for creating thing handlers. * * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management */ @NonNullByDefault @Component(configurationPid = "binding.openwebnet", service = ThingHandlerFactory.class) @@ -60,6 +62,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (OpenWebNetAutomationHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { logger.debug("creating NEW AUTOMATION Handler"); return new OpenWebNetAutomationHandler(thing); + } else if (OpenWebNetEnergyHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("creating NEW ENERGY Handler"); + return new OpenWebNetEnergyHandler(thing); } logger.warn("ThingType {} is not supported by this binding", thing.getThingTypeUID()); return null; diff --git a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/discovery/OpenWebNetDeviceDiscoveryService.java b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/discovery/OpenWebNetDeviceDiscoveryService.java index 8dff4c1a4aa8f..1dc4420c4f783 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/discovery/OpenWebNetDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.openwebnet/src/main/java/org/openhab/binding/openwebnet/internal/discovery/OpenWebNetDeviceDiscoveryService.java @@ -41,6 +41,7 @@ * bridge/gateway * * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management */ @NonNullByDefault public class OpenWebNetDeviceDiscoveryService extends AbstractDiscoveryService @@ -129,6 +130,14 @@ public void newDiscoveryResult(Where where, OpenDeviceType deviceType, @Nullable deviceWho = Who.AUTOMATION; break; } + + case SCS_ENERGY_METER: { + thingTypeUID = OpenWebNetBindingConstants.THING_TYPE_BUS_ENERGY_METER; + thingLabel = OpenWebNetBindingConstants.THING_LABEL_BUS_ENERGY_METER; + deviceWho = Who.ENERGY_MANAGEMENT; + break; + } + default: logger.warn("Device type {} is not supported, default to GENERIC device (WHERE={})", deviceType, where); if (where instanceof WhereZigBee) { diff --git a/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/BusEnergyMeter.xml b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/BusEnergyMeter.xml new file mode 100644 index 0000000000000..2fcd93bd888f9 --- /dev/null +++ b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/BusEnergyMeter.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + A OpenWebNet BUS/SCS Energy Meter. BTicino models: F52x + + + + + + + BTicino/Legrand + BTI-F52x + 1830 + + + ownId + + + + + Example: 5N with N=[1-255] + + + + + diff --git a/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/ZBAutomation.xml b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/ZBAutomation.xml index b84f3c4a9e63d..87a60e4374ce9 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/ZBAutomation.xml +++ b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/ZBAutomation.xml @@ -27,20 +27,18 @@ ownId - + Time (in ms) to go from max position (e.g. CLOSED) to the other position (e.g. OPEN). Example: 12000 (=12sec). Use AUTO (default) to calibrate the shutter automatically (UP->DOWN->Position%) the first time a Position command (%) is sent. - true AUTO - + It identifies one ZigBee device. Example: 765432101#9 - true diff --git a/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/channels.xml index 2a995a15db151..612739564ed0a 100644 --- a/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.openwebnet/src/main/resources/OH-INF/thing/channels.xml @@ -37,4 +37,12 @@ + + + Number:Power + + Current active power + Energy + + diff --git a/bundles/org.openhab.binding.openwebnet/src/test/java/org/openhab/binding/openwebnet/handler/OwnIdTest.java b/bundles/org.openhab.binding.openwebnet/src/test/java/org/openhab/binding/openwebnet/handler/OwnIdTest.java index 3a46344b2857d..f1adad386b8d9 100644 --- a/bundles/org.openhab.binding.openwebnet/src/test/java/org/openhab/binding/openwebnet/handler/OwnIdTest.java +++ b/bundles/org.openhab.binding.openwebnet/src/test/java/org/openhab/binding/openwebnet/handler/OwnIdTest.java @@ -22,6 +22,7 @@ import org.openwebnet4j.message.BaseOpenMessage; import org.openwebnet4j.message.FrameException; import org.openwebnet4j.message.Where; +import org.openwebnet4j.message.WhereEnergyManagement; import org.openwebnet4j.message.WhereLightAutom; import org.openwebnet4j.message.WhereZigBee; import org.openwebnet4j.message.Who; @@ -33,6 +34,7 @@ * methods: normalizeWhere(), ownIdFromWhoWhere(), ownIdFromMessage(), thingIdFromWhere() * * @author Massimo Valla - Initial contribution + * @author Andrea Conte - Energy management */ @NonNullByDefault public class OwnIdTest { @@ -68,12 +70,12 @@ public enum TEST { zb_switch_2u_1(new WhereZigBee("789301201#9"), Who.fromValue(1), "*1*1*789301201#9##", "789301200h9", "1.789301200h9", "789301200h9"), zb_switch_2u_2(new WhereZigBee("789301202#9"), Who.fromValue(1), "*1*1*789301202#9##", "789301200h9", "1.789301200h9", "789301200h9"), bus_switch(new WhereLightAutom("51"), Who.fromValue(1), "*1*1*51##", "51", "1.51", "51"), - bus_localbus(new WhereLightAutom("25#4#01"), Who.fromValue(1), "*1*1*25#4#01##", "25h4h01", "1.25h4h01", "25h4h01"); + bus_localbus(new WhereLightAutom("25#4#01"), Who.fromValue(1), "*1*1*25#4#01##", "25h4h01", "1.25h4h01", "25h4h01"), //bus_thermo_zone(new WhereThermo("1"), Who.fromValue(4),"*#4*1*0*0020##" , "1", "4.1", "1"), //bus_thermo_zone_act(new WhereThermo("2#1"), Who.fromValue(4),"*#4*2#1*20*0##" ,"2", "4.2", "2"), //bus_thermo_via_cu(new WhereThermo("#1"), Who.fromValue(4),"*#4*#1*0*0020##" ,"1", "4.1", "1"), // bus_tempSensor("500", "4", "500", "4.500", "500"), - // bus_energy("51", "18", "51", "18.51", "51"); + bus_energy(new WhereEnergyManagement("51"), Who.fromValue(18), "*#18*51*113##", "51", "18.51", "51"); // @formatter:on diff --git a/bundles/org.openhab.binding.oppo/README.md b/bundles/org.openhab.binding.oppo/README.md index f3a36a167892d..a55bfff477953 100644 --- a/bundles/org.openhab.binding.oppo/README.md +++ b/bundles/org.openhab.binding.oppo/README.md @@ -44,7 +44,7 @@ The thing has the following configuration parameters: | Address | host | Host name or IP address of the Oppo player or serial over IP device. | host name or ip | | Port | port | Communication port for using serial over IP. Leave blank if using direct IP connection to the player. | ip port number | | Serial Port | serialPort | Serial port to use for directly connecting to the Oppo player | a comm port name | -| Verbose Mode | verboseMode | (Optional) If true, the player will send time updates every second. If set false, the binding polls the player every 15 seconds. | Boolean; default false | +| Verbose Mode | verboseMode | (Optional) If true, the player will send time updates every second. If set false, the binding polls the player every 10 seconds. | Boolean; default false | Some notes: @@ -54,9 +54,10 @@ Some notes: * The UDP-20x series should be fully functional over direct IP connection but this was not able to be tested by the developer. * As previously noted, when using verbose mode, the player will send time code messages once per second while playback is ongoing. * Be aware that this could cause performance impacts to your openHAB system. -* In non-verbose (the default), the binding will poll the player every 15 seconds to update play time, track and chapter information instead. +* In non-verbose (the default), the binding will poll the player every 10 seconds to update play time, track and chapter information instead. * In order for the direct IP connection to work while the player is turned off, the Standby Mode setting must be set to "Quick Start" in the Device Setup menu. * Likewise if the player is turned off, it may not be discoverable by the Binding's discovery scan. +* If the player is switched off when the binding first starts up or if power to the player is ever interrupted, up to 30 seconds may elapse before the binding begins to update when the player is switched on. * If you experience any issues using the binding, first ensure that the player's firmware is up to date with the latest available version (especially on the older models). * For the older models, some of the features in the control API were added after the players were shipped. * Available HDMI modes for BDP-83 & BDP-9x: AUTO, SRC, 1080P, 1080I, 720P, SDP, SDI diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java index 8055763283351..0ec5691a106d7 100644 --- a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java +++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.oppo.internal.communication; +import static org.openhab.binding.oppo.internal.OppoBindingConstants.*; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -38,6 +40,8 @@ public abstract class OppoConnector { private static final Pattern QRY_PATTERN = Pattern.compile("^@(Q[A-Z0-9]{2}|VUP|VDN) OK (.*)$"); private static final Pattern STUS_PATTERN = Pattern.compile("^@(U[A-Z0-9]{2}) (.*)$"); + private static final String OK_ON = "@OK ON"; + private static final String OK_OFF = "@OK OFF"; private static final String NOP_OK = "@NOP OK"; private static final String NOP = "NOP"; private static final String OK = "OK"; @@ -249,6 +253,17 @@ public void handleIncomingMessage(byte[] incomingMessage) { return; } + // Before verbose mode 2 & 3 get set, these are the responses to QPW + if (OK_ON.equals(message)) { + dispatchKeyValue(QPW, ON); + return; + } + + if (OK_OFF.equals(message)) { + dispatchKeyValue(QPW, OFF); + return; + } + // Player sent an OK response to a query: @QDT OK DVD-VIDEO or a volume update @VUP OK 100 Matcher matcher = QRY_PATTERN.matcher(message); if (matcher.find()) { diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java index 947c477e575b7..878aa03f9ed4f 100644 --- a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java +++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java @@ -148,7 +148,7 @@ protected void startScan() { multiSocket.receive(receivePacket); String message = new String(receivePacket.getData(), StandardCharsets.US_ASCII).trim(); - if (message != null && message.length() > 0) { + if (message.length() > 0) { messageReceive(message); } } catch (SocketTimeoutException e) { @@ -158,7 +158,7 @@ protected void startScan() { multiSocket.close(); } catch (IOException e) { - if (!e.getMessage().contains("No IP addresses bound to interface")) { + if (e.getMessage() != null && !e.getMessage().contains("No IP addresses bound to interface")) { logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e); } } diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java index 3a1c862897436..c85182ec610a6 100644 --- a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java +++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java @@ -69,8 +69,8 @@ @NonNullByDefault public class OppoHandler extends BaseThingHandler implements OppoMessageEventListener { private static final long RECON_POLLING_INTERVAL_SEC = 60; - private static final long POLLING_INTERVAL_SEC = 15; - private static final long INITIAL_POLLING_DELAY_SEC = 10; + private static final long POLLING_INTERVAL_SEC = 10; + private static final long INITIAL_POLLING_DELAY_SEC = 5; private static final long SLEEP_BETWEEN_CMD_MS = 100; private static final Pattern TIME_CODE_PATTERN = Pattern @@ -221,7 +221,7 @@ public void dispose() { * * @param channelUID the channel sending the command * @param command the command received - * + * */ @Override public void handleCommand(ChannelUID channelUID, Command command) { @@ -615,6 +615,8 @@ private void scheduleReconnectJob() { closeConnection(); } else { updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + isInitialQuery = false; + isVbModeSet = false; } } } @@ -647,19 +649,21 @@ private void schedulePollingJob() { synchronized (sequenceLock) { try { - // the verbose mode must be set while the player is on - if (isPowerOn && !isVbModeSet && !isBdpIP) { - connector.sendCommand(OppoCommand.SET_VERBOSE_MODE, this.verboseMode); - isVbModeSet = true; - Thread.sleep(SLEEP_BETWEEN_CMD_MS); + // Verbose mode 2 & 3 only do once until power comes on OR always for BDP direct IP + if ((!isPowerOn && !isInitialQuery) || isBdpIP) { + connector.sendCommand(OppoCommand.QUERY_POWER_STATUS); } - // If using direct serial connection, the query is done once after the player is turned on - // - OR - if using direct IP connection on the 83/9x/10x, no unsolicited updates are sent - // so we must query everything to know what changed. - if ((isPowerOn && !isInitialQuery) || isBdpIP) { - connector.sendCommand(OppoCommand.QUERY_POWER_STATUS); - if (isPowerOn) { + if (isPowerOn) { + // the verbose mode must be set while the player is on + if (!isVbModeSet && !isBdpIP) { + connector.sendCommand(OppoCommand.SET_VERBOSE_MODE, this.verboseMode); + isVbModeSet = true; + Thread.sleep(SLEEP_BETWEEN_CMD_MS); + } + + // Verbose mode 2 & 3 only do once OR always for BDP direct IP + if (!isInitialQuery || isBdpIP) { isInitialQuery = true; OppoCommand.QUERY_COMMANDS.forEach(cmd -> { try { @@ -670,34 +674,34 @@ private void schedulePollingJob() { } }); } - } - // for Verbose mode 2 get the current play back time if we are playing, otherwise just do NO_OP - if ((VERBOSE_2.equals(this.verboseMode) && PLAY.equals(currentPlayMode)) - || (isBdpIP && isPowerOn)) { - switch (currentTimeMode) { - case T: - connector.sendCommand(OppoCommand.QUERY_TITLE_ELAPSED); - break; - case X: - connector.sendCommand(OppoCommand.QUERY_TITLE_REMAIN); - break; - case C: - connector.sendCommand(OppoCommand.QUERY_CHAPTER_ELAPSED); - break; - case K: - connector.sendCommand(OppoCommand.QUERY_CHAPTER_REMAIN); - break; - } - Thread.sleep(SLEEP_BETWEEN_CMD_MS); + // for Verbose mode 2 get the current play back time if we are playing, otherwise just do + // NO_OP + if ((VERBOSE_2.equals(this.verboseMode) && PLAY.equals(currentPlayMode)) || isBdpIP) { + switch (currentTimeMode) { + case T: + connector.sendCommand(OppoCommand.QUERY_TITLE_ELAPSED); + break; + case X: + connector.sendCommand(OppoCommand.QUERY_TITLE_REMAIN); + break; + case C: + connector.sendCommand(OppoCommand.QUERY_CHAPTER_ELAPSED); + break; + case K: + connector.sendCommand(OppoCommand.QUERY_CHAPTER_REMAIN); + break; + } + Thread.sleep(SLEEP_BETWEEN_CMD_MS); - // make queries to refresh total number of titles/tracks & chapters - connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK); - Thread.sleep(SLEEP_BETWEEN_CMD_MS); - connector.sendCommand(OppoCommand.QUERY_CHAPTER); - } else if (!isBdpIP) { - // verbose mode 3 - connector.sendCommand(OppoCommand.NO_OP); + // make queries to refresh total number of titles/tracks & chapters + connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK); + Thread.sleep(SLEEP_BETWEEN_CMD_MS); + connector.sendCommand(OppoCommand.QUERY_CHAPTER); + } else if (!isBdpIP) { + // verbose mode 3 + connector.sendCommand(OppoCommand.NO_OP); + } } } catch (OppoException | InterruptedException e) { diff --git a/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/discovery/SocketDiscoveryService.java b/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/discovery/SocketDiscoveryService.java index 340aec98a3a27..62eb39b16209c 100644 --- a/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/discovery/SocketDiscoveryService.java +++ b/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/discovery/SocketDiscoveryService.java @@ -17,7 +17,6 @@ import java.util.Map; import java.util.Set; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.orvibo.internal.OrviboBindingConstants; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; @@ -102,7 +101,7 @@ public void socketDiscovered(Socket socket) { private DiscoveryResult createDiscoveryResult(Socket socket) { ThingUID thingUID = getUID(socket); String label = socket.getLabel(); - if (StringUtils.isBlank(label)) { + if (label == null || label.isBlank()) { label = "S20"; } return DiscoveryResultBuilder.create(thingUID).withLabel(label) diff --git a/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/handler/S20Handler.java b/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/handler/S20Handler.java index add5b3d1e0754..d8cdfa4b630a7 100644 --- a/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/handler/S20Handler.java +++ b/bundles/org.openhab.binding.orvibo/src/main/java/org/openhab/binding/orvibo/internal/handler/S20Handler.java @@ -18,7 +18,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -98,7 +97,7 @@ private void configure() { @Override public void socketDidChangeLabel(Socket socket, String label) { - if (!StringUtils.isBlank(label)) { + if (label != null && !label.isBlank()) { logger.debug("Updating thing label to {}", label); thing.setLabel(label); } diff --git a/bundles/org.openhab.binding.orvibo/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.orvibo/src/main/resources/OH-INF/thing/thing-types.xml index 178fe6a97f7b0..1ea38ed30e452 100644 --- a/bundles/org.openhab.binding.orvibo/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.orvibo/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,7 +14,6 @@ The MAC address identifies a specific S20 Socket. - true diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/resources/OH-INF/thing/ip150connector.xml b/bundles/org.openhab.binding.paradoxalarm/src/main/resources/OH-INF/thing/ip150connector.xml index ccc91b45c52c4..7eaa1e0c53f6b 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/resources/OH-INF/thing/ip150connector.xml +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/resources/OH-INF/thing/ip150connector.xml @@ -27,7 +27,6 @@ Password for accessing IP150 password - true @@ -39,7 +38,6 @@ IP address or host name of IP150 network-address - true diff --git a/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java b/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java index d8e646565909d..7f9f893fc904e 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java @@ -100,7 +100,7 @@ private static void readArguments(String[] args) { ipAddress = parser.getIpAddress(); logger.info("IP150 IP Address: {}", ipAddress); - port = new Integer(parser.getPort()); + port = Integer.parseInt(parser.getPort()); logger.info("IP150 port: {}", port); } } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java index 52eba89e936ef..28798ec6d0b3d 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.pentair.internal.config; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Configuration parameters for IP Bridge diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java index 4e5f4d7d0d310..86ada023a33c5 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.pentair.internal.config; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Configuration parameters for Serial Bridge diff --git a/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/PHCHelper.java b/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/PHCHelper.java index cd637b52b40c1..f5f20a4d2df5b 100644 --- a/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/PHCHelper.java +++ b/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/PHCHelper.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.phc.internal; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; diff --git a/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/handler/PHCBridgeHandler.java b/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/handler/PHCBridgeHandler.java index 77c1fd3040556..d4c0ceb14ee68 100644 --- a/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/handler/PHCBridgeHandler.java +++ b/bundles/org.openhab.binding.phc/src/main/java/org/openhab/binding/phc/internal/handler/PHCBridgeHandler.java @@ -24,7 +24,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledThreadPoolExecutor; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.phc.internal.PHCBindingConstants; diff --git a/bundles/org.openhab.binding.phc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.phc/src/main/resources/OH-INF/thing/thing-types.xml index e6a0c2a31cf81..c0de447653961 100644 --- a/bundles/org.openhab.binding.phc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.phc/src/main/resources/OH-INF/thing/thing-types.xml @@ -10,10 +10,9 @@ STM. - + Serial Port the PHC modules are connected to - true serial-port false @@ -35,10 +34,9 @@ - + Address of the module as binary, like the DIP switches. - true @@ -58,10 +56,9 @@ - + Address of the module as binary, like the DIP switches. - true @@ -80,10 +77,9 @@ - + Address of the module as binary, like the DIP switches. - true true @@ -125,10 +121,9 @@ - + Address of the module as binary, like the DIP switches. - true true diff --git a/bundles/org.openhab.binding.pilight/NOTICE b/bundles/org.openhab.binding.pilight/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.pilight/README.md b/bundles/org.openhab.binding.pilight/README.md new file mode 100644 index 0000000000000..84d35ac52ac16 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/README.md @@ -0,0 +1,119 @@ +# pilight Binding + +The pilight binding allows openHAB to communicate with a [pilight](http://www.pilight.org/) instance running pilight +version 6.0 or greater. + +> pilight is a free open source full fledge domotica solution that runs on a Raspberry Pi, HummingBoard, BananaPi, +> Radxa, but also on *BSD and various linuxes (tested on Arch, Ubuntu and Debian). It's open source and freely available +> for anyone. pilight works with a great deal of devices and is frequency independent. Therefor, it can control devices +> working at 315Mhz, 433Mhz, 868Mhz etc. Support for these devices are dependent on community, because we as developers +> don't own them all. + +pilight is a cheap way to control 'Click On Click Off' devices. It started as an application for the Raspberry Pi (using +the GPIO interface) but it's also possible now to connect it to any other PC using an Arduino Nano. You will need a +cheap 433Mhz transceiver in both cases. See the [Pilight manual](https://manual.pilight.org/electronics/wiring.html) for +more information. + +## Supported Things + +| Thing | Type | Description | +|-----------|--------|----------------------------------------------------------------------------| +| `bridge` | Bridge | Pilight bridge required for the communication with the pilight daemon. | +| `contact` | Thing | Pilight contact (read-only). | +| `dimmer` | Thing | Pilight dimmer. | +| `switch` | Thing | Pilight switch. | +| `generic` | Thing | Pilight generic device for which you have to add the channels dynamically. | + +## Binding Configuration + +### `bridge` Thing + +A `bridge` is required for the communication with a pilight daemon. Multiple pilight instances are supported by creating +different pilight `bridge` things. + +The `bridge` requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | +|-----------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| IP Address | ipAddress | Host name or IP address of the pilight daemon | yes | +| Port | port | Port number on which the pilight daemon is listening. Default: 5000 | yes | +| Delay | delay | Delay (in millisecond) between consecutive commands. Recommended value without band pass filter: 1000. Recommended value with band pass filter: somewhere between 200-500. Default: 500 | no | + +Important: you must explicitly configure the port in the pilight daemon config or otherwise a random port will be used +and the binding will not be able to connect. + +### `contact`, `dimmer`, `switch`, `generic` Things + +These things have all one required parameter: + +| Parameter Label | Parameter ID | Description | Required | +|-----------------|--------------|------------------------|----------| +| Name | name | Name of pilight device | yes | + +## Channels + +The `bridge` thing has no channels. + +The `contact`, `dimmer` and `switch` things all have one channel: + +| Thing | Channel | Type | Description | +|-----------|----------|---------|-------------------------| +| `contact` | state | Contact | State of the contact | +| `dimmer` | dimlevel | Dimmer | Dim level of the dimmer | +| `switch` | state | Switch | State of the switch | + +The `generic` thing has no fixed channels, so you have to add them manually. Currently, only String and Number channels +are supported. + +## Auto Discovery + +### Bridge Auto Discovery + +The pilight daemon implements a SSDP interface, which can be used to search for running pilight daemon instances by +sending a SSDP request via multicast udp (this mechanism may only work if +the [standalone mode](https://manual.pilight.org/configuration/settings.html#standalone) in the pilight daemon is +disabled. After loading the binding this bridge discovery is automatically run and scheduled to scan for bridges every +10 minutes. + +### Device Auto Discovery + +After a `bridge` thing has been configured in openHAB, it automatically establishes a connection between pilight daemon +and openHAB. As soon as the bridge is connected, the devices configured in the pilight daemon are automatically found +via autodiscovery in background (or via a manually triggered discovery) and are displayed in the inbox to easily create +things from them. + +## Full Example + +things/pilight.things + +``` +Bridge pilight:bridge:raspi "Pilight Daemon raspi" [ ipAddress="192.168.1.1", port=5000 ] { + Thing switch office "Office" [ name="office" ] + Thing dimmer piano "Piano" [ name="piano" ] + Thing generic weather "Weather" [ name="weather" ] { + Channels: + State Number : temperature [ property="temperature"] + State Number : humidity [ property="humidity"] + } +} +``` + +items/pilight.items + +``` +Switch office_switch "Büro" { channel="pilight:switch:raspi:office:state" } +Dimmer piano_light "Klavier [%.0f %%]" { channel="pilight:dimmer:raspi:piano:dimlevel" } +Number weather_temperature "Aussentemperatur [%.1f °C]" { channel="pilight:generic:raspi:weather:temperature" } +Number weather_humidity "Feuchtigkeit [%.0f %%]" { channel="pilight:generic:raspi:weather:humidity" } + +``` + +sitemaps/fragment.sitemap + +``` +Switch item=office_switch +Slider item=piano_light +Text item=weather_temperature +Text item=weather_humidity +``` + diff --git a/bundles/org.openhab.binding.pilight/pom.xml b/bundles/org.openhab.binding.pilight/pom.xml new file mode 100644 index 0000000000000..1acde190f7394 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.pilight + + openHAB Add-ons :: Bundles :: Pilight Binding + + diff --git a/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml b/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml new file mode 100644 index 0000000000000..38a968d3f2ba8 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.pilight/${project.version} + + diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java new file mode 100644 index 0000000000000..9caa84f3f976f --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.dto.Config; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.binding.pilight.internal.dto.Version; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; + +/** + * Callback interface to signal any listeners that an update was received from pilight + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public interface IPilightCallback { + + /** + * Update thing status + * + * @param status status of thing + * @param statusDetail status detail of thing + * @param description description of thing status + */ + void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description); + + /** + * Update for one or more device received. + * + * @param allStatus list of Object containing list of devices that were updated and their current state + */ + void statusReceived(List allStatus); + + /** + * Configuration received. + * + * @param config Object containing configuration of pilight + */ + void configReceived(Config config); + + /** + * Version information received. + * + * @param version Object containing software version information of pilight daemon + */ + void versionReceived(Version version); +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java new file mode 100644 index 0000000000000..bcf731315b83c --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link PilightBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightBindingConstants { + + public static final String BINDING_ID = "pilight"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_CONTACT = new ThingTypeUID(BINDING_ID, "contact"); + public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer"); + public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch"); + public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "generic"); + + // List of property names + public static final String PROPERTY_IP_ADDRESS = "ipAddress"; + public static final String PROPERTY_PORT = "port"; + public static final String PROPERTY_NAME = "name"; + + // List of all Channel ids + public static final String CHANNEL_STATE = "state"; + public static final String CHANNEL_DIMLEVEL = "dimlevel"; +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java new file mode 100644 index 0000000000000..85dce4004946f --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PilightBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightBridgeConfiguration { + + private String ipAddress = ""; + private int port = 0; + private int delay = 500; + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public int getDelay() { + return delay; + } + + public void setDelay(Integer delay) { + this.delay = delay; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java new file mode 100644 index 0000000000000..594eff9d77088 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PilightChannelConfiguration} class contains fields mapping channel configuration parameters. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightChannelConfiguration { + private String property = ""; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java new file mode 100644 index 0000000000000..75fca4f9cf4e2 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import java.io.*; +import java.net.Socket; +import java.util.Collections; +import java.util.concurrent.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.dto.*; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This class listens for updates from the pilight daemon. It is also responsible for requesting + * and propagating the current pilight configuration. + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + * + */ +@NonNullByDefault +public class PilightConnector implements Runnable, Closeable { + + private static final int RECONNECT_DELAY_MSEC = 10 * 1000; // 10 seconds + + private final Logger logger = LoggerFactory.getLogger(PilightConnector.class); + + private final PilightBridgeConfiguration config; + + private final IPilightCallback callback; + + private final ObjectMapper inputMapper = new ObjectMapper( + new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)); + + private final ObjectMapper outputMapper = new ObjectMapper( + new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)) + .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); + + private @Nullable Socket socket; + private @Nullable PrintStream printStream; + + private final ScheduledExecutorService scheduler; + private final ConcurrentLinkedQueue delayedActionQueue = new ConcurrentLinkedQueue<>(); + private @Nullable ScheduledFuture delayedActionWorkerFuture; + + public PilightConnector(final PilightBridgeConfiguration config, final IPilightCallback callback, + final ScheduledExecutorService scheduler) { + this.config = config; + this.callback = callback; + this.scheduler = scheduler; + } + + @Override + public void run() { + try { + connect(); + + while (!Thread.currentThread().isInterrupted()) { + try { + final @Nullable Socket socket = this.socket; + if (socket != null && !socket.isClosed()) { + try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { + String line = in.readLine(); + while (!Thread.currentThread().isInterrupted() && line != null) { + if (!line.isEmpty()) { + logger.trace("Received from pilight: {}", line); + final ObjectMapper inputMapper = this.inputMapper; + if (line.startsWith("{\"message\":\"config\"")) { + final @Nullable Message message = inputMapper.readValue(line, Message.class); + callback.configReceived(message.getConfig()); + } else if (line.startsWith("{\"message\":\"values\"")) { + final @Nullable AllStatus status = inputMapper.readValue(line, AllStatus.class); + callback.statusReceived(status.getValues()); + } else if (line.startsWith("{\"version\":")) { + final @Nullable Version version = inputMapper.readValue(line, Version.class); + callback.versionReceived(version); + } else if (line.startsWith("{\"status\":")) { + // currently unused + } else if (line.equals("1")) { + throw new IOException("Connection to pilight lost"); + } else { + final @Nullable Status status = inputMapper.readValue(line, Status.class); + callback.statusReceived(Collections.singletonList(status)); + } + } + + line = in.readLine(); + } + } + } + } catch (IOException e) { + if (!Thread.currentThread().isInterrupted()) { + logger.debug("Error in pilight listener thread: {}", e.getMessage()); + } + } + + logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort()); + + if (!Thread.currentThread().isInterrupted()) { + callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, null); + // empty line received (socket closed) or pilight stopped but binding + // is still running, try to reconnect + connect(); + } + } + + } catch (InterruptedException e) { + logger.debug("Interrupting thread."); + Thread.currentThread().interrupt(); + } + } + + /** + * Tells the connector to refresh the configuration + */ + public void refreshConfig() { + doSendAction(new Action(Action.ACTION_REQUEST_CONFIG)); + } + + /** + * Tells the connector to refresh the status of all devices + */ + public void refreshStatus() { + doSendAction(new Action(Action.ACTION_REQUEST_VALUES)); + } + + /** + * Stops the listener + */ + public void close() { + disconnect(); + Thread.currentThread().interrupt(); + } + + private void disconnect() { + final @Nullable PrintStream printStream = this.printStream; + if (printStream != null) { + printStream.close(); + this.printStream = null; + } + + final @Nullable Socket socket = this.socket; + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + logger.debug("Error while closing pilight socket: {}", e.getMessage()); + } + this.socket = null; + } + } + + private boolean isConnected() { + final @Nullable Socket socket = this.socket; + return socket != null && !socket.isClosed(); + } + + private void connect() throws InterruptedException { + disconnect(); + + int delay = 0; + + while (!isConnected()) { + try { + logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort()); + + Thread.sleep(delay); + Socket socket = new Socket(config.getIpAddress(), config.getPort()); + + Options options = new Options(); + options.setConfig(true); + + Identification identification = new Identification(); + identification.setOptions(options); + + // For some reason, directly using the outputMapper to write to the socket's OutputStream doesn't work. + PrintStream printStream = new PrintStream(socket.getOutputStream(), true); + printStream.println(outputMapper.writeValueAsString(identification)); + + final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class); + + if (response.getStatus().equals(Response.SUCCESS)) { + logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(), + config.getPort()); + this.socket = socket; + this.printStream = printStream; + callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + } else { + printStream.close(); + socket.close(); + logger.debug("pilight client not accepted: {}", response.getStatus()); + } + } catch (IOException e) { + final @Nullable PrintStream printStream = this.printStream; + if (printStream != null) { + printStream.close(); + } + logger.debug("connect failed: {}", e.getMessage()); + callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + + delay = RECONNECT_DELAY_MSEC; + } + } + + /** + * send action to pilight daemon + * + * @param action action to send + */ + public void sendAction(Action action) { + delayedActionQueue.add(action); + final @Nullable ScheduledFuture delayedActionWorkerFuture = this.delayedActionWorkerFuture; + + if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) { + this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> { + if (!delayedActionQueue.isEmpty()) { + doSendAction(delayedActionQueue.poll()); + } else { + final @Nullable ScheduledFuture workerFuture = this.delayedActionWorkerFuture; + if (workerFuture != null) { + workerFuture.cancel(false); + } + this.delayedActionWorkerFuture = null; + } + }, 0, config.getDelay(), TimeUnit.MILLISECONDS); + } + } + + private void doSendAction(Action action) { + final @Nullable PrintStream printStream = this.printStream; + if (printStream != null) { + try { + printStream.println(outputMapper.writeValueAsString(action)); + } catch (IOException e) { + logger.debug("Error while sending action '{}' to pilight server: {}", action.getAction(), + e.getMessage()); + } + } else { + logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction()); + } + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java new file mode 100644 index 0000000000000..b5cbff1f000ab --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PilightDeviceConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightDeviceConfiguration { + + private String name = ""; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java new file mode 100644 index 0000000000000..366b2e4b21de8 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal; + +import static org.openhab.binding.pilight.internal.PilightBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.handler.PilightBridgeHandler; +import org.openhab.binding.pilight.internal.handler.PilightContactHandler; +import org.openhab.binding.pilight.internal.handler.PilightDimmerHandler; +import org.openhab.binding.pilight.internal.handler.PilightGenericHandler; +import org.openhab.binding.pilight.internal.handler.PilightSwitchHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link PilightHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +@Component(configurationPid = "binding.pilight", service = ThingHandlerFactory.class) +public class PilightHandlerFactory extends BaseThingHandlerFactory { + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_CONTACT, + THING_TYPE_DIMMER, THING_TYPE_GENERIC, THING_TYPE_SWITCH); + + private final ChannelTypeRegistry channelTypeRegistry; + + @Activate + public PilightHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry) { + this.channelTypeRegistry = channelTypeRegistry; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new PilightBridgeHandler((Bridge) thing); + } + + if (THING_TYPE_CONTACT.equals(thingTypeUID)) { + return new PilightContactHandler(thing); + } + + if (THING_TYPE_DIMMER.equals(thingTypeUID)) { + return new PilightDimmerHandler(thing); + } + + if (THING_TYPE_GENERIC.equals(thingTypeUID)) { + return new PilightGenericHandler(thing, channelTypeRegistry); + } + + if (THING_TYPE_SWITCH.equals(thingTypeUID)) { + return new PilightSwitchHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java new file mode 100644 index 0000000000000..67cb46bd87b6e --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.discovery; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.PilightBindingConstants; +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.config.discovery.DiscoveryService; +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 PilightBridgeDiscoveryService} is responsible for discovering new pilight daemons on the network + * by sending a ssdp multicast request via udp. + * + * @author Niklas Dörfler - Initial contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pilight") +public class PilightBridgeDiscoveryService extends AbstractDiscoveryService { + + private static final int AUTODISCOVERY_SEARCH_TIME_SEC = 5; + private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 60 * 10; + + private static final String SSDP_DISCOVERY_REQUEST_MESSAGE = "M-SEARCH * HTTP/1.1\r\n" + + "Host:239.255.255.250:1900\r\n" + "ST:urn:schemas-upnp-org:service:pilight:1\r\n" + + "Man:\"ssdp:discover\"\r\n" + "MX:3\r\n\r\n"; + public static final String SSDP_MULTICAST_ADDRESS = "239.255.255.250"; + public static final int SSDP_PORT = 1900; + public static final int SSDP_WAIT_TIMEOUT = 2000; // in milliseconds + + private final Logger logger = LoggerFactory.getLogger(PilightBridgeDiscoveryService.class); + + private @Nullable ScheduledFuture backgroundDiscoveryJob; + + public PilightBridgeDiscoveryService() throws IllegalArgumentException { + super(getSupportedThingTypeUIDs(), AUTODISCOVERY_SEARCH_TIME_SEC, true); + } + + public static Set getSupportedThingTypeUIDs() { + return Collections.singleton(PilightBindingConstants.THING_TYPE_BRIDGE); + } + + @Override + protected void startScan() { + logger.debug("Pilight bridge discovery scan started"); + removeOlderResults(getTimestampOfLastScan()); + try { + List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface nic : interfaces) { + Enumeration inetAddresses = nic.getInetAddresses(); + for (InetAddress inetAddress : Collections.list(inetAddresses)) { + if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { + DatagramSocket ssdp = new DatagramSocket( + new InetSocketAddress(inetAddress.getHostAddress(), 0)); + byte[] buff = SSDP_DISCOVERY_REQUEST_MESSAGE.getBytes(StandardCharsets.UTF_8); + DatagramPacket sendPack = new DatagramPacket(buff, buff.length); + sendPack.setAddress(InetAddress.getByName(SSDP_MULTICAST_ADDRESS)); + sendPack.setPort(SSDP_PORT); + ssdp.send(sendPack); + ssdp.setSoTimeout(SSDP_WAIT_TIMEOUT); + + boolean loop = true; + while (loop) { + DatagramPacket recvPack = new DatagramPacket(new byte[1024], 1024); + ssdp.receive(recvPack); + byte[] recvData = recvPack.getData(); + + final Scanner scanner = new Scanner(new ByteArrayInputStream(recvData), + StandardCharsets.UTF_8); + loop = scanner.findAll("Location:([0-9.]+):(.*)").peek(matchResult -> { + final String server = matchResult.group(1); + final Integer port = Integer.parseInt(matchResult.group(2)); + final String bridgeName = server.replace(".", "") + "" + port; + + logger.debug("Found pilight daemon at {}:{}", server, port); + + Map properties = new HashMap<>(); + properties.put(PilightBindingConstants.PROPERTY_IP_ADDRESS, server); + properties.put(PilightBindingConstants.PROPERTY_PORT, port); + properties.put(PilightBindingConstants.PROPERTY_NAME, bridgeName); + + ThingUID uid = new ThingUID(PilightBindingConstants.THING_TYPE_BRIDGE, bridgeName); + + DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties) + .withRepresentationProperty(PilightBindingConstants.PROPERTY_NAME) + .withLabel("Pilight Bridge (" + server + ")").build(); + + thingDiscovered(result); + }).count() == 0; + } + } + } + } + } catch (IOException e) { + if (e.getMessage() != null && !"Receive timed out".equals(e.getMessage())) { + logger.warn("Unable to enumerate the local network interfaces {}", e.getMessage()); + } + } + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + @Override + protected void startBackgroundDiscovery() { + logger.debug("Start Pilight device background discovery"); + final @Nullable ScheduledFuture backgroundDiscoveryJob = this.backgroundDiscoveryJob; + if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) { + this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5, + AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS); + } + } + + @Override + protected void stopBackgroundDiscovery() { + logger.debug("Stop Pilight device background discovery"); + final @Nullable ScheduledFuture backgroundDiscoveryJob = this.backgroundDiscoveryJob; + if (backgroundDiscoveryJob != null) { + backgroundDiscoveryJob.cancel(true); + this.backgroundDiscoveryJob = null; + } + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java new file mode 100644 index 0000000000000..c735a976e36a5 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.discovery; + +import static org.openhab.binding.pilight.internal.PilightBindingConstants.*; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.PilightHandlerFactory; +import org.openhab.binding.pilight.internal.dto.Config; +import org.openhab.binding.pilight.internal.dto.DeviceType; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.binding.pilight.internal.handler.PilightBridgeHandler; +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.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightDeviceDiscoveryService} discovers pilight devices after a bridge thing has been created and + * connected to the pilight daemon. Things are discovered periodically in the background or after a manual trigger. + * + * @author Niklas Dörfler - Initial contribution + */ +@NonNullByDefault +public class PilightDeviceDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private static final Set SUPPORTED_THING_TYPES_UIDS = PilightHandlerFactory.SUPPORTED_THING_TYPES_UIDS; + + private static final int AUTODISCOVERY_SEARCH_TIME_SEC = 10; + private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 60 * 10; + + private final Logger logger = LoggerFactory.getLogger(PilightDeviceDiscoveryService.class); + + private @Nullable PilightBridgeHandler pilightBridgeHandler; + private @Nullable ThingUID bridgeUID; + + private @Nullable ScheduledFuture backgroundDiscoveryJob; + private CompletableFuture configFuture; + private CompletableFuture> statusFuture; + + public PilightDeviceDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, AUTODISCOVERY_SEARCH_TIME_SEC); + configFuture = new CompletableFuture<>(); + statusFuture = new CompletableFuture<>(); + } + + @Override + protected void startScan() { + if (pilightBridgeHandler != null) { + configFuture = new CompletableFuture<>(); + statusFuture = new CompletableFuture<>(); + + configFuture.thenAcceptBoth(statusFuture, (config, allStatus) -> { + removeOlderResults(getTimestampOfLastScan(), bridgeUID); + config.getDevices().forEach((deviceId, device) -> { + if (this.pilightBridgeHandler != null) { + final Optional status = allStatus.stream() + .filter(s -> s.getDevices().contains(deviceId)).findFirst(); + + final ThingTypeUID thingTypeUID; + final String typeString; + + if (status.isPresent()) { + if (status.get().getType().equals(DeviceType.SWITCH)) { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_SWITCH.getId()); + typeString = "Switch"; + } else if (status.get().getType().equals(DeviceType.DIMMER)) { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_DIMMER.getId()); + typeString = "Dimmer"; + } else if (status.get().getType().equals(DeviceType.VALUE)) { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId()); + typeString = "Generic"; + } else if (status.get().getType().equals(DeviceType.CONTACT)) { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_CONTACT.getId()); + typeString = "Contact"; + } else { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId()); + typeString = "Generic"; + } + } else { + thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId()); + typeString = "Generic"; + } + + final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler; + if (pilightBridgeHandler != null) { + final ThingUID thingUID = new ThingUID(thingTypeUID, + pilightBridgeHandler.getThing().getUID(), deviceId); + + final Map properties = new HashMap<>(); + properties.put(PROPERTY_NAME, deviceId); + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) + .withThingType(thingTypeUID).withProperties(properties).withBridge(bridgeUID) + .withRepresentationProperty(PROPERTY_NAME) + .withLabel("Pilight " + typeString + " Device '" + deviceId + "'").build(); + + thingDiscovered(discoveryResult); + } + } + }); + }); + + final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler; + if (pilightBridgeHandler != null) { + pilightBridgeHandler.refreshConfigAndStatus(); + } + } + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + configFuture.cancel(true); + statusFuture.cancel(true); + if (bridgeUID != null) { + removeOlderResults(getTimestampOfLastScan(), bridgeUID); + } + } + + @Override + protected void startBackgroundDiscovery() { + logger.debug("Start Pilight device background discovery"); + final @Nullable ScheduledFuture backgroundDiscoveryJob = this.backgroundDiscoveryJob; + if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) { + this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 20, + AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS); + } + } + + @Override + protected void stopBackgroundDiscovery() { + logger.debug("Stop Pilight device background discovery"); + final @Nullable ScheduledFuture backgroundDiscoveryJob = this.backgroundDiscoveryJob; + if (backgroundDiscoveryJob != null) { + backgroundDiscoveryJob.cancel(true); + this.backgroundDiscoveryJob = null; + } + } + + @Override + public void setThingHandler(final ThingHandler handler) { + if (handler instanceof PilightBridgeHandler) { + this.pilightBridgeHandler = (PilightBridgeHandler) handler; + final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler; + if (pilightBridgeHandler != null) { + bridgeUID = pilightBridgeHandler.getThing().getUID(); + } + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return pilightBridgeHandler; + } + + @Override + public void activate() { + super.activate(null); + final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler; + if (pilightBridgeHandler != null) { + pilightBridgeHandler.registerDiscoveryListener(this); + } + } + + @Override + public void deactivate() { + if (bridgeUID != null) { + removeOlderResults(getTimestampOfLastScan(), bridgeUID); + } + + final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler; + if (pilightBridgeHandler != null) { + pilightBridgeHandler.unregisterDiscoveryListener(); + } + + super.deactivate(); + } + + /** + * Method used to get pilight device config into the discovery class. + * + * @param config config to get + */ + public void setConfig(Config config) { + configFuture.complete(config); + } + + /** + * Method used to get pilight device status list into the discovery class. + * + * @param status list of status objects + */ + public void setStatus(List status) { + statusFuture.complete(status); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_THING_TYPES_UIDS; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java new file mode 100644 index 0000000000000..ed0a9259295db --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * This message is sent when we want to change the state of a device or request the + * current configuration in pilight. + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Action { + + public static final String ACTION_SEND = "send"; + + public static final String ACTION_CONTROL = "control"; + + public static final String ACTION_REQUEST_CONFIG = "request config"; + + public static final String ACTION_REQUEST_VALUES = "request values"; + + private String action; + + private Code code; + + private Options options; + + public Action(String action) { + this.action = action; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public Code getCode() { + return code; + } + + public void setCode(Code code) { + this.code = code; + } + + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java new file mode 100644 index 0000000000000..69418aa581ea0 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * All status messages. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class AllStatus { + + private String message; + + private List values = new ArrayList<>(); + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java new file mode 100644 index 0000000000000..9cc253bc1407e --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * Part of the {@link Action} message that is sent to pilight. + * This contains the desired state for a single device. + * + * {@link http://www.pilight.org/development/api/#sender} + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Code { + + public static final String STATE_ON = "on"; + + public static final String STATE_OFF = "off"; + + private String device; + + private String state; + + private Values values; + + public String getDevice() { + return device; + } + + public void setDevice(String device) { + this.device = device; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public Values getValues() { + return values; + } + + public void setValues(Values values) { + this.values = values; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java new file mode 100644 index 0000000000000..6f368a62f1d4d --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * pilight configuration object + * + * {@link http://www.pilight.org/development/api/#controller} + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Config { + + private Map devices; + + public Map getDevices() { + return devices; + } + + public void setDevices(Map devices) { + this.devices = devices; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java new file mode 100644 index 0000000000000..62c6c26cdcdb4 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Class describing a device in pilight + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Device { + + private String uuid; + + private String origin; + + private String timestamp; + + private List protocol; + + private String state; + + private Integer dimlevel = null; + + // @SerializedName("dimlevel-maximum") + private Integer dimlevelMaximum = null; + + private Integer dimlevelMinimum = null; + + private List> id; + + private Map properties = new HashMap<>(); + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public List getProtocol() { + return protocol; + } + + public void setProtocol(List protocol) { + this.protocol = protocol; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public Integer getDimlevel() { + return dimlevel; + } + + public void setDimlevel(Integer dimlevel) { + this.dimlevel = dimlevel; + } + + public Integer getDimlevelMaximum() { + return dimlevelMaximum; + } + + @JsonProperty("dimlevel-maximum") + public void setDimlevelMaximum(Integer dimlevelMaximum) { + this.dimlevelMaximum = dimlevelMaximum; + } + + public Integer getDimlevelMinimum() { + return dimlevelMinimum; + } + + @JsonProperty("dimlevel-minimum") + public void setDimlevelMinimum(Integer dimlevelMinimum) { + this.dimlevelMinimum = dimlevelMinimum; + } + + public List> getId() { + return id; + } + + public void setId(List> id) { + this.id = id; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public Map getProperties() { + return properties; + } + + @JsonAnySetter + public void set(String name, Object value) { + properties.put(name, value.toString()); + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java new file mode 100644 index 0000000000000..19a1929287d7d --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * Different types of devices in pilight + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class DeviceType { + + public static final Integer SERVER = -1; + + public static final Integer SWITCH = 1; + + public static final Integer DIMMER = 2; + + public static final Integer VALUE = 3; + + public static final Integer CONTACT = 6; +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java new file mode 100644 index 0000000000000..749492a5d3be6 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This object is sent to pilight right after the initial connection. It describes what kind of client we want to be. + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class Identification { + + public static final String ACTION_IDENTIFY = "identify"; + + private String action; + + private Options options = new Options(); + + public Identification() { + this.action = ACTION_IDENTIFY; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Message.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Message.java new file mode 100644 index 0000000000000..449f204ca47cf --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Message.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * Wrapper for the {@code Config} object + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Message { + + private Config config; + + private String message; + + public Config getConfig() { + return config; + } + + public void setConfig(Config config) { + this.config = config; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java new file mode 100644 index 0000000000000..3f639c819969b --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import org.openhab.binding.pilight.internal.serializers.BooleanToIntegerSerializer; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + * Options that can be set as a pilight client. + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Options { + + public static final String MEDIA_ALL = "all"; + + public static final String MEDIA_WEB = "web"; + + public static final String MEDIA_MOBILE = "mobile"; + + public static final String MEDIA_DESKTOP = "desktop"; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = BooleanToIntegerSerializer.class) + private Boolean core; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = BooleanToIntegerSerializer.class) + private Boolean receiver; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = BooleanToIntegerSerializer.class) + private Boolean config; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = BooleanToIntegerSerializer.class) + private Boolean forward; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = BooleanToIntegerSerializer.class) + private Boolean stats; + + private String uuid; + + private String media; + + public Boolean getCore() { + return core; + } + + public void setCore(Boolean core) { + this.core = core; + } + + public Boolean getReceiver() { + return receiver; + } + + public void setReceiver(Boolean receiver) { + this.receiver = receiver; + } + + public Boolean getConfig() { + return config; + } + + public void setConfig(Boolean config) { + this.config = config; + } + + public Boolean getForward() { + return forward; + } + + public void setForward(Boolean forward) { + this.forward = forward; + } + + public Boolean getStats() { + return stats; + } + + public void setStats(Boolean stats) { + this.stats = stats; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getMedia() { + return media; + } + + public void setMedia(String media) { + this.media = media; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java new file mode 100644 index 0000000000000..bdde312ae96f3 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * Response to a connection or state change request + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Response { + + public static final String SUCCESS = "success"; + + public static final String FAILURE = "failure"; + + private String status; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public boolean isSuccess() { + return SUCCESS.equals(status); + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java new file mode 100644 index 0000000000000..8c5c4a3afd055 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A Status message is received when a device in pilight changes state. + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Status { + + private String origin; + + private Integer type; + + private String uuid; + + private List devices = new ArrayList<>(); + + private Map values = new HashMap<>(); + + public Status() { + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public List getDevices() { + return devices; + } + + public void setDevices(List devices) { + this.devices = devices; + } + + public Map getValues() { + return values; + } + + public void setValues(Map values) { + this.values = values; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java new file mode 100644 index 0000000000000..65798b7eacb22 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +/** + * Describes the specific properties of a device + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public class Values { + + private Integer dimlevel; + + public Integer getDimlevel() { + return dimlevel; + } + + public void setDimlevel(Integer dimlevel) { + this.dimlevel = dimlevel; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java new file mode 100644 index 0000000000000..755eff8c4da79 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * pilight version information object + * + * {@link http://www.pilight.org/development/api/#controller} + * + * @author Niklas Dörfler - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Version { + + private String version; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java new file mode 100644 index 0000000000000..aeb1e4845a1fe --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.PilightDeviceConfiguration; +import org.openhab.binding.pilight.internal.dto.Action; +import org.openhab.binding.pilight.internal.dto.Config; +import org.openhab.binding.pilight.internal.dto.Device; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.core.thing.*; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightBaseHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public abstract class PilightBaseHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(PilightBaseHandler.class); + + private String name = ""; + + public PilightBaseHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + refreshConfigAndStatus(); + return; + } + + @Nullable + Action action = createUpdateCommand(channelUID, command); + if (action != null) { + sendAction(action); + } + } + + @Override + public void initialize() { + PilightDeviceConfiguration config = getConfigAs(PilightDeviceConfiguration.class); + name = config.getName(); + + refreshConfigAndStatus(); + } + + public void updateFromStatusIfMatches(Status status) { + if (status.getDevices() != null && !status.getDevices().isEmpty()) { + if (name.equals(status.getDevices().get(0))) { + if (!ThingStatus.ONLINE.equals(getThing().getStatus())) { + updateStatus(ThingStatus.ONLINE); + } + updateFromStatus(status); + } + } + } + + public void updateFromConfigIfMatches(Config config) { + Device device = config.getDevices().get(getName()); + if (device != null) { + updateFromConfigDevice(device); + } + } + + abstract void updateFromStatus(Status status); + + abstract void updateFromConfigDevice(Device device); + + abstract @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command); + + protected String getName() { + return name; + } + + private void sendAction(Action action) { + final @Nullable PilightBridgeHandler handler = getPilightBridgeHandler(); + if (handler != null) { + handler.sendAction(action); + } else { + logger.warn("No pilight bridge handler found to send action."); + } + } + + private void refreshConfigAndStatus() { + final @Nullable PilightBridgeHandler handler = getPilightBridgeHandler(); + if (handler != null) { + handler.refreshConfigAndStatus(); + } else { + logger.warn("No pilight bridge handler found to refresh config and status."); + } + } + + private @Nullable PilightBridgeHandler getPilightBridgeHandler() { + final @Nullable Bridge bridge = getBridge(); + if (bridge != null) { + @Nullable + BridgeHandler handler = bridge.getHandler(); + if (handler instanceof PilightBridgeHandler) { + return (PilightBridgeHandler) handler; + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java new file mode 100644 index 0000000000000..dfcfd9fdfd087 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.IPilightCallback; +import org.openhab.binding.pilight.internal.PilightBridgeConfiguration; +import org.openhab.binding.pilight.internal.PilightConnector; +import org.openhab.binding.pilight.internal.discovery.PilightDeviceDiscoveryService; +import org.openhab.binding.pilight.internal.dto.*; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.thing.*; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightBridgeHandler} is responsible dispatching commands for the child + * things to the Pilight daemon and sending status updates to the child things. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightBridgeHandler extends BaseBridgeHandler { + + private static final int REFRESH_CONFIG_MSEC = 500; + + private final Logger logger = LoggerFactory.getLogger(PilightBridgeHandler.class); + + private @Nullable PilightConnector connector = null; + + private @Nullable ScheduledFuture refreshJob = null; + + private @Nullable PilightDeviceDiscoveryService discoveryService = null; + + private final ExecutorService connectorExecutor = Executors + .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true)); + + public PilightBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Pilight Bridge is read-only and does not handle commands."); + } + + @Override + public void initialize() { + PilightBridgeConfiguration config = getConfigAs(PilightBridgeConfiguration.class); + + final @Nullable PilightDeviceDiscoveryService discoveryService = this.discoveryService; + PilightConnector connector = new PilightConnector(config, new IPilightCallback() { + @Override + public void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, + @Nullable String description) { + updateStatus(status, statusDetail, description); + if (status == ThingStatus.ONLINE) { + refreshConfigAndStatus(); + } + } + + @Override + public void statusReceived(List allStatus) { + for (Status status : allStatus) { + processStatus(status); + } + + if (discoveryService != null) { + discoveryService.setStatus(allStatus); + } + } + + @Override + public void configReceived(Config config) { + processConfig(config); + } + + @Override + public void versionReceived(Version version) { + getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, version.getVersion()); + } + }, scheduler); + + updateStatus(ThingStatus.UNKNOWN); + + connectorExecutor.execute(connector); + this.connector = connector; + } + + @Override + public void dispose() { + final @Nullable ScheduledFuture future = this.refreshJob; + if (future != null) { + future.cancel(true); + } + + final @Nullable PilightConnector connector = this.connector; + if (connector != null) { + connector.close(); + this.connector = null; + } + + connectorExecutor.shutdown(); + } + + /** + * send action to pilight daemon + * + * @param action action to send + */ + public void sendAction(Action action) { + final @Nullable PilightConnector connector = this.connector; + if (connector != null) { + connector.sendAction(action); + } + } + + /** + * refresh config and status by requesting config and all values from pilight daemon + */ + public synchronized void refreshConfigAndStatus() { + if (thing.getStatus() == ThingStatus.ONLINE) { + final @Nullable ScheduledFuture refreshJob = this.refreshJob; + if (refreshJob == null || refreshJob.isCancelled() || refreshJob.isDone()) { + logger.debug("schedule refresh of config and status"); + this.refreshJob = scheduler.schedule(this::doRefreshConfigAndStatus, REFRESH_CONFIG_MSEC, + TimeUnit.MILLISECONDS); + } + } else { + logger.warn("Bridge is not online - ignoring refresh of config and status."); + } + } + + private void doRefreshConfigAndStatus() { + final @Nullable PilightConnector connector = this.connector; + if (connector != null) { + // the config is required for dimmers to get the minimum and maximum dim levels + connector.refreshConfig(); + connector.refreshStatus(); + } + } + + /** + * Processes a status update received from pilight + * + * @param status The new Status + */ + private void processStatus(Status status) { + final Integer type = status.getType(); + logger.trace("processStatus device '{}' type {}", status.getDevices().get(0), type); + + if (!DeviceType.SERVER.equals(type)) { + for (Thing thing : getThing().getThings()) { + final @Nullable ThingHandler handler = thing.getHandler(); + if (handler instanceof PilightBaseHandler) { + ((PilightBaseHandler) handler).updateFromStatusIfMatches(status); + } + } + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(PilightDeviceDiscoveryService.class); + } + + /** + * Register discovery service to this bridge instance. + */ + public boolean registerDiscoveryListener(PilightDeviceDiscoveryService listener) { + if (discoveryService == null) { + discoveryService = listener; + return true; + } + return false; + } + + /** + * Unregister discovery service from this bridge instance. + */ + public boolean unregisterDiscoveryListener() { + if (discoveryService != null) { + discoveryService = null; + return true; + } + + return false; + } + + /** + * Processes a config received from pilight + * + * @param config The new config + */ + private void processConfig(Config config) { + for (Thing thing : getThing().getThings()) { + final @Nullable ThingHandler handler = thing.getHandler(); + if (handler instanceof PilightBaseHandler) { + ((PilightBaseHandler) handler).updateFromConfigIfMatches(config); + } + } + + final @Nullable PilightDeviceDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + discoveryService.setConfig(config); + } + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java new file mode 100644 index 0000000000000..d4d26e641c123 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_STATE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.dto.Action; +import org.openhab.binding.pilight.internal.dto.Device; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.binding.pilight.internal.types.PilightContactType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightContactHandler} is responsible for handling a pilight contact. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightContactHandler extends PilightBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(PilightContactHandler.class); + + public PilightContactHandler(Thing thing) { + super(thing); + } + + @Override + protected void updateFromStatus(Status status) { + String state = status.getValues().get("state"); + if (state != null) { + updateState(CHANNEL_STATE, PilightContactType.valueOf(state.toUpperCase()).toOpenClosedType()); + } + } + + @Override + void updateFromConfigDevice(Device device) { + } + + @Override + protected @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command) { + logger.warn("A contact is a read only device"); + return null; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java new file mode 100644 index 0000000000000..a0d18d9c822ee --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_DIMLEVEL; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.dto.Action; +import org.openhab.binding.pilight.internal.dto.Code; +import org.openhab.binding.pilight.internal.dto.Device; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.binding.pilight.internal.dto.Values; +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.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightDimmerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightDimmerHandler extends PilightBaseHandler { + + private static final int MAX_DIM_LEVEL_DEFAULT = 15; + private static final BigDecimal BIG_DECIMAL_100 = new BigDecimal(100); + + private final Logger logger = LoggerFactory.getLogger(PilightDimmerHandler.class); + + private int maxDimLevel = MAX_DIM_LEVEL_DEFAULT; + + public PilightDimmerHandler(Thing thing) { + super(thing); + } + + @Override + protected void updateFromStatus(Status status) { + BigDecimal dimLevel = BigDecimal.ZERO; + String dimLevelAsString = status.getValues().get("dimlevel"); + + if (dimLevelAsString != null) { + dimLevel = getPercentageFromDimLevel(dimLevelAsString); + } else { + // Dimmer items can can also be switched on or off in pilight. + // When this happens, the dimmer value is not reported. At least we know it's on or off. + String stateAsString = status.getValues().get("state"); + if (stateAsString != null) { + State state = OnOffType.valueOf(stateAsString.toUpperCase()); + dimLevel = state.equals(OnOffType.ON) ? BIG_DECIMAL_100 : BigDecimal.ZERO; + } + } + + State state = new PercentType(dimLevel); + updateState(CHANNEL_DIMLEVEL, state); + } + + @Override + protected void updateFromConfigDevice(Device device) { + Integer max = device.getDimlevelMaximum(); + + if (max != null) { + maxDimLevel = max; + } + } + + @Override + protected @Nullable Action createUpdateCommand(ChannelUID unused, Command command) { + Code code = new Code(); + code.setDevice(getName()); + + if (command instanceof OnOffType) { + code.setState(command.equals(OnOffType.ON) ? Code.STATE_ON : Code.STATE_OFF); + } else if (command instanceof PercentType) { + setDimmerValue((PercentType) command, code); + } else { + logger.warn("Only OnOffType and PercentType are supported by a dimmer."); + return null; + } + + Action action = new Action(Action.ACTION_CONTROL); + action.setCode(code); + return action; + } + + private BigDecimal getPercentageFromDimLevel(String string) { + return new BigDecimal(string).setScale(2).divide(new BigDecimal(maxDimLevel), RoundingMode.HALF_UP) + .multiply(BIG_DECIMAL_100); + } + + private void setDimmerValue(PercentType percent, Code code) { + if (PercentType.ZERO.equals(percent)) { + // pilight is not responding to commands that set both the dimlevel to 0 and state to off. + // So, we're only updating the state for now + code.setState(Code.STATE_OFF); + } else { + BigDecimal dimlevel = percent.toBigDecimal().setScale(2).divide(BIG_DECIMAL_100, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(maxDimLevel)).setScale(0, RoundingMode.HALF_UP); + + Values values = new Values(); + values.setDimlevel(dimlevel.intValue()); + code.setValues(values); + code.setState(dimlevel.compareTo(BigDecimal.ZERO) == 1 ? Code.STATE_ON : Code.STATE_OFF); + } + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java new file mode 100644 index 0000000000000..21adaa309dd91 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.PilightChannelConfiguration; +import org.openhab.binding.pilight.internal.dto.Action; +import org.openhab.binding.pilight.internal.dto.Device; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.core.library.CoreItemFactory; +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.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightGenericHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightGenericHandler extends PilightBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(PilightGenericHandler.class); + + private final ChannelTypeRegistry channelTypeRegistry; + + private final Map channelReadOnlyMap = new HashMap<>(); + + public PilightGenericHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { + super(thing); + this.channelTypeRegistry = channelTypeRegistry; + } + + @Override + public void initialize() { + super.initialize(); + initializeReadOnlyChannels(); + } + + @Override + protected void updateFromStatus(Status status) { + for (Channel channel : thing.getChannels()) { + PilightChannelConfiguration config = channel.getConfiguration().as(PilightChannelConfiguration.class); + updateState(channel.getUID(), + getDynamicChannelState(channel, status.getValues().get(config.getProperty()))); + } + } + + @Override + void updateFromConfigDevice(Device device) { + } + + @Override + protected @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command) { + if (isChannelReadOnly(channelUID)) { + logger.debug("Can't apply command '{}' to '{}' because channel is readonly.", command, channelUID.getId()); + return null; + } + + logger.debug("Create update command for '{}' not implemented.", channelUID.getId()); + + return null; + } + + private State getDynamicChannelState(final Channel channel, final @Nullable String value) { + final @Nullable String acceptedItemType = channel.getAcceptedItemType(); + + if (value == null || acceptedItemType == null) { + return UnDefType.UNDEF; + } + + switch (acceptedItemType) { + case CoreItemFactory.NUMBER: + return new DecimalType(value); + case CoreItemFactory.STRING: + return StringType.valueOf(value); + case CoreItemFactory.SWITCH: + return OnOffType.from(value); + default: + logger.trace("Type '{}' for channel '{}' not implemented", channel.getAcceptedItemType(), channel); + return UnDefType.UNDEF; + } + } + + private void initializeReadOnlyChannels() { + channelReadOnlyMap.clear(); + for (Channel channel : thing.getChannels()) { + final @Nullable ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID != null) { + final @Nullable ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID, null); + + if (channelType != null) { + logger.debug("initializeReadOnly {} {}", channelType, channelType.getState()); + } + + if (channelType != null) { + final @Nullable StateDescription state = channelType.getState(); + if (state != null) { + channelReadOnlyMap.putIfAbsent(channel.getUID(), state.isReadOnly()); + } + } + } + } + } + + private boolean isChannelReadOnly(ChannelUID channelUID) { + Boolean isReadOnly = channelReadOnlyMap.get(channelUID); + return isReadOnly != null ? isReadOnly : true; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java new file mode 100644 index 0000000000000..179d6d2f83c59 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.handler; + +import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_STATE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pilight.internal.dto.Action; +import org.openhab.binding.pilight.internal.dto.Code; +import org.openhab.binding.pilight.internal.dto.Device; +import org.openhab.binding.pilight.internal.dto.Status; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PilightSwitchHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Röllin - Initial contribution + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class PilightSwitchHandler extends PilightBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(PilightSwitchHandler.class); + + public PilightSwitchHandler(Thing thing) { + super(thing); + } + + @Override + protected void updateFromStatus(Status status) { + String state = status.getValues().get("state"); + if (state != null) { + updateState(CHANNEL_STATE, OnOffType.valueOf(state.toUpperCase())); + } + } + + @Override + void updateFromConfigDevice(Device device) { + } + + @Override + protected @Nullable Action createUpdateCommand(ChannelUID unused, Command command) { + if (command instanceof OnOffType) { + Code code = new Code(); + code.setDevice(getName()); + code.setState(command.equals(OnOffType.ON) ? Code.STATE_ON : Code.STATE_OFF); + + Action action = new Action(Action.ACTION_CONTROL); + action.setCode(code); + return action; + } + + logger.warn("A pilight switch only accepts OnOffType commands."); + return null; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java new file mode 100644 index 0000000000000..0a28a9d0fa18c --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.serializers; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * Serializer to map boolean values to an integer (1 and 0). + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +@NonNullByDefault +public class BooleanToIntegerSerializer extends JsonSerializer { + + @Override + public void serialize(@Nullable Boolean bool, @Nullable JsonGenerator jsonGenerator, + @Nullable SerializerProvider serializerProvider) throws IOException { + if (bool != null && jsonGenerator != null) { + jsonGenerator.writeObject(bool ? 1 : 0); + } + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java new file mode 100644 index 0000000000000..8867d83a82611 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 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.pilight.internal.types; + +import org.openhab.core.library.types.OpenClosedType; + +/** + * Enum to represent the state of a contact sensor in pilight + * + * @author Jeroen Idserda - Initial contribution + * @author Stefan Röllin - Port to openHAB 2 pilight binding + * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery + */ +public enum PilightContactType { + OPENED, + CLOSED; + + public OpenClosedType toOpenClosedType() { + return this.equals(PilightContactType.OPENED) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + } +} diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..2540db4c312a0 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,11 @@ + + + + Pilight Binding + The pilight binding allows openHAB to communicate with a pilight instance. Pilight is a service used to + control 'Click On Click Off' devices like 433 MHz remote controlled sockets a cheap way, e.g. by using a Raspberry Pi + with corresponding 433 MHz sender. + + diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..84809bcf447db --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,15 @@ + + + + + + + The name of the pilight device. + + + + diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties new file mode 100644 index 0000000000000..b2b8e8ed5f750 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties @@ -0,0 +1,38 @@ +# binding +binding.pilight.name = Pilight Binding +binding.pilight.description = Das pilight-Binding erm�glicht openHAB die Kommunikation mit einer pilight-Instanz. Pilight ist ein Dienst, der verwendet wird, um 'Click On Click Off'-Ger�te wie bspw. 433 MHz Funksteckdosen auf kosteng�nstige Weise zu steuern, z.B. durch Verwendung eines Raspberry Pi mit entsprechendem 433 MHz Sender. + +# thing types +thing-type.pilight.bridge.label = Pilight Bridge +thing-type.pilight.bridge.description = Verbindung zwischen openHAB und einem pilight Daemon. + +thing-type.pilight.contact.label = Pilight Kontakt +thing-type.pilight.contact.description = Pilight Kontakt + +thing-type.pilight.dimmer.label = Pilight Dimmer +thing-type.pilight.dimmer.description = Pilight Dimmer + +thing-type.pilight.switch.label = Pilight Schalter +thing-type.pilight.switch.description = Pilight Schalter + +thing-type.pilight.generic.label = Generisches pilight Ger�t +thing-type.pilight.generic.description = Ger�t bei dem die Kan�le dynamisch hinzugef�gt werden. + +# thing type config description +thing-type.config.pilight.bridge.ipAddress.label = IP-Adresse +thing-type.config.pilight.bridge.ipAddress.description = Lokale IP-Adresse oder Hostname des pilight Daemons. +thing-type.config.pilight.bridge.port.label = Port +thing-type.config.pilight.bridge.port.description = Port des pilight Daemons. +thing-type.config.pilight.bridge.delay.label = Verz�gerung +thing-type.config.pilight.bridge.delay.description = Verz�gerung (in Millisekunden) zwischen zwei Kommandos. Empfohlener Wert ohne Bandpassfilter: 1000 und mit Bandpassfilter zwischen 200 und 500. + +thing-type.config.pilight.device.name.label = Name +thing-type.config.pilight.device.name.description = Name des pilight Ger�ts + +# channel types +channel-type.pilight.contact-state.label = Status +channel-type.pilight.contact-state.description = Status des pilight Kontakts +channel-type.pilight.switch-state.label = Status +channel-type.pilight.switch-state.description = Status des pilight Schalters +channel-type.pilight.dimlevel.label = Dimmerwert +channel-type.pilight.dimlevel.description = Wert des pilight Dimmers diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..e52c70a6021bb --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,39 @@ + + + + + + Pilight Bridge which connects to a Pilight instance. + + + - + + + + + + The IP or host name of the Pilight instance. + network-address + + + + Port of the Pilight daemon. You must explicitly configure the port in the Pilight daemon config or + otherwise a random port will be used and the binding will not be able to connect. + + 5000 + + + + Delay (in millisecond) between consecutive commands. Recommended value without band pass filter: 1000. + Recommended value with band pass filter: somewhere between 200-500. + 500 + true + + + + + + diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml new file mode 100644 index 0000000000000..cecd34ef7d023 --- /dev/null +++ b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml @@ -0,0 +1,94 @@ + + + + + + + + + + Pilight Switch + + + + + + + + + + + + + + + Pilight Contact + + + + + + + + + + + + + + + Pilight Dimmer + + + + + + + + + + + + + + + Pilight Generic Device + + + + + + Contact + + State of Pilight Contact. + + + + + String + + + + + + The Property of the Device. + + + + + + Number + + + + + + The Property of the Device. + + + + + diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/PioneerAvrBindingConstants.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/PioneerAvrBindingConstants.java index 46780397657a0..ac152daae4c87 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/PioneerAvrBindingConstants.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/PioneerAvrBindingConstants.java @@ -32,10 +32,10 @@ public class PioneerAvrBindingConstants { public static final String BINDING_ID = "pioneeravr"; - public static final Set SUPPORTED_DEVICE_MODELS = Collections - .unmodifiableSet(Stream.of("SC-57", "SC-LX85", "SC-55", "SC-1526", "SC-LX75", "VSX-53", "VSX-1326", - "VSX-LX55", "VSX-2021", "VSA-LX55", "VSX-52", "VSX-1126", "VSX-1121", "VSX-51", "VSX-1021", - "VSX-1026", "VSA-1021", "VSX-50", "VSX-926", "VSX-921", "VSA-921").collect(Collectors.toSet())); + public static final Set SUPPORTED_DEVICE_MODELS = Collections.unmodifiableSet( + Stream.of("SC-57", "SC-LX85", "SC-55", "SC-1526", "SC-LX75", "VSX-53", "VSX-1326", "VSX-LX55", "VSX-2021", + "VSA-LX55", "VSX-52", "VSX-1126", "VSX-1121", "VSX-51", "VSX-1021", "VSX-1026", "VSA-1021", + "VSX-50", "VSX-926", "VSX-921", "VSA-921", "VSX-922").collect(Collectors.toSet())); public static final Set SUPPORTED_DEVICE_MODELS2014 = Collections .unmodifiableSet(Stream.of("SC-LX87", "SC-LX77", "SC-LX57", "SC-2023", "SC-1223", "VSX-1123", "VSX-923") @@ -92,6 +92,7 @@ public class PioneerAvrBindingConstants { public static final String LISTENING_MODE_CHANNEL = "listeningMode"; public static final String PLAYING_LISTENING_MODE_CHANNEL = "playingListeningMode"; public static final String DISPLAY_INFORMATION_CHANNEL = "displayInformation#displayInformation"; + public static final String MCACC_MEMORY_CHANNEL = "MCACCMemory#MCACCMemory"; public static final String GROUP_CHANNEL_PATTERN = "zone%s#%s"; public static final Pattern GROUP_CHANNEL_ZONE_PATTERN = Pattern.compile("zone([0-4])#.*"); diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/discovery/PioneerAvrDiscoveryParticipant.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/discovery/PioneerAvrDiscoveryParticipant.java index 95d157028da87..ecafae9e21dfe 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/discovery/PioneerAvrDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/discovery/PioneerAvrDiscoveryParticipant.java @@ -17,7 +17,6 @@ import java.util.Map; import java.util.Set; -import org.apache.commons.lang.StringUtils; import org.jupnp.model.meta.RemoteDevice; import org.openhab.binding.pioneeravr.internal.PioneerAvrBindingConstants; import org.openhab.core.config.discovery.DiscoveryResult; @@ -58,7 +57,7 @@ public PioneerAvrDiscoveryParticipant() { protected void activate(ComponentContext componentContext) { if (componentContext.getProperties() != null) { String autoDiscoveryPropertyValue = (String) componentContext.getProperties().get("enableAutoDiscovery"); - if (StringUtils.isNotEmpty(autoDiscoveryPropertyValue)) { + if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isEmpty()) { isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue); } } @@ -76,8 +75,8 @@ public DiscoveryResult createResult(RemoteDevice device) { DiscoveryResult result = null; ThingUID thingUid = getThingUID(device); if (thingUid != null) { - String label = StringUtils.isEmpty(device.getDetails().getFriendlyName()) ? device.getDisplayString() - : device.getDetails().getFriendlyName(); + String friendlyName = device.getDetails().getFriendlyName(); + String label = friendlyName == null || friendlyName.isEmpty() ? device.getDisplayString() : friendlyName; Map properties = new HashMap<>(2, 1); properties.put(PioneerAvrBindingConstants.HOST_PARAMETER, device.getIdentity().getDescriptorURL().getHost()); @@ -93,15 +92,16 @@ public DiscoveryResult createResult(RemoteDevice device) { public ThingUID getThingUID(RemoteDevice device) { ThingUID result = null; if (isAutoDiscoveryEnabled) { - if (StringUtils.containsIgnoreCase(device.getDetails().getManufacturerDetails().getManufacturer(), - PioneerAvrBindingConstants.MANUFACTURER)) { + String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer(); + if (manufacturer != null + && manufacturer.toLowerCase().contains(PioneerAvrBindingConstants.MANUFACTURER.toLowerCase())) { logger.debug("Manufacturer matched: search: {}, device value: {}.", - PioneerAvrBindingConstants.MANUFACTURER, - device.getDetails().getManufacturerDetails().getManufacturer()); - if (StringUtils.containsIgnoreCase(device.getType().getType(), - PioneerAvrBindingConstants.UPNP_DEVICE_TYPE)) { + PioneerAvrBindingConstants.MANUFACTURER, manufacturer); + String type = device.getType().getType(); + if (type != null + && type.toLowerCase().contains(PioneerAvrBindingConstants.UPNP_DEVICE_TYPE.toLowerCase())) { logger.debug("Device type matched: search: {}, device value: {}.", - PioneerAvrBindingConstants.UPNP_DEVICE_TYPE, device.getType().getType()); + PioneerAvrBindingConstants.UPNP_DEVICE_TYPE, type); String deviceModel = device.getDetails().getModelDetails() != null ? device.getDetails().getModelDetails().getModelName() @@ -150,7 +150,7 @@ public ThingUID getThingUID(RemoteDevice device) { * @return */ private boolean isSupportedDeviceModel(String deviceModel, Set supportedDeviceModels) { - return StringUtils.isNotBlank(deviceModel) && supportedDeviceModels.stream() - .anyMatch(input -> StringUtils.startsWithIgnoreCase(deviceModel, input)); + return deviceModel != null && !deviceModel.isBlank() && supportedDeviceModels.stream() + .anyMatch(input -> deviceModel.toLowerCase().startsWith(input.toLowerCase())); } } diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/handler/AbstractAvrHandler.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/handler/AbstractAvrHandler.java index cd950ace3777f..11c04f94ba53a 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/handler/AbstractAvrHandler.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/handler/AbstractAvrHandler.java @@ -123,6 +123,9 @@ public void onPowerOn(int zone) { connection.sendMuteQuery(zone); connection.sendInputSourceQuery(zone); connection.sendListeningModeQuery(zone); + + // Channels which are not bound to any specific zone + connection.sendMCACCMemoryQuery(); } /** @@ -136,6 +139,11 @@ public void onPowerOff(int zone) { updateState(getChannelUID(PioneerAvrBindingConstants.SET_INPUT_SOURCE_CHANNEL, zone), UnDefType.UNDEF); updateState(getChannelUID(PioneerAvrBindingConstants.LISTENING_MODE_CHANNEL, zone), UnDefType.UNDEF); updateState(getChannelUID(PioneerAvrBindingConstants.PLAYING_LISTENING_MODE_CHANNEL, zone), UnDefType.UNDEF); + + // Channels which are not bound to any specific zone + if (zone == 1) { + updateState(PioneerAvrBindingConstants.MCACC_MEMORY_CHANNEL, UnDefType.UNDEF); + } } /** @@ -198,6 +206,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else { commandSent = connection.sendMuteCommand(command, getZoneFromChannelUID(channelUID.getId())); } + } else if (channelUID.getId().contains(PioneerAvrBindingConstants.MCACC_MEMORY_CHANNEL)) { + if (command == RefreshType.REFRESH) { + commandSent = connection.sendMCACCMemoryQuery(); + } else { + commandSent = connection.sendMCACCMemoryCommand(command); + } } else { unknownCommand = true; } @@ -248,6 +262,10 @@ public void statusUpdateReceived(AvrStatusUpdateEvent event) { manageDisplayedInformationUpdate(response); break; + case MCACC_MEMORY: + manageMCACCMemoryUpdate(response); + break; + default: logger.debug("Unknown response type from AVR @{}. Response discarded: {}", event.getData(), event.getConnection()); @@ -354,6 +372,15 @@ private void manageDisplayedInformationUpdate(AvrResponse response) { new StringType(DisplayInformationConverter.convertMessageFromIpControl(response.getParameterValue()))); } + /** + * Notify an AVR MCACC Memory update to openHAB + * + * @param response + */ + private void manageMCACCMemoryUpdate(AvrResponse response) { + updateState(PioneerAvrBindingConstants.MCACC_MEMORY_CHANNEL, new StringType(response.getParameterValue())); + } + /** * Build the channelUID from the channel name and the zone number. * diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/ParameterizedCommand.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/ParameterizedCommand.java index edf2fd842f566..ab3f55e371e77 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/ParameterizedCommand.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/ParameterizedCommand.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.pioneeravr.internal.protocol; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrCommand; import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrConnectionException; @@ -31,7 +30,8 @@ public enum ParameterizedCommandType implements AvrCommand.CommandType { VOLUME_SET("[0-9]{2,3}", "VL", "ZV", "YV", "HZV"), INPUT_CHANNEL_SET("[0-9]{2}", "FN", "ZS", "ZT", "ZEA"), - LISTENING_MODE_SET("[0-9]{4}", "SR"); + LISTENING_MODE_SET("[0-9]{4}", "SR"), + MCACC_MEMORY_SET("[1-6]{1}", "MC"); private String[] zoneCommands; private String parameterPattern; @@ -41,6 +41,11 @@ private ParameterizedCommandType(String parameterPattern, String... zoneCommands this.parameterPattern = parameterPattern; } + @Override + public String getCommand() { + return zoneCommands[0]; + } + @Override public String getCommand(int zone) { return zoneCommands[zone - 1]; @@ -55,6 +60,10 @@ public String getParameterPattern() { private String parameterPattern; + protected ParameterizedCommand(ParameterizedCommandType command) { + this(command, 0); + } + protected ParameterizedCommand(ParameterizedCommandType command, int zone) { super(command, zone); this.parameterPattern = command.getParameterPattern(); @@ -72,7 +81,7 @@ public String getCommand() throws AvrConnectionException { "The parameter of the command " + super.getCommand() + " must not be null."); } - if (StringUtils.isNotEmpty(parameterPattern) && !parameter.matches(parameterPattern)) { + if (parameterPattern != null && !parameterPattern.isEmpty() && !parameter.matches(parameterPattern)) { throw new AvrConnectionException("The parameter value " + parameter + " of the command " + super.getCommand() + " does not match the pattern " + parameterPattern); } diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/RequestResponseFactory.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/RequestResponseFactory.java index 130df2b48c77b..56258ef124ace 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/RequestResponseFactory.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/RequestResponseFactory.java @@ -34,6 +34,17 @@ public static IpAvrConnection getConnection(String host, Integer port) { return new IpAvrConnection(host, port); } + /** + * Return a SimpleCommand of the type given in parameter. + * + * @param command + * @return + */ + public static SimpleCommand getIpControlCommand(SimpleCommandType command) { + SimpleCommand result = new SimpleCommand(command); + return result; + } + /** * Return a ParameterizedCommand of the type given in parameter and for the given zone. * @@ -46,6 +57,18 @@ public static SimpleCommand getIpControlCommand(SimpleCommandType command, int z return result; } + /** + * Return a ParameterizedCommand of the type given in parameter. The + * parameter of the command has to be set before send. + * + * @param command + * @return + */ + public static ParameterizedCommand getIpControlCommand(ParameterizedCommandType command) { + ParameterizedCommand result = new ParameterizedCommand(command); + return result; + } + /** * Return a ParameterizedCommand of the type given in parameter. The * parameter of the command has to be set before send. @@ -61,7 +84,22 @@ public static ParameterizedCommand getIpControlCommand(ParameterizedCommandType /** * Return a ParameterizedCommand of the type given in parameter. The - * parameter of the command is set with the given paramter value. + * parameter of the command is set with the given parameter value. + * + * @param command + * @param parameter + * @param zone + * @return + */ + public static ParameterizedCommand getIpControlCommand(ParameterizedCommandType command, String parameter) { + ParameterizedCommand result = getIpControlCommand(command); + result.setParameter(parameter); + return result; + } + + /** + * Return a ParameterizedCommand of the type given in parameter. The + * parameter of the command is set with the given parameter value. * * @param command * @param parameter diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/Response.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/Response.java index 238ace9368d8a..1ca045c6fa71f 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/Response.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/Response.java @@ -15,7 +15,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrConnectionException; import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrResponse; @@ -37,15 +37,16 @@ public enum ResponseType implements AvrResponse.AvrResponseType { INPUT_SOURCE_CHANNEL("[0-9]{2}", "FN", "Z2F", "Z3F", "ZEA"), LISTENING_MODE("[0-9]{4}", "SR"), PLAYING_LISTENING_MODE("[0-9a-f]{4}", "LM"), - DISPLAY_INFORMATION("[0-9a-fA-F]{30}", "FL"); + DISPLAY_INFORMATION("[0-9a-fA-F]{30}", "FL"), + MCACC_MEMORY("[1-6]{1}", "MC"); private String[] responsePrefixZone; - private String parameterPattern; + private @Nullable String parameterPattern; private Pattern[] matchPatternZone; - private ResponseType(String parameterPattern, String... responsePrefixZone) { + private ResponseType(@Nullable String parameterPattern, String... responsePrefixZone) { this.responsePrefixZone = responsePrefixZone; this.parameterPattern = parameterPattern; @@ -53,8 +54,8 @@ private ResponseType(String parameterPattern, String... responsePrefixZone) { for (int zoneIndex = 0; zoneIndex < responsePrefixZone.length; zoneIndex++) { String responsePrefix = responsePrefixZone[zoneIndex]; - matchPatternZone[zoneIndex] = Pattern.compile(responsePrefix + "(" - + (StringUtils.isNotEmpty(parameterPattern) ? parameterPattern : "") + ")"); + matchPatternZone[zoneIndex] = Pattern + .compile(responsePrefix + "(" + (parameterPattern == null ? "" : parameterPattern) + ")"); } } @@ -65,11 +66,11 @@ public String getResponsePrefix(int zone) { @Override public boolean hasParameter() { - return StringUtils.isNotEmpty(parameterPattern); + return parameterPattern != null && !parameterPattern.isEmpty(); } @Override - public String getParameterPattern() { + public @Nullable String getParameterPattern() { return parameterPattern; } @@ -115,7 +116,7 @@ public String parseParameter(String responseData) { private String parameter; public Response(String responseData) throws AvrConnectionException { - if (StringUtils.isEmpty(responseData)) { + if (responseData == null || responseData.isEmpty()) { throw new AvrConnectionException("responseData is empty. Cannot parse the response."); } diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/SimpleCommand.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/SimpleCommand.java index 839a4a160f32b..e04f06e239a9f 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/SimpleCommand.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/SimpleCommand.java @@ -40,7 +40,9 @@ public enum SimpleCommandType implements AvrCommand.CommandType { INPUT_CHANGE_REVERSE("FD"), LISTENING_MODE_CHANGE_CYCLIC("0010SR"), LISTENING_MODE_QUERY("?S"), - INPUT_QUERY("?F", "?ZS", "?ZT", "?ZEA"); + INPUT_QUERY("?F", "?ZS", "?ZT", "?ZEA"), + MCACC_MEMORY_CHANGE_CYCLIC("0MC"), + MCACC_MEMORY_QUERY("?MC"); private String zoneCommands[]; @@ -48,6 +50,11 @@ private SimpleCommandType(String... command) { this.zoneCommands = command; } + @Override + public String getCommand() { + return zoneCommands[0]; + } + @Override public String getCommand(int zone) { return zoneCommands[zone - 1]; @@ -62,8 +69,15 @@ public SimpleCommand(CommandType commandType, int zone) { this.zone = zone; } + public SimpleCommand(CommandType commandType) { + this(commandType, 0); + } + @Override public String getCommand() { + if (zone == 0) { + return commandType.getCommand() + "\r"; + } return commandType.getCommand(zone) + "\r"; } diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/StreamAvrConnection.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/StreamAvrConnection.java index df4893ad0eb19..0c27977ae4d14 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/StreamAvrConnection.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/StreamAvrConnection.java @@ -195,6 +195,11 @@ public boolean sendListeningModeQuery(int zone) { return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_QUERY, zone)); } + @Override + public boolean sendMCACCMemoryQuery() { + return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_QUERY)); + } + @Override public boolean sendPowerCommand(Command command, int zone) throws CommandTypeNotSupportedException { AvrCommand commandToSend = null; @@ -307,6 +312,23 @@ public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotS return sendCommand(commandToSend); } + @Override + public boolean sendMCACCMemoryCommand(Command command) throws CommandTypeNotSupportedException { + AvrCommand commandToSend = null; + + if (command == IncreaseDecreaseType.INCREASE) { + commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_CHANGE_CYCLIC); + } else if (command instanceof StringType) { + String MCACCMemoryValue = ((StringType) command).toString(); + commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.MCACC_MEMORY_SET) + .setParameter(MCACCMemoryValue); + } else { + throw new CommandTypeNotSupportedException("Command type not supported."); + } + + return sendCommand(commandToSend); + } + /** * Read incoming data from the AVR and notify listeners for dataReceived and disconnection. * diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrCommand.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrCommand.java index 06a2e4ad40bff..e3ccb125eaabe 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrCommand.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrCommand.java @@ -23,6 +23,12 @@ public interface AvrCommand { * Represent a CommandType of command requests */ public interface CommandType { + /** + * Return the command of this command type. + * + * @return + */ + public String getCommand(); /** * Return the command of this command type for the given zone. diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrConnection.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrConnection.java index edcda596121f7..ca09045e1fc80 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrConnection.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrConnection.java @@ -96,6 +96,13 @@ public interface AvrConnection { */ public boolean sendListeningModeQuery(int zone); + /** + * Send an MCACC Memory query to the AVR + * + * @return + */ + public boolean sendMCACCMemoryQuery(); + /** * Send a power command ot the AVR based on the openHAB command * @@ -141,6 +148,15 @@ public interface AvrConnection { */ public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotSupportedException; + /** + * Send an MCACC Memory selection command to the AVR based on the openHAB command + * + * @param command + * @param zone + * @return + */ + public boolean sendMCACCMemoryCommand(Command command) throws CommandTypeNotSupportedException; + /** * Return the connection name * diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrResponse.java b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrResponse.java index da90e461733d2..64cfd0901a757 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrResponse.java +++ b/bundles/org.openhab.binding.pioneeravr/src/main/java/org/openhab/binding/pioneeravr/internal/protocol/avr/AvrResponse.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.pioneeravr.internal.protocol.avr; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pioneeravr.internal.protocol.Response.ResponseType; /** @@ -46,7 +47,7 @@ public interface AvrResponseType { * * @return */ - public String getParameterPattern(); + public @Nullable String getParameterPattern(); /** * Return the zone number if the responseData matches a zone of this responseType. diff --git a/bundles/org.openhab.binding.pioneeravr/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pioneeravr/src/main/resources/OH-INF/thing/thing-types.xml index 3c0d161a457da..24efc93656023 100644 --- a/bundles/org.openhab.binding.pioneeravr/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.pioneeravr/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,11 +9,12 @@ Control a classic Pioneer AVR (SC-57, SC-LX85, SC-55, SC-1526, SC-LX75, VSX-53, VSX-1326, VSX-LX55, VSX-2021, VSA-LX55, VSX-52, VSX-1126, VSX-1121, VSX-51, VSX-1021, VSX-1026, VSA-1021, VSX-50, VSX-926, VSX-921, - VSA-921) over IP + VSA-921, VSX-922) over IP + @@ -44,6 +45,7 @@ Control a 2014 Pioneer AVR (SC-LX87, SC-LX77, SC-LX57, SC-2023, SC-1223, VSX-1123, VSX-923) over IP + @@ -80,6 +82,7 @@ + @@ -116,6 +119,7 @@ + @@ -152,6 +156,7 @@ + @@ -186,6 +191,7 @@ + @@ -220,6 +226,7 @@ + @@ -254,6 +261,7 @@ + @@ -290,6 +298,7 @@ + @@ -316,6 +325,7 @@ + @@ -338,6 +348,13 @@ + + + + + + + @@ -643,7 +660,7 @@ Set the volume level (dB) SoundVolume - + @@ -1378,4 +1395,21 @@ + + String + + Set the MCACC Memory profile + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.pixometer/README.md b/bundles/org.openhab.binding.pixometer/README.md index 88d0815c89165..89b6f1834936c 100644 --- a/bundles/org.openhab.binding.pixometer/README.md +++ b/bundles/org.openhab.binding.pixometer/README.md @@ -29,7 +29,7 @@ The different meter types are pretty similar in basic, but are implemented in pa | Parameter | Description | Required | |------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| -| resource_id | The ID which represents the current meter. You can find it in the pixometer browser app while editing a specific meter. It should look like this: "https://pixometer.io/portal/#/meters/XXXXX/edit" | Yes | +| resourceId | The ID which represents the current meter. You can find it in the pixometer browser app while editing a specific meter. It should look like this: "https://pixometer.io/portal/#/meters/XXXXX/edit" | Yes | ## Channels @@ -47,19 +47,19 @@ pixometer.things: ```java Bridge pixometer:account:AccountName "MyAccountName" [ user="xxxxxxxx@xxxx.xx", password="xxxxxxxxxxxx", refresh= 60 ] { - Thing energymeter MeterName1 "MyMeterName1" [ resource_id = "xxxxxxxx" ] - Thing gasmeter MeterName2 "MyMeterName2" [ resource_id = "xxxxxxxx" ] - Thing watermeter MeterName3 "MyMeterName3" [ resource_id = "xxxxxxxx" ] + Thing energymeter MeterName1 "MyMeterName1" [ resourceId = "xxxxxxxx" ] + Thing gasmeter MeterName2 "MyMeterName2" [ resourceId = "xxxxxxxx" ] + Thing watermeter MeterName3 "MyMeterName3" [ resourceId = "xxxxxxxx" ] } ``` pixometer.items: ```java -Number:Volume Meter_Gas_ReadingValue "[.3%f %unit%]" [] {channel="pixometer:gasmeter:accountname:metername1:last_reading_value"} +Number:Volume Meter_Gas_ReadingValue "[%.3f %unit%]" [] {channel="pixometer:gasmeter:accountname:metername1:last_reading_value"} DateTime Meter_Gas_LastReadingDate "[%1$td.%1$tm.%1$tY %1$tH:%1$tM]" [] {channel="pixometer:gasmeter:accountname:metername1:last_reading_date"} -Number:Energy Meter_Electricity_ReadingValue "[.3%f %unit%]" [] {channel="pixometer:energymeter:accountname:metername2:last_reading_value"} +Number:Energy Meter_Electricity_ReadingValue "[%.1f %unit%]" [] {channel="pixometer:energymeter:accountname:metername2:last_reading_value"} DateTime Meter_Electricity_LastReadingDate "[%1$td.%1$tm.%1$tY %1$tH:%1$tM]" [] {channel="pixometer:energymeter:accountname:metername2:last_reading_date"} -Number:Volume Meter_Water_ReadingValue "[.3%f %unit%]" [] {channel="pixometer:watermeter:accountname:metername3:last_reading_value"} +Number:Volume Meter_Water_ReadingValue "[%.3f %unit%]" [] {channel="pixometer:watermeter:accountname:metername3:last_reading_value"} DateTime Meter_Water_LastReadingDate "[%1$td.%1$tm.%1$tY %1$tH:%1$tM]" [] {channel="pixometer:watermeter:accountname:metername3:last_reading_date"} ``` diff --git a/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/AccountHandler.java b/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/AccountHandler.java index 5f9c4a1cf44cc..3af4ab735a6b7 100644 --- a/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/AccountHandler.java @@ -47,7 +47,6 @@ public class AccountHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final int TOKEN_MIN_DIFF_MS = (int) TimeUnit.DAYS.toMillis(2); - private final JsonParser jsonParser = new JsonParser(); private @NonNullByDefault({}) String authToken; private int refreshInterval; @@ -114,7 +113,7 @@ private void obtainAuthTokenAndExpiryDate(String user, String password, String s InputStream content = new ByteArrayInputStream(httpBody.toString().getBytes(StandardCharsets.UTF_8)); String urlResponse = HttpUtil.executeUrl("POST", url, urlHeader, content, "application/json", 2000); - JsonObject responseJson = (JsonObject) jsonParser.parse(urlResponse); + JsonObject responseJson = (JsonObject) JsonParser.parseString(urlResponse); if (responseJson.has(AUTH_TOKEN)) { // Store the expire date for automatic token refresh diff --git a/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/MeterHandler.java b/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/MeterHandler.java index cf34f3a81c028..926e07128e297 100644 --- a/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/MeterHandler.java +++ b/bundles/org.openhab.binding.pixometer/src/main/java/org/openhab/binding/pixometer/handler/MeterHandler.java @@ -70,7 +70,6 @@ public class MeterHandler extends BaseThingHandler { private final GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(ReadingInstance.class, new CustomReadingInstanceDeserializer()); private final Gson gson = gsonBuilder.create(); - private final JsonParser jsonParser = new JsonParser(); private @NonNullByDefault({}) String resourceID; private @NonNullByDefault({}) String meterID; @@ -178,7 +177,7 @@ private void obtainMeterId() { urlHeader.put("Authorization", token); String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000); - JsonObject responseJson = (JsonObject) jsonParser.parse(urlResponse); + JsonObject responseJson = (JsonObject) JsonParser.parseString(urlResponse); if (responseJson.has("meter_id")) { setMeterID(responseJson.get("meter_id").toString()); @@ -251,7 +250,7 @@ private void updateMeter(@Nullable MeterState meterState) { String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000); - ReadingInstance latestReading = gson.fromJson(new JsonParser().parse(urlResponse), ReadingInstance.class); + ReadingInstance latestReading = gson.fromJson(JsonParser.parseString(urlResponse), ReadingInstance.class); return new MeterState(Objects.requireNonNull(latestReading)); } catch (IOException e) { diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/analog.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/analog.xml index d6540f719bd72..f1db22b2c98d3 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/analog.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/analog.xml @@ -5,22 +5,19 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + LOGO! analog block kind - true - + Propagate channels update to openHAB whether value changed or not false - false - + Smallest value change will be sent to openHAB 0 - false diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/bridge.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/bridge.xml index 4050f224ade81..d52ba4c9a4fd6 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/bridge.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/bridge.xml @@ -5,37 +5,32 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + network-address Network address of the PLC. - true - + LOGO! PLC hardware family version - true - + Local TSAP of the client as hex string - true 0x3000 - + Remote TSAP of the client as hex string - true 0x2000 - + Milliseconds between reread data from PLC. - true 100 diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/datetime.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/datetime.xml index 1a873949fe8f2..6362ab4eba2e2 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/datetime.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/datetime.xml @@ -5,12 +5,11 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + LOGO! memory address - true - + Interpret received channel value as date or time @@ -18,13 +17,11 @@ time - true - + Propagate channels update to openHAB whether value changed or not false - false diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/digital.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/digital.xml index 5dae526130ad0..a539f7675ba52 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/digital.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/digital.xml @@ -5,16 +5,14 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + LOGO! digital block kind - true - + Propagate channels update to openHAB whether value changed or not false - false diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/memory.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/memory.xml index d05eb2f893826..fa8b72d6421db 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/memory.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/memory.xml @@ -6,22 +6,20 @@ + pattern="VB(\d|[1-9]\d|[1-7]\d{2}|8[0-4]\d|850)\.[0-7]|VB(\d|[1-9]\d|[1-7]\d{2}|8[0-4]\d|850)|VW(\d|[1-9]\d|[1-7]\d{2}|8[0-4]\d)|VD(\d|[1-9]\d|[1-7]\d{2}|8[0-3]\d|84[0-7])" + required="true"> LOGO! memory address - true - + Update of the channel be should propagated to openHAB false - false - + Smallest value change will be sent to openHAB 0 - false diff --git a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/pulse.xml b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/pulse.xml index 58a9194bd7ea9..be93fca04dff0 100644 --- a/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/pulse.xml +++ b/bundles/org.openhab.binding.plclogo/src/main/resources/OH-INF/config/pulse.xml @@ -5,22 +5,20 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + LOGO! memory address - true + pattern="I([1-9]|1\d|2[0-4])|NI([1-9]|[1-5]\d|6[0-4])|Q([1-9]|1\d|20)|NQ([1-9]|[1-5]\d|6[0-4])|M([1-9]|[1-5]\d|6[0-4])|VB(\d|[1-9]\d|[1-7]\d{2}|8[0-4]\d|850)\.[0-7]" + required="false"> LOGO! block or memory address to observe - false - + Time to wait before state reset 150 - false diff --git a/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseMessageProcessor.java b/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseMessageProcessor.java index 94660067e4573..3cfefd8883886 100644 --- a/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseMessageProcessor.java +++ b/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseMessageProcessor.java @@ -22,7 +22,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage; diff --git a/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseUtils.java b/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseUtils.java index c58e42b9c6e33..eb6d7e5cdbe58 100644 --- a/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseUtils.java +++ b/bundles/org.openhab.binding.plugwise/src/main/java/org/openhab/binding/plugwise/internal/PlugwiseUtils.java @@ -20,8 +20,8 @@ import java.time.format.DateTimeFormatter; import java.util.Map; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage; diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnector.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnector.java index 6dc6337ebcee2..4764c764ad300 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnector.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnector.java @@ -47,7 +47,7 @@ public PowermaxConnector(String readerThreadName) { } @Override - public abstract void open(); + public abstract void open() throws Exception; @Override public abstract void close(); @@ -101,9 +101,15 @@ public void handleIncomingMessage(byte[] incomingMessage) { PowermaxBaseMessage.getMessageHandler(incomingMessage)); // send message to event listeners - for (int i = 0; i < listeners.size(); i++) { - listeners.get(i).onNewMessageEvent(event); - } + listeners.forEach(listener -> listener.onNewMessageEvent(event)); + } + + /** + * Handles a communication failure + */ + public void handleCommunicationFailure(String message) { + close(); + listeners.forEach(listener -> listener.onCommunicationFailure(message)); } @Override @@ -113,7 +119,7 @@ public void sendMessage(byte[] data) { output.flush(); } catch (IOException e) { logger.debug("sendMessage(): Writing error: {}", e.getMessage(), e); - setConnected(false); + handleCommunicationFailure(e.getMessage()); } } diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnectorInterface.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnectorInterface.java index a794b3c7b21ec..9cf3241622fa1 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnectorInterface.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxConnectorInterface.java @@ -26,7 +26,7 @@ public interface PowermaxConnectorInterface { /** * Method for opening a connection to the Visonic alarm panel. */ - public void open(); + public void open() throws Exception; /** * Method for closing a connection to the Visonic alarm panel. diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxReaderThread.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxReaderThread.java index a2a172e6c11df..9eb94870acfaa 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxReaderThread.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxReaderThread.java @@ -50,7 +50,7 @@ public PowermaxReaderThread(PowermaxConnector connector, String threadName) { @Override public void run() { - logger.debug("Data listener started"); + logger.info("Data listener started"); byte[] readDataBuffer = new byte[READ_BUFFER_SIZE]; byte[] dataBuffer = new byte[MAX_MSG_SIZE]; @@ -129,9 +129,14 @@ public void run() { logger.debug("Interrupted via InterruptedIOException"); } catch (IOException e) { logger.debug("Reading failed: {}", e.getMessage(), e); + connector.handleCommunicationFailure(e.getMessage()); + } catch (Exception e) { + String msg = e.getMessage() != null ? e.getMessage() : e.toString(); + logger.debug("Error reading or processing message: {}", msg, e); + connector.handleCommunicationFailure(msg); } - logger.debug("Data listener stopped"); + logger.info("Data listener stopped"); } /** @@ -143,6 +148,11 @@ public void run() { * @return true if the CRC is valid or false if not */ private boolean checkCRC(byte[] data, int len) { + // Messages of type 0xF1 are always sent with a bad CRC (possible panel bug?) + if (len == 9 && (data[1] & 0xFF) == 0xF1) { + return true; + } + byte checksum = PowermaxCommManager.computeCRC(data, len); byte expected = data[len - 2]; if (checksum != expected) { diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxSerialConnector.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxSerialConnector.java index 28aec1acf1ed2..9fa671d8c4bc6 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxSerialConnector.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxSerialConnector.java @@ -13,16 +13,12 @@ package org.openhab.binding.powermax.internal.connector; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.TooManyListenersException; -import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPortEvent; import org.openhab.core.io.transport.serial.SerialPortEventListener; import org.openhab.core.io.transport.serial.SerialPortIdentifier; import org.openhab.core.io.transport.serial.SerialPortManager; -import org.openhab.core.io.transport.serial.UnsupportedCommOperationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,59 +54,39 @@ public PowermaxSerialConnector(SerialPortManager serialPortManager, String seria } @Override - public void open() { + public void open() throws Exception { logger.debug("open(): Opening Serial Connection"); - try { - SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName); - if (portIdentifier != null) { - SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000); - - serialPort = commPort; - serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, - SerialPort.PARITY_NONE); - serialPort.enableReceiveThreshold(1); - serialPort.enableReceiveTimeout(250); - - setInput(serialPort.getInputStream()); - setOutput(serialPort.getOutputStream()); - - getOutput().flush(); - if (getInput().markSupported()) { - getInput().reset(); - } - - // RXTX serial port library causes high CPU load - // Start event listener, which will just sleep and slow down event - // loop - try { - serialPort.addEventListener(this); - serialPort.notifyOnDataAvailable(true); - } catch (TooManyListenersException e) { - logger.debug("Too Many Listeners Exception: {}", e.getMessage(), e); - } - - setReaderThread(new PowermaxReaderThread(this, readerThreadName)); - getReaderThread().start(); - - setConnected(true); - } else { - logger.debug("open(): No Such Port: {}", serialPortName); - setConnected(false); - } - } catch (PortInUseException e) { - logger.debug("open(): Port in Use Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (UnsupportedCommOperationException e) { - logger.debug("open(): Unsupported Comm Operation Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (UnsupportedEncodingException e) { - logger.debug("open(): Unsupported Encoding Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (IOException e) { - logger.debug("open(): IO Exception: {}", e.getMessage(), e); - setConnected(false); + SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName); + if (portIdentifier == null) { + throw new IOException("No Such Port: " + serialPortName); } + + SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000); + + serialPort = commPort; + serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); + serialPort.enableReceiveThreshold(1); + serialPort.enableReceiveTimeout(250); + + setInput(serialPort.getInputStream()); + setOutput(serialPort.getOutputStream()); + + getOutput().flush(); + if (getInput().markSupported()) { + getInput().reset(); + } + + // RXTX serial port library causes high CPU load + // Start event listener, which will just sleep and slow down event + // loop + serialPort.addEventListener(this); + serialPort.notifyOnDataAvailable(true); + + setReaderThread(new PowermaxReaderThread(this, readerThreadName)); + getReaderThread().start(); + + setConnected(true); } @Override @@ -140,6 +116,7 @@ public void serialEvent(SerialPortEvent serialPortEvent) { logger.trace("RXTX library CPU load workaround, sleep forever"); Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxTcpConnector.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxTcpConnector.java index 833d2bddaef31..40014a1c1378a 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxTcpConnector.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/connector/PowermaxTcpConnector.java @@ -16,9 +16,7 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; -import java.net.SocketException; import java.net.SocketTimeoutException; -import java.net.UnknownHostException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,35 +51,21 @@ public PowermaxTcpConnector(String ip, int port, int timeout, String readerThrea } @Override - public void open() { + public void open() throws Exception { logger.debug("open(): Opening TCP Connection"); - try { - tcpSocket = new Socket(); - tcpSocket.setSoTimeout(250); - SocketAddress socketAddress = new InetSocketAddress(ipAddress, tcpPort); - tcpSocket.connect(socketAddress, connectTimeout); - - setInput(tcpSocket.getInputStream()); - setOutput(tcpSocket.getOutputStream()); - - setReaderThread(new PowermaxReaderThread(this, readerThreadName)); - getReaderThread().start(); - - setConnected(true); - } catch (UnknownHostException e) { - logger.debug("open(): Unknown Host Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (SocketException e) { - logger.debug("open(): Socket Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (IOException e) { - logger.debug("open(): IO Exception: {}", e.getMessage(), e); - setConnected(false); - } catch (Exception e) { - logger.debug("open(): Exception: {}", e.getMessage(), e); - setConnected(false); - } + tcpSocket = new Socket(); + tcpSocket.setSoTimeout(250); + SocketAddress socketAddress = new InetSocketAddress(ipAddress, tcpPort); + tcpSocket.connect(socketAddress, connectTimeout); + + setInput(tcpSocket.getInputStream()); + setOutput(tcpSocket.getOutputStream()); + + setReaderThread(new PowermaxReaderThread(this, readerThreadName)); + getReaderThread().start(); + + setConnected(true); } @Override diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxBridgeHandler.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxBridgeHandler.java index f5033c8065c92..6ff9b22c9c65e 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxBridgeHandler.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxBridgeHandler.java @@ -27,6 +27,7 @@ import org.openhab.binding.powermax.internal.config.PowermaxSerialConfiguration; import org.openhab.binding.powermax.internal.discovery.PowermaxDiscoveryService; import org.openhab.binding.powermax.internal.message.PowermaxCommManager; +import org.openhab.binding.powermax.internal.message.PowermaxSendType; import org.openhab.binding.powermax.internal.state.PowermaxArmMode; import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings; import org.openhab.binding.powermax.internal.state.PowermaxPanelSettingsListener; @@ -48,6 +49,7 @@ import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +66,7 @@ public class PowermaxBridgeHandler extends BaseBridgeHandler implements Powermax private final TimeZoneProvider timeZoneProvider; private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1); + private static final long FIVE_MINUTES = TimeUnit.MINUTES.toMillis(5); /** Default delay in milliseconds to reset a motion detection */ private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3); @@ -252,24 +255,31 @@ private void updateMotionSensorState() { } /* - * Check that we receive a keep alive message during the last minute + * Check that we're actively communicating with the panel */ private void checkKeepAlive() { long now = System.currentTimeMillis(); if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) && (currentState.lastKeepAlive.getValue() != null) && ((now - currentState.lastKeepAlive.getValue()) > ONE_MINUTE)) { - // Let Powermax know we are alive + // In Powerlink mode: let Powermax know we are alive commManager.sendRestoreMessage(); currentState.lastKeepAlive.setValue(now); + } else if (!Boolean.TRUE.equals(currentState.downloadMode.getValue()) + && (currentState.lastMessageReceived.getValue() != null) + && ((now - currentState.lastMessageReceived.getValue()) > FIVE_MINUTES)) { + // In Standard mode: ping the panel every so often to detect disconnects + commManager.sendMessage(PowermaxSendType.STATUS); } } private void tryReconnect() { - logger.debug("trying to reconnect..."); + logger.info("Trying to connect or reconnect..."); closeConnection(); currentState = commManager.createNewState(); - if (openConnection()) { + try { + openConnection(); + logger.debug("openConnection(): connected"); updateStatus(ThingStatus.ONLINE); if (forceStandardMode) { currentState.powerlinkMode.setValue(false); @@ -278,8 +288,10 @@ private void tryReconnect() { } else { commManager.startDownload(); } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reconnection failed"); + } catch (Exception e) { + logger.debug("openConnection(): {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + setAllChannelsOffline(); } } @@ -288,14 +300,12 @@ private void tryReconnect() { * * @return true if the connection has been opened */ - private synchronized boolean openConnection() { + private synchronized void openConnection() throws Exception { if (commManager != null) { commManager.addEventListener(this); commManager.open(); } remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS; - logger.debug("openConnection(): {}", isConnected() ? "connected" : "disconnected"); - return isConnected(); } /** @@ -474,6 +484,12 @@ public void onNewStateEvent(EventObject event) { } } + @Override + public void onCommunicationFailure(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + setAllChannelsOffline(); + } + private void processPanelSettings() { if (commManager.processPanelSettings(Boolean.TRUE.equals(currentState.powerlinkMode.getValue()))) { for (PowermaxPanelSettingsListener listener : listeners) { @@ -489,10 +505,10 @@ private void processPanelSettings() { } updatePropertiesFromPanelSettings(); if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) { - logger.debug("Powermax alarm binding: running in Powerlink mode"); + logger.info("Powermax alarm binding: running in Powerlink mode"); commManager.sendRestoreMessage(); } else { - logger.debug("Powermax alarm binding: running in Standard mode"); + logger.info("Powermax alarm binding: running in Standard mode"); commManager.getInfosWhenInStandardMode(); } } @@ -513,7 +529,7 @@ private void updateChannelsFromAlarmState(PowermaxState state) { * @param state: the alarm system state */ private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) { - if (state == null) { + if (state == null || !isConnected()) { return; } @@ -560,6 +576,13 @@ private synchronized void updateChannelsFromAlarmState(String channel, PowermaxS } } + /** + * Update all channels to an UNDEF state to indicate that communication with the panel is offline + */ + private synchronized void setAllChannelsOffline() { + getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF)); + } + /** * Update properties to match the alarm panel settings */ diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxThingHandler.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxThingHandler.java index c31c1b777e38c..4e20a14184328 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxThingHandler.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/handler/PowermaxThingHandler.java @@ -34,6 +34,7 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -113,6 +114,7 @@ private void initializeThingState(ThingHandler bridgeHandler, ThingStatus bridge } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + setAllChannelsOffline(); logger.debug("Set handler status to OFFLINE for thing {} (bridge OFFLINE)", getThing().getUID()); } } else { @@ -121,6 +123,13 @@ private void initializeThingState(ThingHandler bridgeHandler, ThingStatus bridge } } + /** + * Update all channels to an UNDEF state to indicate that the bridge is offline + */ + private synchronized void setAllChannelsOffline() { + getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF)); + } + @Override public void dispose() { logger.debug("Handler disposed for thing {}", getThing().getUID()); diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxCommManager.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxCommManager.java index e902002000d0b..36d73935debb5 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxCommManager.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxCommManager.java @@ -176,13 +176,12 @@ public synchronized void removeEventListener(PowermaxStateEventListener listener * * @return true if connected or false if not */ - public boolean open() { + public void open() throws Exception { if (connector != null) { connector.open(); } lastSendMsg = null; msgQueue = new ConcurrentLinkedQueue<>(); - return isConnected(); } /** @@ -255,28 +254,37 @@ public void onNewMessageEvent(EventObject event) { } PowermaxState updateState = message.handleMessage(this); - if (updateState != null) { - if (updateState.getUpdateSettings() != null) { - panelSettings.updateRawSettings(updateState.getUpdateSettings()); - } - if (!updateState.getUpdatedZoneNames().isEmpty()) { - for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) { - panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx)); - } - } - if (!updateState.getUpdatedZoneInfos().isEmpty()) { - for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) { - panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx)); - } - } - PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState); + if (updateState == null) { + updateState = createNewState(); + } + + updateState.lastMessageReceived.setValue(System.currentTimeMillis()); - // send message to event listeners - for (int i = 0; i < listeners.size(); i++) { - listeners.get(i).onNewStateEvent(newEvent); + if (updateState.getUpdateSettings() != null) { + panelSettings.updateRawSettings(updateState.getUpdateSettings()); + } + if (!updateState.getUpdatedZoneNames().isEmpty()) { + for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) { + panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx)); + } + } + if (!updateState.getUpdatedZoneInfos().isEmpty()) { + for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) { + panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx)); } } + + PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState); + + // send message to event listeners + listeners.forEach(listener -> listener.onNewStateEvent(newEvent)); + } + + @Override + public void onCommunicationFailure(String message) { + close(); + listeners.forEach(listener -> listener.onCommunicationFailure(message)); } /** diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxMessageEventListener.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxMessageEventListener.java index ecc966471a5ae..ce13ad2293ad8 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxMessageEventListener.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/message/PowermaxMessageEventListener.java @@ -28,4 +28,9 @@ public interface PowermaxMessageEventListener extends EventListener { * @param event the event object */ public void onNewMessageEvent(EventObject event); + + /** + * Event handler method to indicate that communication has been lost + */ + public void onCommunicationFailure(String message); } diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxPanelSettings.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxPanelSettings.java index 07fc054ecc156..14785583510ee 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxPanelSettings.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxPanelSettings.java @@ -512,16 +512,19 @@ public boolean process(boolean PowerlinkMode, PowermaxPanelType defaultPanelType result = false; } - // Check if partitions are enabled - byte[] partitions = readSettings(PowermaxSendType.DL_PARTITIONS, 0, 0x10 + zoneCnt); - if (partitions != null) { - partitionsEnabled = (partitions[0] & 0x000000FF) == 1; - } else { - logger.debug("Cannot get partitions information"); - result = false; - } - if (!partitionsEnabled) { - partitionCnt = 1; + // Check if partitions are enabled (only on panels that support partitions) + byte[] partitions = null; + if (partitionCnt > 1) { + partitions = readSettings(PowermaxSendType.DL_PARTITIONS, 0, 0x10 + zoneCnt); + if (partitions != null) { + partitionsEnabled = (partitions[0] & 0x000000FF) == 1; + } else { + logger.debug("Cannot get partitions information"); + result = false; + } + if (!partitionsEnabled) { + partitionCnt = 1; + } } // Process zone settings diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxState.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxState.java index a6722222a38ac..5cfbc3bc72b77 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxState.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxState.java @@ -47,6 +47,7 @@ public class PowermaxState extends PowermaxStateContainer { public StringValue armMode = new StringValue(this, "_arm_mode"); public BooleanValue downloadSetupRequired = new BooleanValue(this, "_download_setup_required"); public DateTimeValue lastKeepAlive = new DateTimeValue(this, "_last_keepalive"); + public DateTimeValue lastMessageReceived = new DateTimeValue(this, "_last_message_received"); public StringValue panelStatus = new StringValue(this, "_panel_status"); public StringValue alarmType = new StringValue(this, "_alarm_type"); public StringValue troubleType = new StringValue(this, "_trouble_type"); diff --git a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxStateEventListener.java b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxStateEventListener.java index 900ea114a0b94..f4e24abc49947 100644 --- a/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxStateEventListener.java +++ b/bundles/org.openhab.binding.powermax/src/main/java/org/openhab/binding/powermax/internal/state/PowermaxStateEventListener.java @@ -28,4 +28,9 @@ public interface PowermaxStateEventListener extends EventListener { * @param event the event object */ public void onNewStateEvent(EventObject event); + + /** + * Event handler method to indicate that communication has been lost + */ + public void onCommunicationFailure(String message); } diff --git a/bundles/org.openhab.binding.pulseaudio/README.md b/bundles/org.openhab.binding.pulseaudio/README.md index 39b7766620dfb..8951f9d18b872 100644 --- a/bundles/org.openhab.binding.pulseaudio/README.md +++ b/bundles/org.openhab.binding.pulseaudio/README.md @@ -6,7 +6,7 @@ This binding integrates pulseaudio devices. The Pulseaudio bridge is required as a "bridge" for accessing any other Pulseaudio devices. -You need a running pulseaudio server whith module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported: +You need a running pulseaudio server with module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported: * Sink * Source @@ -35,12 +35,18 @@ All devices support some of the following channels: | slaves | String | Slave sinks of a combined sink | | routeToSink | String | Shows the sink a sink-input is currently routed to | +## Audio sink + +Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported. +Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink). +This requires the module **module-simple-protocol-tcp** to be present on the server which runs your openHAB instance. The binding will try to command (if not discovered first) the load of this module on the pulseaudio server. + ## Full Example ### pulseaudio.things ``` Bridge pulseaudio:bridge: "" @ "" [ host="", port=4712 ] { Things: - Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3"] // this name corresponds to pactl list-sinks output + Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the name corresponds to pactl list-sinks output Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"] Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"] Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"] diff --git a/bundles/org.openhab.binding.pulseaudio/pom.xml b/bundles/org.openhab.binding.pulseaudio/pom.xml index 29ec07a21c6e3..efc42fa11186b 100644 --- a/bundles/org.openhab.binding.pulseaudio/pom.xml +++ b/bundles/org.openhab.binding.pulseaudio/pom.xml @@ -14,4 +14,25 @@ openHAB Add-ons :: Bundles :: Pulseaudio Binding + + + com.googlecode.soundlibs + mp3spi + 1.9.5.4 + compile + + + com.googlecode.soundlibs + jlayer + 1.0.1.4 + compile + + + com.googlecode.soundlibs + tritonus-share + 0.3.7.4 + compile + + + diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml b/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml index afdd623480728..cd43aea257f86 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml @@ -7,5 +7,8 @@ openhab-transport-mdns openhab-transport-upnp mvn:org.openhab.addons.bundles/org.openhab.binding.pulseaudio/${project.version} + mvn:com.googlecode.soundlibs/tritonus-share/0.3.7.4 + mvn:com.googlecode.soundlibs/mp3spi/1.9.5.4 + mvn:com.googlecode.soundlibs/jlayer/1.0.1.4 diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java new file mode 100644 index 0000000000000..d1fe0705848e2 --- /dev/null +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2010-2021 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.pulseaudio.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; +import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioSink; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.audio.FixedLengthAudioStream; +import org.openhab.core.audio.UnsupportedAudioFormatException; +import org.openhab.core.audio.UnsupportedAudioStreamException; +import org.openhab.core.library.types.PercentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The audio sink for openhab, implemented by a connection to a pulseaudio sink + * + * @author Gwendal Roulleau - Initial contribution + * + */ +@NonNullByDefault +public class PulseAudioAudioSink implements AudioSink { + + private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class); + + private static final HashSet SUPPORTED_FORMATS = new HashSet<>(); + private static final HashSet> SUPPORTED_STREAMS = new HashSet<>(); + + private PulseaudioHandler pulseaudioHandler; + + private @Nullable Socket clientSocket; + + static { + SUPPORTED_FORMATS.add(AudioFormat.WAV); + SUPPORTED_FORMATS.add(AudioFormat.MP3); + SUPPORTED_STREAMS.add(FixedLengthAudioStream.class); + } + + public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) { + this.pulseaudioHandler = pulseaudioHandler; + } + + @Override + public String getId() { + return pulseaudioHandler.getThing().getUID().toString(); + } + + @Override + public @Nullable String getLabel(@Nullable Locale locale) { + return pulseaudioHandler.getThing().getLabel(); + } + + /** + * Convert MP3 to PCM, as this is the only possible format + * + * @param input + * @return + */ + private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) { + try { + MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader(); + AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input); + javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat(); + + MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider(); + javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat( + javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16, + sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false); + + return mpegconverter.getAudioInputStream(convertFormat, sourceAIS); + + } catch (IOException | UnsupportedAudioFileException e) { + logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage()); + } + return null; + } + + /** + * Connect to pulseaudio with the simple protocol + * + * @throws IOException + * @throws InterruptedException when interrupted during the loading module wait + */ + public void connectIfNeeded() throws IOException, InterruptedException { + Socket clientSocketLocal = clientSocket; + if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { + String host = pulseaudioHandler.getHost(); + int port = pulseaudioHandler.getSimpleTcpPort(); + clientSocket = new Socket(host, port); + clientSocket.setSoTimeout(500); + } + } + + /** + * Disconnect the socket to pulseaudio simple protocol + */ + public void disconnect() { + if (clientSocket != null) { + try { + clientSocket.close(); + } catch (IOException e) { + } + } + } + + @Override + public void process(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + + if (audioStream == null) { + return; + } + + InputStream audioInputStream = null; + try { + + if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) { + audioInputStream = getPCMStreamFromMp3Stream(audioStream); + } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) { + audioInputStream = audioStream; + } else { + throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream", + audioStream.getFormat()); + } + + for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed + try { + connectIfNeeded(); + if (audioInputStream != null && clientSocket != null) { + // send raw audio to the socket and to pulse audio + audioInputStream.transferTo(clientSocket.getOutputStream()); + break; + } + } catch (IOException e) { + disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown + if (countAttempt == 2) { // we won't retry : log and quit + if (logger.isWarnEnabled()) { + String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown"; + logger.warn( + "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}", + pulseaudioHandler.getHost(), port, e.getMessage()); + } + break; + } + } catch (InterruptedException ie) { + logger.info("Interrupted during sink audio connection: {}", ie.getMessage()); + break; + } + } + } finally { + try { + if (audioInputStream != null) { + audioInputStream.close(); + } + audioStream.close(); + } catch (IOException e) { + } + } + } + + @Override + public Set getSupportedFormats() { + return SUPPORTED_FORMATS; + } + + @Override + public Set> getSupportedStreams() { + return SUPPORTED_STREAMS; + } + + @Override + public PercentType getVolume() { + return new PercentType(pulseaudioHandler.getLastVolume()); + } + + @Override + public void setVolume(PercentType volume) { + pulseaudioHandler.setVolume(volume.intValue()); + } +} diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java index 19d17c9fec0b8..9c8f4f8521927 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java @@ -51,6 +51,11 @@ public class PulseaudioBindingConstants { public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh"; public static final String DEVICE_PARAMETER_NAME = "name"; + public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink"; + public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort"; + + public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp"; + public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711; public static final Map TYPE_FILTERS = new HashMap<>(); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 39fa310c25269..54cbc552b2a21 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -24,8 +24,10 @@ import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.Random; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.pulseaudio.internal.cli.Parser; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State; @@ -139,28 +141,30 @@ public boolean isConnected() { /** * updates the item states and their relationships */ - public void update() { - modules.clear(); - modules.addAll(Parser.parseModules(listModules())); + public synchronized void update() { + // one step copy + modules = new ArrayList(Parser.parseModules(listModules())); - items.clear(); - if (TYPE_FILTERS.get(SINK_THING_TYPE.getId())) { + List newItems = new ArrayList<>(); // prepare new list before assigning it + newItems.clear(); + if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sinks"); - items.addAll(Parser.parseSinks(listSinks(), this)); + newItems.addAll(Parser.parseSinks(listSinks(), this)); } - if (TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sources"); - items.addAll(Parser.parseSources(listSources(), this)); + newItems.addAll(Parser.parseSources(listSources(), this)); } - if (TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sink-inputs"); - items.addAll(Parser.parseSinkInputs(listSinkInputs(), this)); + newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this)); } - if (TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading source-outputs"); - items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this)); + newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this)); } - logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), items.size()); + logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size()); + items = newItems; } private String listModules() { @@ -378,6 +382,74 @@ public void setVolume(AbstractAudioDeviceConfig item, int vol) { item.setVolume(Math.round(100f / 65536f * vol)); } + /** + * Locate or load (if needed) the simple protocol tcp module for the given sink + * and returns the port. + * The module loading (if needed) will be tried several times, on a new random port each time. + * + * @param item the sink we are searching for + * @param simpleTcpPortPref the port to use if we have to load the module + * @return the port on which the module is listening + * @throws InterruptedException + */ + public Optional loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item, + Integer simpleTcpPortPref) throws InterruptedException { + int currentTry = 0; + int simpleTcpPortToTry = simpleTcpPortPref; + do { + Optional simplePort = findSimpleProtocolTcpModule(item); + + if (simplePort.isPresent()) { + return simplePort; + } else { + sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port=" + + simpleTcpPortToTry); + simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024 + } + Thread.sleep(100); + currentTry++; + } while (currentTry < 3); + + logger.warn("The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp" + + " on random port on the pulseaudio server and give up trying"); + return Optional.empty(); + } + + /** + * Find a simple protocol module corresponding to the given sink in argument + * and returns the port it listens to + * + * @param item + * @return + */ + private Optional findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) { + update(); + + List modulesCopy = new ArrayList(modules); + return modulesCopy.stream() // iteration on modules + .filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name + .filter(module -> extractArgumentFromLine("sink", module.getArgument()) // extract sink in argument + .map(sinkName -> sinkName.equals(item.getPaName())).orElse(false)) // filter on sink name + .findAny() // get a corresponding module + .map(module -> extractArgumentFromLine("port", module.getArgument()) + .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port + .map(portS -> Integer.parseInt(portS)); + } + + private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) { + String argument = null; + int startPortIndex = argumentLine.indexOf(argumentWanted + "="); + if (startPortIndex != -1) { + startPortIndex = startPortIndex + argumentWanted.length() + 1; + int endPortIndex = argumentLine.indexOf(" ", startPortIndex); + if (endPortIndex == -1) { + endPortIndex = argumentLine.length(); + } + argument = argumentLine.substring(startPortIndex, endPortIndex); + } + return Optional.ofNullable(argument); + } + /** * returns the item names that can be used in commands * @@ -405,13 +477,14 @@ private String getItemCommandName(AbstractAudioDeviceConfig item) { * values from 0 - 100) */ public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) { + int volumeToSet = vol; if (item == null) { return; } - if (vol <= 100) { - vol = toAbsoluteVolume(vol); + if (volumeToSet <= 100) { + volumeToSet = toAbsoluteVolume(volumeToSet); } - setVolume(item, vol); + setVolume(item, volumeToSet); } /** @@ -442,7 +515,7 @@ public void setCombinedSinkSlaves(Sink combinedSink, List sinks) { sendRawCommand(CMD_UNLOAD_MODULE + " " + combinedSink.getModule().getId()); // 2. add new combined-sink with same name and all slaves sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName() - + " slaves=" + StringUtils.join(slaves, ",")); + + " slaves=" + String.join(",", slaves)); // 3. update internal data structure because the combined sink has a new number + other slaves update(); } @@ -533,7 +606,7 @@ public void setCombinedSinkSlaves(String combinedSinkName, List sinks) { } // add new combined-sink with same name and all slaves sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSinkName + " slaves=" - + StringUtils.join(slaves, ",")); + + String.join(",", slaves)); // update internal data structure because the combined sink is new update(); } @@ -584,6 +657,8 @@ private String sendRawRequest(String command) { } catch (SocketTimeoutException e) { // Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance // to detect the end of the answer, except by this timeout + } catch (SocketException e) { + logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage()); } catch (IOException e) { logger.error("Exception while reading socket: {}", e.getMessage()); } @@ -620,7 +695,7 @@ private void connect() throws IOException { } catch (NoRouteToHostException e) { logger.error("no route to host {}", host); } catch (SocketException e) { - logger.error("{}", e.getLocalizedMessage(), e); + logger.error("cannot connect to host {} : {}", host, e.getMessage()); } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java index 4afffd91b7652..06c32ab26605d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java @@ -92,25 +92,28 @@ private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, ThingUID thin @Override protected void removeHandler(ThingHandler thingHandler) { - if (this.discoveryServiceReg.containsKey(thingHandler)) { + ServiceRegistration serviceRegistration = this.discoveryServiceReg.get(thingHandler); + if (serviceRegistration != null) { PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext - .getService(discoveryServiceReg.get(thingHandler).getReference()); + .getService(serviceRegistration.getReference()); service.deactivate(); - discoveryServiceReg.get(thingHandler).unregister(); - discoveryServiceReg.remove(thingHandler); + serviceRegistration.unregister(); } + discoveryServiceReg.remove(thingHandler); super.removeHandler(thingHandler); } @Override protected ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing); registerDeviceDiscoveryService(handler); return handler; } else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new PulseaudioHandler(thing); + return new PulseaudioHandler(thing, bundleContext); } return null; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java index 68905acc44c39..9675d4400f790 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java @@ -135,15 +135,18 @@ public static Collection parseSinks(String raw, PulseaudioClient client) { } } if (properties.containsKey("muted")) { - sink.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + sink.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); } if (properties.containsKey("combine.slaves")) { // this is a combined sink, the combined sink object should be - for (String sinkName : properties.get("combine.slaves").replace("\"", "").split(",")) { - sink.addCombinedSinkName(sinkName); + String sinkNames = properties.get("combine.slaves"); + if (sinkNames != null) { + for (String sinkName : sinkNames.replace("\"", "").split(",")) { + sink.addCombinedSinkName(sinkName); + } } combinedSinks.add(sink); } @@ -203,7 +206,7 @@ public static List parseSinkInputs(String raw, PulseaudioClient clien } } if (properties.containsKey("muted")) { - item.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + item.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { item.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); @@ -262,7 +265,7 @@ public static List parseSources(String raw, PulseaudioClient client) { } } if (properties.containsKey("muted")) { - source.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + source.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { source.setVolume(parseVolume(properties.get("volume"))); @@ -322,7 +325,7 @@ public static List parseSourceOutputs(String raw, PulseaudioClient } } if (properties.containsKey("muted")) { - item.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + item.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { item.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java index 86ffa16c99bda..3a7a24fe3891c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java @@ -73,7 +73,6 @@ public DiscoveryResult createResult(ServiceInfo info) { } return result; } catch (IOException e) { - result = null; } } return result; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java index 12d56ad7d96a5..c91ecfe2c55de 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java @@ -155,8 +155,12 @@ public void initialize() { @Override public void dispose() { - pollingJob.cancel(true); - client.disconnect(); + if (pollingJob != null) { + pollingJob.cancel(true); + } + if (client != null) { + client.disconnect(); + } super.dispose(); } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index 357523051680a..88690998ac819 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -14,19 +14,26 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*; +import java.io.IOException; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; +import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; 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.pulseaudio.internal.PulseAudioAudioSink; +import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.Sink; import org.openhab.binding.pulseaudio.internal.items.SinkInput; +import org.openhab.core.audio.AudioSink; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; @@ -45,6 +52,8 @@ import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,8 +78,17 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL private String name; - public PulseaudioHandler(Thing thing) { + private PulseAudioAudioSink audioSink; + + private Integer savedVolume; + + private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); + + private BundleContext bundleContext; + + public PulseaudioHandler(Thing thing, BundleContext bundleContext) { super(thing); + this.bundleContext = bundleContext; } @Override @@ -81,6 +99,42 @@ public void initialize() { // until we get an update put the Thing offline updateStatus(ThingStatus.OFFLINE); deviceOnlineWatchdog(); + + // if it's a SINK thing, then maybe we have to activate the audio sink + if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) { + // check the property to see if we it's enabled : + Boolean sinkActivated = (Boolean) thing.getConfiguration() + .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION); + if (sinkActivated != null && sinkActivated) { + audioSinkSetup(); + } + } + } + + private void audioSinkSetup() { + final PulseaudioHandler thisHandler = this; + scheduler.submit(new Runnable() { + @Override + public void run() { + // Register the sink as an audio sink in openhab + logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID()); + PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler); + setAudioSink(audioSink); + try { + audioSink.connectIfNeeded(); + } catch (IOException e) { + logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})", + getHost(), e.getMessage()); + } catch (InterruptedException i) { + logger.info("Interrupted during sink audio connection: {}", i.getMessage()); + return; + } + @SuppressWarnings("unchecked") + ServiceRegistration reg = (ServiceRegistration) bundleContext + .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>()); + audioSinkRegistrations.put(thing.getUID().toString(), reg); + } + }); } @Override @@ -90,9 +144,21 @@ public void dispose() { refreshJob = null; } updateStatus(ThingStatus.OFFLINE); + bridgeHandler.unregisterDeviceStatusListener(this); bridgeHandler = null; logger.trace("Thing {} {} disposed.", getThing().getUID(), name); super.dispose(); + + if (audioSink != null) { + audioSink.disconnect(); + } + + // Unregister the potential pulse audio sink's audio sink + ServiceRegistration reg = audioSinkRegistrations.remove(getThing().getUID().toString()); + if (reg != null) { + logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID()); + reg.unregister(); + } } private void deviceOnlineWatchdog() { @@ -163,15 +229,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { // refresh to get the current volume level bridge.getClient().update(); device = bridge.getDevice(name); - int volume = device.getVolume(); + savedVolume = device.getVolume(); if (command.equals(IncreaseDecreaseType.INCREASE)) { - volume = Math.min(100, volume + 5); + savedVolume = Math.min(100, savedVolume + 5); } if (command.equals(IncreaseDecreaseType.DECREASE)) { - volume = Math.max(0, volume - 5); + savedVolume = Math.max(0, savedVolume - 5); } - bridge.getClient().setVolumePercent(device, volume); - updateState = new PercentType(volume); + bridge.getClient().setVolumePercent(device, savedVolume); + updateState = new PercentType(savedVolume); } else if (command instanceof PercentType) { DecimalType volume = (DecimalType) command; bridge.getClient().setVolumePercent(device, volume.intValue()); @@ -228,12 +294,37 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + /** + * Use last checked volume for faster access + * + * @return + */ + public int getLastVolume() { + if (savedVolume == null) { + PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridge.getDevice(name); + // refresh to get the current volume level + bridge.getClient().update(); + device = bridge.getDevice(name); + savedVolume = device.getVolume(); + } + return savedVolume == null ? 50 : savedVolume; + } + + public void setVolume(int volume) { + PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridge.getDevice(name); + bridge.getClient().setVolumePercent(device, volume); + updateState(VOLUME_CHANNEL, new PercentType(volume)); + } + @Override public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) { if (device.getPaName().equals(name)) { updateStatus(ThingStatus.ONLINE); logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL); - updateState(VOLUME_CHANNEL, new PercentType(device.getVolume())); + savedVolume = device.getVolume(); + updateState(VOLUME_CHANNEL, new PercentType(savedVolume)); updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF); updateState(STATE_CHANNEL, device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-")); @@ -244,17 +335,45 @@ public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig devi : new StringType("-")); } if (device instanceof Sink && ((Sink) device).isCombinedSink()) { - updateState(SLAVES_CHANNEL, - new StringType(StringUtils.join(((Sink) device).getCombinedSinkNames(), ","))); + updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames()))); } } } + public String getHost() { + Bridge bridge = getBridge(); + if (bridge != null) { + return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); + } else { + logger.error("A bridge must be configured for this pulseaudio thing"); + return "null"; + } + } + + /** + * This method will scan the pulseaudio server to find the port on which the module/sink is listening + * If no module is listening, then it will command the module to load on the pulse audio server, + * + * @return the port on which the pulseaudio server is listening for this sink + * @throws InterruptedException when interrupted during the loading module wait + */ + public int getSimpleTcpPort() throws InterruptedException { + Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration() + .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue(); + + PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name); + return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref) + .orElse(simpleTcpPortPref); + } + @Override public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) { if (device.getPaName().equals(name)) { bridgeHandler.unregisterDeviceStatusListener(this); bridgeHandler = null; + audioSink.disconnect(); + audioSink = null; updateStatus(ThingStatus.OFFLINE); } } @@ -263,4 +382,8 @@ public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceC public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) { logger.trace("new device discovered {} by {}", device, bridge); } + + public void setAudioSink(PulseAudioAudioSink audioSink) { + this.audioSink = audioSink; + } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/bridge.xml index c5a43cbb393be..986e677b02966 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/bridge.xml @@ -9,23 +9,20 @@ This bridge represents a pulseaudio server. - + Hostname or IP address of the pulseaudio server localhost - true - + Port of the pulseaudio server 4712 - false - + The refresh interval in ms which is used to poll given pulseaudio server. 30000 - false diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/combined-sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/combined-sink.xml index 277fd5390e291..0d850a9fa4558 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/combined-sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/combined-sink.xml @@ -18,10 +18,9 @@ - + The name of the combined sink. - true diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml index 0ecda4bfc5de2..76ee975019e9d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml @@ -18,10 +18,9 @@ - + The name of one specific device. - true diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml index 727f81c346dcb..9f98aca6ace7f 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml @@ -10,6 +10,7 @@ represents a pulseaudio sink + Speaker @@ -17,10 +18,20 @@ - + The name of one specific device. - true + + + + Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the + pulseaudio server) + false + + + + Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server + 4711 diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml index 5e1e590a60a17..847f3fe308d39 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml @@ -17,10 +17,9 @@ - + The name of one specific device. - true diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml index e926978cca34b..f783b3485b291 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml @@ -17,10 +17,9 @@ - + The name of one specific device. - true diff --git a/bundles/org.openhab.binding.pushover/README.md b/bundles/org.openhab.binding.pushover/README.md index eff3bc485b4f9..1719689689c9e 100644 --- a/bundles/org.openhab.binding.pushover/README.md +++ b/bundles/org.openhab.binding.pushover/README.md @@ -17,7 +17,7 @@ You are able to create multiple instances of this Thing to broadcast to differen | `apikey` | text | Your API token / key (APP_TOKEN) to access the Pushover Message API. **mandatory** | | `user` | text | Your user key or group key (USER_KEY) to which you want to push notifications. **mandatory** | | `title` | text | The default title of a message (default: `openHAB`). | -| `format` | text | The default format (`none`, `HTML` or `monospace`) of a message (default: `none`). | +| `format` | text | The default format (`none`, `html` or `monospace`) of a message (default: `none`). | | `sound` | text | The default notification sound on target device (default: `default`) (see [supported notification sounds](https://pushover.net/api#sounds)). | | `retry` | integer | The retry parameter specifies how often (in seconds) the Pushover servers will send the same notification to the user (default: `300`). **advanced** | | `expire` | integer | The expire parameter specifies how long (in seconds) your notification will continue to be retried (default: `3600`). **advanced** | @@ -31,7 +31,13 @@ Currently the binding does not support any Channels. ## Thing Actions All actions return a `Boolean` value to indicate if the message was sent successfully or not. +If the communication to Pushover servers fails the binding does not try to send the message again. +One has to take care of that on its own if it is important. The parameter `message` is **mandatory**, the `title` parameter defaults to whatever value you defined in the `title` related configuration parameter. +Parameters declared as `@Nullable` are not optional. +One has to pass a `null` value if it should be skipped or the default value for it should be used. + +- `sendMessage(String message, @Nullable String title, @Nullable String sound, @Nullable String url, @Nullable String urlTitle, @Nullable String attachment, @Nullable String contentType, @Nullable Integer priority, @Nullable String device)` - This method is used to send a plain text message providing all available parameters. - `sendMessage(String message, @Nullable String title)` - This method is used to send a plain text message. diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java index 2b85055fd79e8..ebef8cdc058d5 100644 --- a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java +++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.pushover.internal.actions; -import static org.openhab.binding.pushover.internal.PushoverBindingConstants.DEFAULT_TITLE; +import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*; import static org.openhab.binding.pushover.internal.connection.PushoverMessageBuilder.*; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -43,6 +43,53 @@ public class PushoverActions implements ThingActions { private @NonNullByDefault({}) PushoverAccountHandler accountHandler; + @RuleAction(label = "@text/sendMessageActionLabel", description = "@text/sendMessageActionDescription") + public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendMessage( + @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message, + @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title, + @ActionInput(name = "sound", label = "@text/sendMessageActionInputSoundLabel", description = "@text/sendMessageActionInputSoundDescription", type = "java.lang.String", defaultValue = DEFAULT_SOUND) @Nullable String sound, + @ActionInput(name = "url", label = "@text/sendMessageActionInputURLLabel", description = "@text/sendMessageActionInputURLDescription", type = "java.lang.String") @Nullable String url, + @ActionInput(name = "urlTitle", label = "@text/sendMessageActionInputURLTitleLabel", description = "@text/sendMessageActionInputURLTitleDescription", type = "java.lang.String") @Nullable String urlTitle, + @ActionInput(name = "attachment", label = "@text/sendMessageActionInputAttachmentLabel", description = "@text/sendMessageActionInputAttachmentDescription", type = "java.lang.String") @Nullable String attachment, + @ActionInput(name = "contentType", label = "@text/sendMessageActionInputContentTypeLabel", description = "@text/sendMessageActionInputContentTypeDescription", type = "java.lang.String", defaultValue = DEFAULT_CONTENT_TYPE) @Nullable String contentType, + @ActionInput(name = "priority", label = "@text/sendMessageActionInputPriorityLabel", description = "@text/sendMessageActionInputPriorityDescription", type = "java.lang.Integer", defaultValue = DEFAULT_EMERGENCY_PRIORITY) @Nullable Integer priority, + @ActionInput(name = "device", label = "@text/sendMessageActionInputDeviceLabel", description = "@text/sendMessageActionInputDeviceDescription", type = "java.lang.String") @Nullable String device) { + logger.trace( + "ThingAction 'sendMessage' called with value(s): message='{}', title='{}', sound='{}', url='{}', urlTitle='{}', attachment='{}', contentType='{}', priority='{}', device='{}'", + message, title, sound, url, urlTitle, attachment, contentType, priority, device); + + PushoverMessageBuilder builder = getDefaultPushoverMessageBuilder(message); + if (sound != null) { + builder.withSound(sound); + } + if (url != null) { + builder.withUrl(url); + if (urlTitle != null) { + builder.withUrlTitle(urlTitle); + } + } + if (attachment != null) { + builder.withAttachment(attachment); + if (contentType != null) { + builder.withContentType(contentType); + } + } + if (priority != null) { + builder.withPriority(priority.intValue()); + } + if (device != null) { + builder.withDevice(device); + } + return send(builder, title); + } + + public static Boolean sendMessage(ThingActions actions, String message, @Nullable String title, + @Nullable String sound, @Nullable String url, @Nullable String urlTitle, @Nullable String attachment, + @Nullable String contentType, @Nullable Integer priority, @Nullable String device) { + return ((PushoverActions) actions).sendMessage(message, title, sound, url, urlTitle, attachment, contentType, + priority, device); + } + @RuleAction(label = "@text/sendMessageActionLabel", description = "@text/sendMessageActionDescription") public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendMessage( @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message, diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java index cc5cd1a73cc88..8e8f6e820c1d3 100644 --- a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java +++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java @@ -37,7 +37,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -61,8 +61,6 @@ public class PushoverAPIConnection { private final ExpiringCacheMap cache = new ExpiringCacheMap<>(TimeUnit.DAYS.toMillis(1)); - private final JsonParser parser = new JsonParser(); - public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) { this.httpClient = httpClient; this.config = config; @@ -80,7 +78,7 @@ public boolean sendMessage(PushoverMessageBuilder message) public String sendPriorityMessage(PushoverMessageBuilder message) throws PushoverCommunicationException, PushoverConfigurationException { - final JsonObject json = parser.parse(post(MESSAGE_URL, message.build())).getAsJsonObject(); + final JsonObject json = JsonParser.parseString(post(MESSAGE_URL, message.build())).getAsJsonObject(); return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : ""; } @@ -100,16 +98,14 @@ public List getSounds() throws PushoverCommunicationException, PushoverCo params.put(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey); // TODO do not cache the response, cache the parsed list of sounds - final JsonObject json = parser.parse(getFromCache(buildURL(SOUNDS_URL, params))).getAsJsonObject(); - if (json.has("sounds")) { - final JsonObject sounds = json.get("sounds").getAsJsonObject(); - if (sounds != null) { - return Collections.unmodifiableList(sounds.entrySet().stream() + final String content = getFromCache(buildURL(SOUNDS_URL, params)); + final JsonObject json = content == null ? null : JsonParser.parseString(content).getAsJsonObject(); + final JsonObject sounds = json == null || !json.has("sounds") ? null : json.get("sounds").getAsJsonObject(); + + return sounds == null ? List.of() + : Collections.unmodifiableList(sounds.entrySet().stream() .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString())) .collect(Collectors.toList())); - } - } - return Collections.emptyList(); } private String buildURL(String url, Map requestParams) { @@ -134,7 +130,7 @@ private String post(String url, ContentProvider body) return executeRequest(HttpMethod.POST, url, body); } - private String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body) + private synchronized String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body) throws PushoverCommunicationException, PushoverConfigurationException { logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url); try { @@ -172,18 +168,16 @@ private String executeRequest(HttpMethod httpMethod, String url, @Nullable Conte } private String getMessageError(String content) { - final JsonObject json = parser.parse(content).getAsJsonObject(); - if (json.has("errors")) { - final JsonArray errors = json.get("errors").getAsJsonArray(); - if (errors != null) { - return errors.toString(); - } + final JsonObject json = JsonParser.parseString(content).getAsJsonObject(); + final JsonElement errorsElement = json.get("errors"); + if (errorsElement != null && errorsElement.isJsonArray()) { + return errorsElement.getAsJsonArray().toString(); } - return "Unknown error occured."; + return "@text/offline.conf-error-unknown"; } private boolean getMessageStatus(String content) { - final JsonObject json = parser.parse(content).getAsJsonObject(); + final JsonObject json = JsonParser.parseString(content).getAsJsonObject(); return json.has("status") ? json.get("status").getAsInt() == 1 : false; } diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java index a08d95242f825..2ce4f3cfa0049 100644 --- a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java +++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java @@ -31,8 +31,7 @@ import org.osgi.service.component.annotations.Reference; /** - * The {@link PushoverHandlerFactory} is responsible for creating things and thing - * handlers. + * The {@link PushoverHandlerFactory} is responsible for creating things and thing handlers. * * @author Christoph Weitkamp - Initial contribution */ diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java index ae717907636a1..9840bc23ae8c6 100644 --- a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java +++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java @@ -134,7 +134,14 @@ public PushoverMessageBuilder getDefaultPushoverMessageBuilder(String message) { public boolean sendMessage(PushoverMessageBuilder messageBuilder) { if (connection != null) { - return connection.sendMessage(messageBuilder); + try { + return connection.sendMessage(messageBuilder); + } catch (PushoverCommunicationException e) { + // do nothing, causing exception is already logged + } catch (PushoverConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + return false; } else { throw new IllegalArgumentException("PushoverAPIConnection is null!"); } @@ -142,7 +149,14 @@ public boolean sendMessage(PushoverMessageBuilder messageBuilder) { public String sendPriorityMessage(PushoverMessageBuilder messageBuilder) { if (connection != null) { - return connection.sendPriorityMessage(messageBuilder); + try { + return connection.sendPriorityMessage(messageBuilder); + } catch (PushoverCommunicationException e) { + // do nothing, causing exception is already logged + } catch (PushoverConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + return ""; } else { throw new IllegalArgumentException("PushoverAPIConnection is null!"); } @@ -150,12 +164,20 @@ public String sendPriorityMessage(PushoverMessageBuilder messageBuilder) { public boolean cancelPriorityMessage(String receipt) { if (connection != null) { - return connection.cancelPriorityMessage(receipt); + try { + return connection.cancelPriorityMessage(receipt); + } catch (PushoverCommunicationException e) { + // do nothing, causing exception is already logged + } catch (PushoverConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + return false; } else { throw new IllegalArgumentException("PushoverAPIConnection is null!"); } } + @SuppressWarnings("null") private void asyncValidateUser() { try { connection.validateUser(); diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties index c6a3bf9a84703..85e66740a8985 100644 --- a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties +++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties @@ -1,6 +1,7 @@ # user defined messages offline.conf-error-missing-apikey = The 'apikey' parameter must be configured. offline.conf-error-missing-user = The 'user' parameter must be configured. +offline.conf-error-unknown = An unknown error occurred. # actions sendMessageActionLabel = send a plain text message @@ -19,6 +20,9 @@ sendMessageActionInputURLDescription = A supplementary URL to show with the mess sendMessageActionInputURLTitleLabel = URL Title sendMessageActionInputURLTitleDescription = A title for the URL, otherwise just the URL is shown. +sendMessageActionInputSoundLabel = Sound +sendMessageActionInputSoundDescription = The notification sound on target device. + sendHTMLMessageActionLabel = send a HTML message sendHTMLMessageActionDescription = This method is used to send a HTML message. diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties index bcc86ce12c8a0..9f094e31e8f77 100644 --- a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties +++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties @@ -24,6 +24,7 @@ thing-type.config.pushover.pushover-account.expire.description = Dieser Paramete # user defined messages offline.conf-error-missing-apikey = Der Parameter 'apikey' muss konfiguriert werden. offline.conf-error-missing-user = Der Parameter 'user' muss konfiguriert werden. +offline.conf-error-unknown = Ein unbekannter Fehler ist aufgetreten. # actions sendMessageActionLabel = eine Textnachricht senden @@ -35,6 +36,9 @@ sendMessageActionInputMessageDescription = Die Nachricht. sendMessageActionInputTitleLabel = Titel sendMessageActionInputTitleDescription = Titel der Nachricht. +sendMessageActionInputSoundLabel = Benachrichtigungston +sendMessageActionInputSoundDescription = Benachrichtigungston auf dem Endger�t. + sendURLMessageActionLabel = eine Textnachricht mit URL senden sendURLMessageActionDescription = Action zum Versenden einer Textnachricht mit einer URL. sendMessageActionInputURLLabel = URL diff --git a/bundles/org.openhab.binding.qbus/NOTICE b/bundles/org.openhab.binding.qbus/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md new file mode 100644 index 0000000000000..50b5bedccdf19 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/README.md @@ -0,0 +1,110 @@ +# Qbus Binding + +![Qbus Logo](doc/Logo.JPG) + +This binding for [Qbus](https://qbus.be) communicates with all controllers of the Qbus home automation system. + +We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lots of information to set up openHAB with Qbus client and server (for the moment only in Dutch). + +The controllers can not communicate directly with openHAB, therefore we developed a client/server application which you must install prior to enable this binding. +More information can be found here: +[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer-Installer) + +With this binding you can control and read almost every output from the Qbus system. + +## Supported Things + +The following things are supported by the Qbus binding: + +- `dimmer`: Dimmer 1 button, 2 button and clc +- `onOff`: Bistabiel, Timer1-3, Interval +- `thermostats`: Thermostats - normal and PID +- `scene`: Scenes +- `co2`: CO2 +- `rollershutter`: Rollershutter +- `rollershutter_slats`: Rollerhutter with slats + +For now the following Qbus things are not yet supported but will come: + +- DMX +- Timer 4 & 5 +- HVAC +- Humidity +- Renson +- Duco +- Kinetura +- Energy monitor +- Weather station + + +## Discovery + +The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB. + +## Bridge configuration + +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] { +... +} +``` + + + +| Property | Default | Required | Description | +| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | +| `sn` | | YES | The serial number of your controller | +| `port` | 8447 | YES | The communication port of the client/server | +| `serverCheck` | 10 | NO | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect | + + + +## Things configuration + +| Thing Type ID | Channel Name | Read only | description | +| --------------------- | ------------- | --------- | ------------------------------------------------------ | +| `onOff` | switch | No | This is the channel for Bistable, Timers and Intervals | +| `dimmer` | brightness | No | This is the channel for Dimmers 1&2 buttons and CLC | +| `scene` | Switch | No | This is the channel for scenes | +| `co2` | co2 | Yes | This is the channel for CO2 sensors | +| `rollershutter` | rollershutter | No | This is the channel for rollershutters | +| `rollershutter_slats` | rollershutter | No | This is the channel for rollershutters with slats | +| `thermostat` | setpoint | No | This is the channel for thermostats setpoint | +| `thermostat` | measured | Yes | This is the channel for thermostats currenttemp | +| `thermostat` | mode | No | This is the channel for thermostats mode | + + +## Full Example + +### Things + +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] { + dimmer 1 "ToonzaalLED" [ dimmerId=100 ] + onOff 30 "Toonzaal230V" [ bistabielId=76 ] + thermostat 50 "Service" [ thermostatId=99 ] + scene 70 "Disco" [ sceneId=36 ] + co2 100 "Productie" [ co2Id=26 ] + rollershutter 120 "Roller1" [ rolId=268 ] + rollershutter_slats 121 "Roller2" [ rolId=264 ] +} +``` + +### Items + +``` +Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} +Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} +Number:Temperature ServiceSP"[%.1f %unit%]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} +Number:Temperature ServiceCT"[%.1f %unit%]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} +Number ServiceMode (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"} +Switch Disco {channel="qbus:scene:CTD007841:36:scene"} +Number ProductieCO2 {channel="qbus:co2:CTD007841:100:co2"} +Rollershutter Roller1 {channel="qbus:rollershutter:CTD007841:120:rollershutter"} +Rollershutter Roller2 {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"} +Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} +``` + +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings of this binding and offers a way to communicate with other users. + diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG new file mode 100644 index 0000000000000..93f10e4de88ff Binary files /dev/null and b/bundles/org.openhab.binding.qbus/doc/Logo.JPG differ diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml new file mode 100644 index 0000000000000..c611bfc48f111 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.qbus + + openHAB Add-ons :: Bundles :: Qbus Binding + + diff --git a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml new file mode 100644 index 0000000000000..fcae4ee3aee20 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml @@ -0,0 +1,23 @@ + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.qbus/${project.version} + + diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java new file mode 100644 index 0000000000000..57b0cf5575a78 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link QbusBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Koen Schockaert - Initial contribution + */ +@NonNullByDefault +public class QbusBindingConstants { + + private static final String BINDING_ID = "qbus"; + + // bridge + public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final Set BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE); + // Bridge config properties + public static final String CONFIG_HOST_NAME = "addr"; + public static final String CONFIG_PORT = "port"; + public static final String CONFIG_SN = "sn"; + public static final String CONFIG_SERVERCHECK = "serverCheck"; + + // generic thing types + public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); + public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); + public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); + public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID, + "rollershutter_slats"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); + + // List of all Thing Type UIDs + public static final Set SCENE_THING_TYPES_UIDS = Set.of(THING_TYPE_SCENE); + public static final Set CO2_THING_TYPES_UIDS = Set.of(THING_TYPE_CO2); + public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER); + public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER_SLATS); + public static final Set BISTABIEL_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT); + public static final Set THERMOSTAT_THING_TYPES_UIDS = Set.of(THING_TYPE_THERMOSTAT); + public static final Set DIMMER_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT, + THING_TYPE_DIMMABLE_LIGHT); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT, + THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, THING_TYPE_SCENE, THING_TYPE_CO2, + THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS); + + // List of all Channel ids + public static final String CHANNEL_SWITCH = "switch"; + public static final String CHANNEL_SCENE = "scene"; + public static final String CHANNEL_BRIGHTNESS = "brightness"; + public static final String CHANNEL_MEASURED = "measured"; + public static final String CHANNEL_SETPOINT = "setpoint"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_CO2 = "co2"; + public static final String CHANNEL_ROLLERSHUTTER = "rollershutter"; + public static final String CHANNEL_SLATS = "slats"; + + // Thing config properties + public static final String CONFIG_BISTABIEL_ID = "bistabielId"; + public static final String CONFIG_DIMMER_ID = "dimmerId"; + public static final String CONFIG_THERMOSTAT_ID = "thermostatId"; + public static final String CONFIG_SCENE_ID = "sceneId"; + public static final String CONFIG_CO2_ID = "co2Id"; + public static final String CONFIG_ROLLERSHUTTER_ID = "rolId"; + public static final String CONFIG_STEP_VALUE = "step"; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java new file mode 100644 index 0000000000000..2f625669ae348 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -0,0 +1,359 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal; + +import java.io.IOException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +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.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link QbusBridgeHandler} is the handler for a Qbus controller + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusBridgeHandler extends BaseBridgeHandler { + + private @Nullable QbusCommunication qbusComm; + + protected @Nullable QbusConfiguration bridgeConfig = new QbusConfiguration(); + + private @Nullable ScheduledFuture refreshTimer; + + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + + public QbusBridgeHandler(Bridge Bridge) { + super(Bridge); + } + + /** + * Initialize the bridge + */ + @Override + public void initialize() { + Integer serverCheck = getServerCheck(); + + readConfig(); + + createCommunicationObject(); + + if (serverCheck != null) { + this.setupRefreshTimer(serverCheck); + } + } + + /** + * Sets the Bridge call back + */ + private void setBridgeCallBack() { + QbusCommunication qbusCommunication = getQbusCommunication(); + if (qbusCommunication != null) { + qbusCommunication.setBridgeCallBack(this); + } + } + + /** + * Create communication object to Qbus server and start communication. + * + * @param addr : IP address of Qbus server + * @param port : Communication port of QbusServer + */ + private void createCommunicationObject() { + scheduler.submit(() -> { + + setQbusCommunication(new QbusCommunication(thing)); + + QbusCommunication qbusCommunication = getQbusCommunication(); + + setBridgeCallBack(); + + Integer serverCheck = getServerCheck(); + String sn = getSn(); + if (serverCheck != null) { + if (sn != null) { + if (qbusCommunication != null) { + try { + qbusCommunication.startCommunication(); + } catch (InterruptedException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. InterruptedException: " + msg); + return; + } catch (IOException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. IOException: " + msg); + return; + } + + if (!qbusCommunication.communicationActive()) { + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Server, will try to reconnect every " + serverCheck + + " minutes"); + return; + } + + if (!qbusCommunication.clientConnected()) { + bridgePending("Waiting for Qbus client to come online"); + return; + } + + } + } + } + }); + } + + /** + * Updates offline status off the Bridge when an error occurs. + * + * @param status + * @param detail + * @param message + */ + public void bridgeOffline(ThingStatusDetail detail, String message) { + updateStatus(ThingStatus.OFFLINE, detail, message); + } + + /** + * Updates pending status off the Bridge (usualay when Qbus client id not connected) + * + * @param message + */ + public void bridgePending(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING, message); + } + + /** + * Put bridge online when error in communication resolved. + */ + public void bridgeOnline() { + updateStatus(ThingStatus.ONLINE); + } + + /** + * Initializes a timer that check the communication with Qbus server/client and tries to re-establish communication. + * + * @param refreshInterval Time before refresh in minutes. + */ + private void setupRefreshTimer(int refreshInterval) { + ScheduledFuture timer = refreshTimer; + + if (timer != null) { + timer.cancel(true); + refreshTimer = null; + } + + if (refreshInterval == 0) { + return; + } + + refreshTimer = scheduler.scheduleWithFixedDelay(() -> { + QbusCommunication comm = getCommunication(); + Integer serverCheck = getServerCheck(); + + if (comm != null) { + if (serverCheck != null) { + if (!comm.communicationActive()) { + // Disconnected from Qbus Server, restart communication + try { + comm.startCommunication(); + } catch (InterruptedException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. InterruptedException: " + msg); + } catch (IOException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. IOException: " + msg); + } + } + } + } + }, refreshInterval, refreshInterval, TimeUnit.MINUTES); + } + + /** + * Disposes the Bridge and stops communication with the Qbus server + */ + @Override + public void dispose() { + ScheduledFuture timer = refreshTimer; + if (timer != null) { + timer.cancel(true); + } + + refreshTimer = null; + + QbusCommunication comm = getCommunication(); + + if (comm != null) { + try { + comm.stopCommunication(); + } catch (IOException e) { + String message = e.toString(); + logger.debug("Error on stopping communication.{} ", message); + } + } + + comm = null; + } + + /** + * Reconnect to Qbus server if controller is offline + */ + public void ctdOffline() { + bridgePending("Waiting for CTD connection"); + } + + /** + * Get BridgeCommunication + * + * @return BridgeCommunication + */ + public @Nullable QbusCommunication getQbusCommunication() { + if (this.qbusComm != null) { + return this.qbusComm; + } else { + return null; + } + } + + /** + * Sets BridgeCommunication + * + * @param BridgeCommunication + */ + void setQbusCommunication(QbusCommunication comm) { + this.qbusComm = comm; + } + + /** + * Gets the status off the Bridge + * + * @return + */ + public ThingStatus getStatus() { + return thing.getStatus(); + } + + /** + * Gets the status off the Bridge + * + * @return + */ + public ThingStatusDetail getStatusDetails() { + ThingStatusInfo status = thing.getStatusInfo(); + ThingStatusDetail detail = status.getStatusDetail(); + return detail; + } + + /** + * Sets the configuration parameters + */ + protected void readConfig() { + bridgeConfig = getConfig().as(QbusConfiguration.class); + } + + /** + * Get the Qbus communication object. + * + * @return Qbus communication object + */ + public @Nullable QbusCommunication getCommunication() { + return this.qbusComm; + } + + /** + * Get the ip address of the Qbus server. + * + * @return the ip address + */ + public @Nullable String getAddress() { + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.addr; + } else { + return null; + } + } + + /** + * Get the listening port of the Qbus server. + * + * @return + */ + public @Nullable Integer getPort() { + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.port; + } else { + return null; + } + } + + /** + * Get the serial nr of the Qbus server. + * + * @return the serial nr of the controller + */ + public @Nullable String getSn() { + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.sn; + } else { + return null; + } + } + + /** + * Get the refresh interval. + * + * @return the refresh interval + */ + public @Nullable Integer getServerCheck() { + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.serverCheck; + } else { + return null; + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } +} diff --git a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/reader/io/serialport/converter/Converter.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java similarity index 54% rename from bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/reader/io/serialport/converter/Converter.java rename to bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java index 3a503aadaf24a..6a68c94b10783 100644 --- a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/reader/io/serialport/converter/Converter.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java @@ -10,19 +10,22 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.teleinfo.internal.reader.io.serialport.converter; + +package org.openhab.binding.qbus.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.teleinfo.internal.reader.io.serialport.ConversionException; /** - * The {@link Converter} interface defines a converter to translate a Teleinfo String value into Java object. + * Class {@link QbusConfiguration} Configuration Class * - * @author Nicolas SIBERIL - Initial contribution + * @author Koen Schockaert - Initial Contribution */ -@NonNullByDefault -public interface Converter { - public @Nullable Object convert(String value) throws ConversionException; +@NonNullByDefault +public class QbusConfiguration { + public @Nullable String addr; + public @Nullable Integer port; + public @Nullable String sn; + public @Nullable Integer serverCheck; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java new file mode 100644 index 0000000000000..6de139b1e6b84 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link qbusHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Koen Schockaert - Initial Contribution + */ + +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") +@NonNullByDefault +public class QbusHandlerFactory extends BaseThingHandlerFactory { + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing); + return handler; + } else if (SCENE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusSceneHandler(thing); + } else if (BISTABIEL_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusBistabielHandler(thing); + } else if (THERMOSTAT_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusThermostatHandler(thing); + } else if (DIMMER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusDimmerHandler(thing); + } else if (CO2_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusCO2Handler(thing); + } else if (ROLLERSHUTTER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } else if (ROLLERSHUTTER_SLATS_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java new file mode 100644 index 0000000000000..76f0d79e9afa5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusBistabielHandler} is responsible for handling the Bistable outputs of Qbus + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusBistabielHandler extends QbusGlobalHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); + + protected @Nullable QbusThingsConfig bistabielConfig = new QbusThingsConfig(); + + private @Nullable Integer bistabielId; + + private @Nullable String sn; + + public QbusBistabielHandler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.bistabielId = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.bistabielId != null) { + controllerComm = getCommunication("Bistabiel", this.bistabielId); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for BISTABIEL no set! " + this.bistabielId); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); + return; + } + + Map bistabielCommLocal = controllerComm.getBistabiel(); + + QbusBistabiel outputLocal = bistabielCommLocal.get(this.bistabielId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize BISTABIEL ID " + this.bistabielId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for BISTABIEL ID " + this.bistabielId); + } + } + }); + } + + /** + * Handle the status update from the bistabiel + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("Bistabiel", this.bistabielId); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); + return; + } else { + Map bistabielComm = qComm.getBistabiel(); + + QbusBistabiel qBistabiel = bistabielComm.get(this.bistabielId); + + if (qBistabiel == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Bistabiel", this.bistabielId); + } + + if (qComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(qBistabiel); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + try { + handleSwitchCommand(qBistabiel, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for bistabiel ID {}. IOException: {}", + this.bistabielId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Switch for bistabiel ID {}. Interruptedexception {}", + this.bistabielId, message); + } + break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } + }); + } + } + } + + /** + * Executes the switch command + * + * @throws IOException + * @throws InterruptedException + */ + private void handleSwitchCommand(QbusBistabiel qBistabiel, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof OnOffType) { + if (command == OnOffType.OFF) { + qBistabiel.execute(0, snr); + } else { + qBistabiel.execute(100, snr); + } + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for BISTABIEL " + this.bistabielId); + } + } + } + + /** + * Method to update state of channel, called from Qbus Bistabiel. + * + * @param qBistabiel + */ + public void handleStateUpdate(QbusBistabiel qBistabiel) { + Integer bistabielState = qBistabiel.getState(); + if (bistabielState != null) { + updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for BISTABIEL " + this.bistabielId); + return; + } + sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + bistabielConfig = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = bistabielConfig; + if (localConfig != null) { + return localConfig.bistabielId; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java new file mode 100644 index 0000000000000..5b191b4e8557e --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_CO2; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCO2; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.library.types.DecimalType; +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.types.Command; + +/** + * The {@link QbusCO2Handler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusCO2Handler extends QbusGlobalHandler { + protected @Nullable QbusThingsConfig config; + + protected @Nullable QbusThingsConfig co2Config = new QbusThingsConfig(); + + private @Nullable Integer co2Id; + + private @Nullable String sn; + + public QbusCO2Handler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.co2Id = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.co2Id != null) { + controllerComm = getCommunication("CO2", this.co2Id); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 no set! " + this.co2Id); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); + return; + } + + Map co2CommLocal = controllerComm.getCo2(); + + QbusCO2 outputLocal = co2CommLocal.get(this.co2Id); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Bridge could not initialize CO2 ID " + this.co2Id); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for CO2 ID " + this.co2Id); + } + } + }); + } + + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("CO2", this.co2Id); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); + return; + } else { + Map co2Comm = qComm.getCo2(); + + QbusCO2 qCo2 = co2Comm.get(this.co2Id); + + if (qCo2 == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "CO2", this.co2Id); + } + + if (qComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(qCo2); + return; + } + + switch (channelUID.getId()) { + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } + }); + } + } + } + + /** + * Method to update state of channel, called from Qbus CO2. + */ + public void handleStateUpdate(QbusCO2 qCo2) { + Integer co2State = qCo2.getState(); + if (co2State != null) { + updateState(CHANNEL_CO2, new DecimalType(co2State)); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for CO2 " + this.co2Id); + return; + } + sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + co2Config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = this.co2Config; + if (localConfig != null) { + return localConfig.co2Id; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java new file mode 100644 index 0000000000000..fc91c6a3f2984 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusDimmer; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusDimmerHandler} is responsible for handling the dimmable outputs of Qbus + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusDimmerHandler extends QbusGlobalHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); + + protected @Nullable QbusThingsConfig dimmerConfig = new QbusThingsConfig(); + + private @Nullable Integer dimmerId; + + private @Nullable String sn; + + public QbusDimmerHandler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.dimmerId = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.dimmerId != null) { + controllerComm = getCommunication("Dimmer", this.dimmerId); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for DIMMER no set! " + this.dimmerId); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); + return; + } + + Map dimmerCommLocal = controllerComm.getDimmer(); + + QbusDimmer outputLocal = dimmerCommLocal.get(this.dimmerId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize DIMMER ID " + this.dimmerId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for DIMMER ID " + this.dimmerId); + } + } + }); + } + + /** + * Handle the status update from the dimmer + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("Dimmer", this.dimmerId); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); + return; + } else { + Map dimmerComm = qComm.getDimmer(); + + QbusDimmer qDimmer = dimmerComm.get(this.dimmerId); + + if (qDimmer == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Dimmer", this.dimmerId); + } + + if (qComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(qDimmer); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + try { + handleSwitchCommand(qDimmer, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for dimmer ID {}. IOException: {}", + this.dimmerId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for dimmer ID {}. Interruptedexception {}", + this.dimmerId, message); + } + break; + + case CHANNEL_BRIGHTNESS: + try { + handleBrightnessCommand(qDimmer, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Brightness for dimmer ID {}. IOException: {}", + this.dimmerId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Brightness for dimmer ID {}. Interruptedexception {}", + this.dimmerId, message); + } + break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } + }); + } + } + } + + /** + * Executes the switch command + * + * @throws IOException + * @throws InterruptedException + */ + private void handleSwitchCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException { + if (command instanceof OnOffType) { + String snr = getSN(); + if (snr != null) { + if (command == OnOffType.OFF) { + qDimmer.execute(0, snr); + } else { + qDimmer.execute(1000, snr); + } + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for DIMMER " + this.dimmerId); + } + } + } + + /** + * Executes the brightness command + * + * @throws IOException + * @throws InterruptedException + */ + private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException { + String snr = getSN(); + + if (snr == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for DIMMER " + this.dimmerId); + return; + } else { + if (command instanceof OnOffType) { + if (command == OnOffType.OFF) { + qDimmer.execute(0, snr); + } else { + qDimmer.execute(100, snr); + } + } else if (command instanceof IncreaseDecreaseType) { + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qDimmer.getState(); + Integer newValue; + Integer sendvalue; + if (currentValue != null) { + if (command == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendvalue = newValue > 100 ? 100 : newValue; + qDimmer.execute(sendvalue, snr); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendvalue = newValue < 0 ? 0 : newValue; + qDimmer.execute(sendvalue, snr); + } + } + } else if (command instanceof PercentType) { + int percentToInt = ((PercentType) command).intValue(); + if (command == PercentType.ZERO) { + qDimmer.execute(0, snr); + } else { + qDimmer.execute(percentToInt, snr); + } + } + } + } + + /** + * Method to update state of channel, called from Qbus Dimmer. + * + * @param qDimmer + */ + public void handleStateUpdate(QbusDimmer qDimmer) { + Integer dimmerState = qDimmer.getState(); + if (dimmerState != null) { + updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial number + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for DIMMER " + this.dimmerId); + return; + } + this.sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + dimmerConfig = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = dimmerConfig; + if (localConfig != null) { + return localConfig.dimmerId; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java new file mode 100644 index 0000000000000..068813c899b5d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; + +/** + * The {@link QbusGlobalHandler} is used in other handlers, to share the functions. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public abstract class QbusGlobalHandler extends BaseThingHandler { + + public QbusGlobalHandler(Thing thing) { + super(thing); + } + + /** + * Get Bridge communication + * + * @param type + * @param globalId + * @return + */ + public @Nullable QbusCommunication getCommunication(String type, @Nullable Integer globalId) { + QbusBridgeHandler qBridgeHandler = null; + if (globalId != null) { + qBridgeHandler = getBridgeHandler(type, globalId); + } + + if (qBridgeHandler == null) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "No bridge handler initialized for " + type + " with id " + globalId + "."); + return null; + } + QbusCommunication qComm = qBridgeHandler.getCommunication(); + return qComm; + } + + /** + * Get the Bridge handler + * + * @param type + * @param globalId + * @return + */ + public @Nullable QbusBridgeHandler getBridgeHandler(String type, @Nullable Integer globalId) { + Bridge qBridge = getBridge(); + if (qBridge == null) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "No bridge initialized for " + type + " with ID " + globalId); + return null; + } + QbusBridgeHandler qBridgeHandler = (QbusBridgeHandler) qBridge.getHandler(); + return qBridgeHandler; + } + + /** + * + * @param qComm + * @param type + * @param globalId + */ + public void restartCommunication(QbusCommunication qComm, String type, @Nullable Integer globalId) { + try { + qComm.restartCommunication(); + } catch (InterruptedException e) { + String message = e.toString(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } catch (IOException e) { + String message = e.toString(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } + + QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId); + + if (qBridgeHandler != null && qComm.communicationActive()) { + qBridgeHandler.bridgeOnline(); + } else { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); + } + } + + /** + * Put thing offline + * + * @param message + */ + public void thingOffline(ThingStatusDetail detail, String message) { + updateStatus(ThingStatus.OFFLINE, detail, message); + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java new file mode 100644 index 0000000000000..0a1a532f11347 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -0,0 +1,333 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.types.UpDownType.DOWN; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusRol; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.UpDownType; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusRolHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusRolHandler extends QbusGlobalHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); + + protected @Nullable QbusThingsConfig rolConfig = new QbusThingsConfig(); + + private @Nullable Integer rolId; + + private @Nullable String sn; + + public QbusRolHandler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.rolId = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.rolId != null) { + controllerComm = getCommunication("Screen/Store", this.rolId); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for Screen/Store no set! " + this.rolId); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for Screen/Store not known in controller " + this.rolId); + return; + } + + Map rolCommLocal = controllerComm.getRol(); + + QbusRol outputLocal = rolCommLocal.get(this.rolId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize Screen/Store ID " + this.rolId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for SCREEN/STORE ID " + this.rolId); + } + } + }); + } + + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("Screen/Store", this.rolId); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId); + return; + } else { + Map rolComm = qComm.getRol(); + + QbusRol qRol = rolComm.get(this.rolId); + + if (qRol == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Screen/Store", this.rolId); + } + + if (qComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(qRol); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_ROLLERSHUTTER: + try { + handleScreenposCommand(qRol, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Rollershutter for screen ID {}. IOException: {}", + this.rolId, message); + } catch (InterruptedException e) { + String message = e.toString(); + logger.warn( + "Error on executing Rollershutter for screen ID {}. Interruptedexception {}", + this.rolId, message); + } + break; + + case CHANNEL_SLATS: + try { + handleSlatsposCommand(qRol, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Slats for screen ID {}. IOException: {}", + this.rolId, message); + } catch (InterruptedException e) { + String message = e.toString(); + logger.warn("Error on executing Slats for screen ID {}. Interruptedexception {}", + this.rolId, message); + } + break; + } + } + }); + } + } + } + + /** + * Executes the command for screen up/down position + * + * @throws IOException + * @throws InterruptedException + */ + private void handleScreenposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof UpDownType) { + UpDownType upDown = (UpDownType) command; + if (upDown == DOWN) { + qRol.execute(0, snr); + } else { + qRol.execute(100, snr); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType inc = (IncreaseDecreaseType) command; + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qRol.getState(); + int newValue; + int sendValue; + if (currentValue != null) { + if (inc == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + qRol.execute(sendValue, snr); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + qRol.execute(sendValue, snr); + } + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + int pp = p.intValue(); + if (p == PercentType.ZERO) { + qRol.execute(0, snr); + } else { + qRol.execute(pp, snr); + } + } + } + } + + /** + * Executes the command for screen slats position + * + * @throws IOException + * @throws InterruptedException + */ + private void handleSlatsposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof UpDownType) { + if (command == DOWN) { + qRol.executeSlats(0, snr); + } else { + qRol.executeSlats(100, snr); + } + } else if (command instanceof IncreaseDecreaseType) { + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qRol.getState(); + int newValue; + int sendValue; + if (currentValue != null) { + if (command == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + qRol.executeSlats(sendValue, snr); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + qRol.executeSlats(sendValue, snr); + } + } + } else if (command instanceof PercentType) { + int percentToInt = ((PercentType) command).intValue(); + if (command == PercentType.ZERO) { + qRol.executeSlats(0, snr); + } else { + qRol.executeSlats(percentToInt, snr); + } + } + } + } + + /** + * Method to update state of channel, called from Qbus Screen/Store. + */ + public void handleStateUpdate(QbusRol qRol) { + Integer rolState = qRol.getState(); + Integer slatState = qRol.getStateSlats(); + + if (rolState != null) { + updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); + } + if (slatState != null) { + updateState(CHANNEL_SLATS, new PercentType(slatState)); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for ROLLERSHUTTER/SCREEN " + this.rolId); + return; + } + sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + rolConfig = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = rolConfig; + if (localConfig != null) { + return localConfig.rolId; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java new file mode 100644 index 0000000000000..fc0381fd4af06 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SCENE; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusScene; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusSceneHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusSceneHandler extends QbusGlobalHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); + + protected @Nullable QbusThingsConfig sceneConfig = new QbusThingsConfig(); + + private @Nullable Integer sceneId; + + private @Nullable String sn; + + public QbusSceneHandler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.sceneId = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.sceneId != null) { + controllerComm = getCommunication("Scene", this.sceneId); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE no set! " + this.sceneId); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for SCENE not known in controller " + this.sceneId); + return; + } + + Map sceneCommLocal = controllerComm.getScene(); + + QbusScene outputLocal = sceneCommLocal.get(this.sceneId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize SCENE ID " + this.sceneId); + return; + } + + outputLocal.setThingHandler(this); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId); + + if ((qBridgeHandler != null) && (qBridgeHandler.getStatus() == ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); + } else { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Bridge offline for SCENE ID " + this.sceneId); + } + }); + } + + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("Scene", this.sceneId); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE not known in controller " + this.sceneId); + return; + } else { + Map sceneComm = qComm.getScene(); + QbusScene qScene = sceneComm.get(this.sceneId); + + if (qScene == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for SCENE not known in controller " + this.sceneId); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Scene", this.sceneId); + } + + if (qComm.communicationActive()) { + switch (channelUID.getId()) { + case CHANNEL_SCENE: + try { + handleSwitchCommand(qScene, channelUID, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Scene for scene ID {}. IOException: {}", + this.sceneId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn("Error on executing Scene for scene ID {}. Interruptedexception {}", + this.sceneId, message); + } + break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } + }); + } + } + } + + /** + * Executes the scene command + * + * @throws IOException + * @throws InterruptedException + */ + void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof OnOffType) { + if (command == OnOffType.OFF) { + qScene.execute(0, snr); + } else { + qScene.execute(100, snr); + } + } + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for SCENE " + this.sceneId); + return; + } + sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + sceneConfig = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = sceneConfig; + if (localConfig != null) { + return localConfig.sceneId; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java new file mode 100644 index 0000000000000..cbef42e775eb3 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.unit.SIUnits.CELSIUS; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusThermostat; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusThermostatHandler} is responsible for handling the Thermostat outputs of Qbus + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusThermostatHandler extends QbusGlobalHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); + + protected @Nullable QbusThingsConfig thermostatConfig = new QbusThingsConfig(); + + private @Nullable Integer thermostatId; + + private @Nullable String sn; + + public QbusThermostatHandler(Thing thing) { + super(thing); + } + + /** + * Main initialization + */ + @Override + public void initialize() { + readConfig(); + + this.thermostatId = getId(); + + setSN(); + + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.thermostatId != null) { + controllerComm = getCommunication("Thermostat", this.thermostatId); + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for THERMOSTAT no set! " + this.thermostatId); + return; + } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); + return; + } + + Map thermostatlCommLocal = controllerComm.getThermostat(); + + QbusThermostat outputLocal = thermostatlCommLocal.get(this.thermostatId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize THERMOSTAT ID " + this.thermostatId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", this.thermostatId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for THERMOSTAT ID " + this.thermostatId); + } + } + }); + } + + /** + * Handle the status update from the thermostat + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication qComm = getCommunication("Thermostat", this.thermostatId); + + if (qComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); + return; + } else { + Map thermostatComm = qComm.getThermostat(); + + QbusThermostat qThermostat = thermostatComm.get(this.thermostatId); + + if (qThermostat == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); + return; + } else { + scheduler.submit(() -> { + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Thermostat", this.thermostatId); + } + + if (qComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(qThermostat); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_MODE: + try { + handleModeCommand(qThermostat, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Mode for thermostat ID {}. IOException: {} ", + this.thermostatId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Mode for thermostat ID {}. Interruptedexception {} ", + this.thermostatId, message); + } + break; + + case CHANNEL_SETPOINT: + try { + handleSetpointCommand(qThermostat, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Setpoint for thermostat ID {}. IOException: {} ", + this.thermostatId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Setpoint for thermostat ID {}. Interruptedexception {} ", + this.thermostatId, message); + } + break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } + }); + } + } + } + + /** + * Executes the Mode command + * + * @param qThermostat + * @param command + * @param snr + * @throws InterruptedException + * @throws IOException + */ + private void handleModeCommand(QbusThermostat qThermostat, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof DecimalType) { + int mode = ((DecimalType) command).intValue(); + qThermostat.executeMode(mode, snr); + } + } + } + + /** + * Executes the Setpoint command + * + * @param qThermostat + * @param command + * @param snr + * @throws InterruptedException + * @throws IOException + */ + private void handleSetpointCommand(QbusThermostat qThermostat, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof QuantityType) { + QuantityType s = (QuantityType) command; + double sp = s.doubleValue(); + QuantityType spCelcius = s.toUnit(CELSIUS); + + if (spCelcius != null) { + qThermostat.executeSetpoint(sp, snr); + } else { + logger.warn("Could not set setpoint for thermostat (conversion failed) {}", this.thermostatId); + } + } + } + } + + /** + * Method to update state of all channels, called from Qbus thermostat. + * + * @param qThermostat + */ + public void handleStateUpdate(QbusThermostat qThermostat) { + Double measured = qThermostat.getMeasured(); + if (measured != null) { + updateState(CHANNEL_MEASURED, new QuantityType<>(measured, CELSIUS)); + } + + Double setpoint = qThermostat.getSetpoint(); + if (setpoint != null) { + updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint, CELSIUS)); + } + + Integer mode = qThermostat.getMode(); + if (mode != null) { + updateState(CHANNEL_MODE, new DecimalType(mode)); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostsat", this.thermostatId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for THERMOSTAT " + this.thermostatId); + return; + } + sn = qBridgeHandler.getSn(); + } + + /** + * Read the configuration + */ + protected synchronized void readConfig() { + thermostatConfig = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return outputId + */ + public @Nullable Integer getId() { + QbusThingsConfig localConfig = thermostatConfig; + if (localConfig != null) { + return localConfig.thermostatId; + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java new file mode 100644 index 0000000000000..56de0cb64d8f4 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link QbusThingsConfig} is responible for handling configurations for all things + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusThingsConfig { + public @Nullable Integer bistabielId; + public @Nullable Integer dimmerId; + public @Nullable Integer co2Id; + public @Nullable Integer rolId; + public @Nullable Integer sceneId; + public @Nullable Integer thermostatId; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java new file mode 100644 index 0000000000000..a55ec87431c69 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; + +/** + * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusBistabiel { + + private @Nullable QbusCommunication qComm; + + private Integer id; + + private @Nullable Integer state; + + private @Nullable QbusBistabielHandler thingHandler; + + QbusBistabiel(Integer id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the bistable output receives an update from the Qbus client. + * + * @param handler + */ + public void setThingHandler(QbusBistabielHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm BISTABIEL of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus client. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Update the value of the Bistabiel. + * + * @param state + */ + void updateState(@Nullable Integer state) { + this.state = state; + QbusBistabielHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Get the value of the Bistabiel. + * + * @return + */ + public @Nullable Integer getState() { + return this.state; + } + + /** + * Sends Bistabiel state to Qbus. + * + * @param value + * @param sn + * @throws InterruptedException + * @throws IOException + */ + public void execute(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); + QbusCommunication comm = this.qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java new file mode 100644 index 0000000000000..88d0f404925c8 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; + +/** + * The {@link QbusCO2} class represents the action Qbus CO2 output. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusCO2 { + + private @Nullable Integer state; + + private @Nullable QbusCO2Handler thingHandler; + + /** + * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the CO2 output receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusCO2Handler handler) { + this.thingHandler = handler; + } + + /** + * Get state of CO2. + * + * @return CO2 state + */ + public @Nullable Integer getState() { + return this.state; + } + + /** + * Update the value of the CO2. + * + * @param CO2 value + */ + void updateState(@Nullable Integer state) { + this.state = state; + QbusCO2Handler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java new file mode 100644 index 0000000000000..e07032d2ebd6a --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -0,0 +1,796 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +/** + * The {@link QbusCommunication} class is able to do the following tasks with Qbus + * CTD controllers: + *
    + *
  • Start and stop TCP socket connection with Qbus Server. + *
  • Read all the outputs and their status from the Qbus Controller. + *
  • Execute Qbus commands. + *
  • Listen to events from Qbus. + *
+ * + * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusCommunication extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); + + private @Nullable Socket qSocket; + private @Nullable PrintWriter qOut; + private @Nullable BufferedReader qIn; + + private boolean listenerStopped; + private boolean qbusListenerRunning; + + private Gson gsonOut = new Gson(); + private Gson gsonIn; + + private @Nullable String ctd; + private boolean ctdConnected; + + private List> outputs = new ArrayList<>(); + private final Map bistabiel = new HashMap<>(); + private final Map scene = new HashMap<>(); + private final Map dimmer = new HashMap<>(); + private final Map rol = new HashMap<>(); + private final Map thermostat = new HashMap<>(); + private final Map co2 = new HashMap<>(); + + private final ExecutorService threadExecutor = Executors + .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true)); + + private @Nullable QbusBridgeHandler bridgeCallBack; + + public QbusCommunication(Thing thing) { + super(thing); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); + gsonIn = gsonBuilder.create(); + } + + /** + * Starts main communication thread. + *
    + *
  • Connect to Qbus server + *
  • Requests outputs + *
  • Start listener + *
+ * + * @throws IOException + * @throws InterruptedException + */ + public synchronized void startCommunication() throws IOException, InterruptedException { + QbusBridgeHandler handler = bridgeCallBack; + ctdConnected = false; + + if (qbusListenerRunning) { + throw new IOException("Previous listening thread is still active."); + } + + if (handler == null) { + throw new IOException("No Bridge handler initialised."); + } + + InetAddress addr = InetAddress.getByName(handler.getAddress()); + Integer port = handler.getPort(); + + if (port != null) { + Socket socket = new Socket(addr, port); + qSocket = socket; + qOut = new PrintWriter(socket.getOutputStream(), true); + qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + } else { + return; + } + + setSN(); + getSN(); + + // Connect to Qbus server + connect(); + + // Then start thread to listen to incoming updates from Qbus. + threadExecutor.execute(() -> { + try { + qbusListener(); + } catch (IOException e) { + String msg = e.getMessage(); + logger.warn("Could not start listening thread, IOException: {}", msg); + } catch (InterruptedException e) { + String msg = e.getMessage(); + logger.warn("Could not start listening thread, InterruptedException: {}", msg); + } + }); + + if (!ctdConnected) { + handler.bridgePending("Waiting for CTD to come online..."); + } + } + + /** + * Cleanup socket when the communication with Qbus Server is closed. + * + * @throws IOException + * + */ + public synchronized void stopCommunication() throws IOException { + listenerStopped = true; + + Socket socket = qSocket; + + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + // ignore IO Error when trying to close the socket if the intention is to close it anyway + } + } + + BufferedReader reader = this.qIn; + if (reader != null) { + reader.close(); + } + + PrintWriter writer = this.qOut; + if (writer != null) { + writer.close(); + } + + qSocket = null; + qbusListenerRunning = false; + ctdConnected = false; + + logger.trace("Communication stopped from thread {}", Thread.currentThread().getId()); + } + + /** + * Close and restart communication with Qbus Server. + * + * @throws InterruptedException + * @throws IOException + */ + public synchronized void restartCommunication() throws InterruptedException, IOException { + stopCommunication(); + + startCommunication(); + } + + /** + * Thread that handles incoming messages from Qbus client. + *

+ * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class + * and interprets all incomming json messages. It triggers state updates for active channels linked to the + * Qbus outputs. It is started after initialization of the communication. + * + * @return + * @throws IOException + * @throws InterruptedException + * + * + */ + private void qbusListener() throws IOException, InterruptedException { + String qMessage; + + listenerStopped = false; + qbusListenerRunning = true; + + BufferedReader reader = this.qIn; + + if (reader == null) { + throw new IOException("Bufferreader for incoming messages not initialized."); + } + + try { + while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) { + readMessage(qMessage); + + } + } catch (IOException e) { + if (!listenerStopped) { + qbusListenerRunning = false; + // the IO has stopped working, so we need to close cleanly and try to restart + restartCommunication(); + return; + } + } finally { + qbusListenerRunning = false; + } + + if (!listenerStopped) { + qbusListenerRunning = false; + + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + ctdConnected = false; + handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server"); + } + } + + qbusListenerRunning = false; + logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId()); + }; + + /** + * Called by other methods to send json data to Qbus. + * + * @param qMessage + * @throws InterruptedException + * @throws IOException + */ + synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException { + PrintWriter writer = qOut; + String json = gsonOut.toJson(qMessage); + + if (writer != null) { + writer.println(json); + // Delay after sending data to improve scene execution + TimeUnit.MILLISECONDS.sleep(250); + } + + if ((writer == null) || (writer.checkError())) { + logger.warn("Error sending message, trying to restart communication"); + + restartCommunication(); + + // retry sending after restart + writer = qOut; + if (writer != null) { + writer.println(json); + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Error resending message"); + + } + } + } + + /** + * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. + *

    + *
  • Get request & update states for Bistabiel/Timers/Intervals/Mono outputs + *
  • Get request & update states for the Scenes + *
  • Get request & update states for Dimmers 1T and 2T + *
  • Get request & update states for Shutters + *
  • Get request & update states for Thermostats + *
  • Get request & update states for CO2 + *
+ * + * @param qMessage message read from Qbus. + * @throws InterruptedException + * @throws IOException + * + */ + private void readMessage(String qMessage) { + String sn = null; + String cmd = ""; + String ctd = null; + Integer id = null; + Integer state = null; + Integer mode = null; + Double setpoint = null; + Double measured = null; + Integer slats = null; + + QbusMessageBase qMessageGson; + try { + qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); + + if (qMessageGson != null) { + ctd = qMessageGson.getSn(); + cmd = qMessageGson.getCmd(); + id = qMessageGson.getId(); + state = qMessageGson.getState(); + mode = qMessageGson.getMode(); + setpoint = qMessageGson.getSetPoint(); + measured = qMessageGson.getMeasured(); + slats = qMessageGson.getSlatState(); + } + } catch (JsonParseException e) { + String msg = e.getMessage(); + logger.trace("Not acted on unsupported json {} : {}", qMessage, msg); + return; + } + + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + sn = handler.getSn(); + } + + if (sn != null && ctd != null) { + try { + if (sn.equals(ctd) && qMessageGson != null) { // Check if commands are for this Bridge + // Handle all outputs from Qbus + if ("returnOutputs".equals(cmd)) { + outputs = ((QbusMessageListMap) qMessageGson).getOutputs(); + + for (Map ctdOutputs : outputs) { + + String ctdType = ctdOutputs.get("type"); + String ctdIdStr = ctdOutputs.get("id"); + Integer ctdId = null; + + if (ctdIdStr != null) { + ctdId = Integer.parseInt(ctdIdStr); + } else { + return; + } + + if (ctdType != null) { + String ctdState = ctdOutputs.get("state"); + String ctdMmode = ctdOutputs.get("regime"); + String ctdSetpoint = ctdOutputs.get("setpoint"); + String ctdMeasured = ctdOutputs.get("measured"); + String ctdSlats = ctdOutputs.get("slats"); + + Integer ctdStateI = null; + if (ctdState != null) { + ctdStateI = Integer.parseInt(ctdState); + } + + Integer ctdSlatsI = null; + if (ctdSlats != null) { + ctdSlatsI = Integer.parseInt(ctdSlats); + } + + Integer ctdMmodeI = null; + if (ctdMmode != null) { + ctdMmodeI = Integer.parseInt(ctdMmode); + } + + Double ctdSetpointD = null; + if (ctdSetpoint != null) { + ctdSetpointD = Double.parseDouble(ctdSetpoint); + } + + Double ctdMeasuredD = null; + if (ctdMeasured != null) { + ctdMeasuredD = Double.parseDouble(ctdMeasured); + } + + if (ctdState != null) { + if (ctdType.equals("bistabiel")) { + QbusBistabiel output = new QbusBistabiel(ctdId); + if (!bistabiel.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + bistabiel.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("dimmer")) { + QbusDimmer output = new QbusDimmer(ctdId); + if (!dimmer.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + dimmer.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("CO2")) { + QbusCO2 output = new QbusCO2(); + if (!co2.containsKey(ctdId)) { + output.updateState(ctdStateI); + co2.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("scene")) { + QbusScene output = new QbusScene(ctdId); + if (!scene.containsKey(ctdId)) { + output.setQComm(this); + scene.put(ctdId, output); + } + } else if (ctdType.equals("rol")) { + QbusRol output = new QbusRol(ctdId); + if (!rol.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + if (ctdSlats != null) { + output.updateSlats(ctdSlatsI); + } + rol.put(ctdId, output); + } else { + output.updateState(ctdStateI); + if (ctdSlats != null) { + output.updateSlats(ctdSlatsI); + } + } + } + } else if (ctdMeasuredD != null && ctdSetpointD != null && ctdMmodeI != null) { + if (ctdType.equals("thermostat")) { + QbusThermostat output = new QbusThermostat(ctdId); + if (!thermostat.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI); + thermostat.put(ctdId, output); + } else { + output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI); + } + } + } + } + } + // Handle update commands from Qbus + } else if ("updateBistabiel".equals(cmd)) { + if (id != null && state != null) { + updateBistabiel(id, state); + } + } else if ("updateDimmer".equals(cmd)) { + if (id != null && state != null) { + updateDimmer(id, state); + } + } else if ("updateDimmer".equals(cmd)) { + if (id != null && state != null) { + updateDimmer(id, state); + } + } else if ("updateCo2".equals(cmd)) { + if (id != null && state != null) { + updateCO2(id, state); + } + } else if ("updateThermostat".equals(cmd)) { + if (id != null && measured != null && setpoint != null && mode != null) { + updateThermostat(id, mode, setpoint, measured); + } + } else if ("updateRol02p".equals(cmd)) { + if (id != null && state != null) { + updateRol(id, state); + } + } else if ("updateRol02pSlat".equals(cmd)) { + if (id != null && state != null && slats != null) { + updateRolSlats(id, state, slats); + } + // Incomming commands from Qbus server to verify the client connection + } else if ("noconnection".equals(cmd)) { + eventDisconnect(); + } else if ("connected".equals(cmd)) { + // threadExecutor.execute(() -> { + try { + requestOutputs(); + } catch (InterruptedException e) { + String msg = e.getMessage(); + logger.warn("Could not request outputs. InterruptedException: {}", msg); + } catch (IOException e) { + String msg = e.getMessage(); + logger.warn("Could not request outputs. IOException: {}", msg); + } + } + } + } catch (JsonParseException e) { + String msg = e.getMessage(); + logger.warn("Not acted on unsupported json {}, {}", qMessage, msg); + } + } + } + + /** + * Initialize the communication object + */ + @Override + public void initialize() { + } + + /** + * Initial connection to Qbus Server to open a communication channel + * + * @throws InterruptedException + * @throws IOException + */ + private void connect() throws InterruptedException, IOException { + String snr = getSN(); + + if (snr != null) { + QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB"); + + sendMessage(qCmd); + + BufferedReader reader = qIn; + + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + + } else { + QbusBridgeHandler handler = bridgeCallBack; + if (handler != null) { + handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined"); + } + } + } + + /** + * Send a request for all available outputs and initializes them via readMessage + * + * @throws InterruptedException + * @throws IOException + */ + private void requestOutputs() throws InterruptedException, IOException { + String snr = getSN(); + QbusBridgeHandler handler = bridgeCallBack; + + if (snr != null) { + QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all"); + sendMessage(qCmd); + + BufferedReader reader = qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + ctdConnected = true; + + if (handler != null) { + handler.bridgeOnline(); + } + + } else { + if (handler != null) { + handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined"); + } + } + } + + /** + * Event on incoming Bistabiel/Timer/Mono/Interval updates + * + * @param id + * @param state + */ + private void updateBistabiel(Integer id, Integer state) { + QbusBistabiel qBistabiel = this.bistabiel.get(id); + + if (qBistabiel != null) { + qBistabiel.updateState(state); + } else { + logger.trace("Bistabiel in controller not known {}", id); + } + } + + /** + * Event on incoming Dimmer updates + * + * @param id + * @param state + */ + private void updateDimmer(Integer id, Integer state) { + QbusDimmer qDimmer = this.dimmer.get(id); + + if (qDimmer != null) { + qDimmer.updateState(state); + } else { + logger.trace("Dimmer in controller not known {}", id); + } + } + + /** + * Event on incoming thermostat updates + * + * @param id + * @param mode + * @param sp + * @param ct + */ + private void updateThermostat(Integer id, int mode, double sp, double ct) { + QbusThermostat qThermostat = this.thermostat.get(id); + + if (qThermostat != null) { + qThermostat.updateState(ct, sp, mode); + } else { + logger.trace("Thermostat in controller not known {}", id); + } + } + + /** + * Event on incoming CO2 updates + * + * @param id + * @param state + */ + private void updateCO2(Integer id, Integer state) { + QbusCO2 qCO2 = this.co2.get(id); + + if (qCO2 != null) { + qCO2.updateState(state); + } else { + logger.trace("CO2 in controller not known {}", id); + } + } + + /** + * Event on incoming screen updates + * + * @param id + * @param state + */ + private void updateRol(Integer id, Integer state) { + QbusRol qRol = this.rol.get(id); + + if (qRol != null) { + qRol.updateState(state); + } else { + logger.trace("ROL02P in controller not known {}", id); + } + } + + /** + * Event on incoming screen with slats updates + * + * @param id + * @param state + * @param slats + */ + private void updateRolSlats(Integer id, Integer state, Integer slats) { + QbusRol qRol = this.rol.get(id); + + if (qRol != null) { + qRol.updateState(state); + qRol.updateSlats(slats); + } else { + logger.trace("ROL02P with slats in controller not known {}", id); + } + } + + /** + * Put Bridge offline when there is no connection from the QbusClient + * + */ + private void eventDisconnect() { + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + handler.bridgePending("Waiting for CTD connection"); + } + } + + /** + * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller. + * + * @return + */ + public Map getBistabiel() { + return this.bistabiel; + } + + /** + * Return all Scenes in the Qbus Controller + * + * @return + */ + public Map getScene() { + return this.scene; + } + + /** + * Return all Dimmers outputs in the Qbus Controller. + * + * @return + */ + public Map getDimmer() { + return this.dimmer; + } + + /** + * Return all rollershutter/screen outputs in the Qbus Controller. + * + * @return + */ + public Map getRol() { + return this.rol; + } + + /** + * Return all Thermostats outputs in the Qbus Controller. + * + * @return + */ + public Map getThermostat() { + return this.thermostat; + } + + /** + * Return all CO2 outputs in the Qbus Controller. + * + * @return + */ + public Map getCo2() { + return this.co2; + } + + /** + * Method to check if communication with Qbus Server is active + * + * @return True if active + */ + public boolean communicationActive() { + return qSocket != null; + } + + /** + * Method to check if communication with Qbus Client is active + * + * @return True if active + */ + public boolean clientConnected() { + return ctdConnected; + } + + /** + * @param bridgeCallBack the bridgeCallBack to set + */ + public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { + this.bridgeCallBack = bridgeCallBack; + } + + /** + * Get the serial number of the CTD as configured in the Bridge. + * + * @return serial number of controller + */ + public @Nullable String getSN() { + return this.ctd; + } + + /** + * Sets the serial number of the CTD, as configured in the Bridge. + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = bridgeCallBack; + + if (qBridgeHandler != null) { + this.ctd = qBridgeHandler.getSn(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java new file mode 100644 index 0000000000000..a7d61973775a4 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; + +/** + * The {@link QbusDimmer} class represents the action Qbus Dimmer output. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusDimmer { + + private @Nullable QbusCommunication qComm; + + private Integer id; + + private @Nullable Integer state; + + private @Nullable QbusDimmerHandler thingHandler; + + QbusDimmer(Integer id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the dimmer receives an update from the Qbus client. + * + * @param handler + */ + public void setThingHandler(QbusDimmerHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm Dimmer of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus client. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Update the value of the dimmer + * + * @param state + */ + public void updateState(@Nullable Integer state) { + this.state = state; + QbusDimmerHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Get the state of dimmer. + * + * @return dimmer state + */ + public @Nullable Integer getState() { + return this.state; + } + + /** + * Sets the state of Dimmer. + * + * @param dimmer state + */ + void setState(int state) { + this.state = state; + QbusDimmerHandler handler = thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Sends the dimmer state to Qbus. + * + * @throws IOException + * @throws InterruptedException + */ + public void execute(int percent, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); + QbusCommunication comm = this.qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java new file mode 100644 index 0000000000000..5c75d1cb7b196 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server. + * This class only contains the common base fields required for the deserializer + * {@link QbusMessageDeserializer} to select the specific formats implemented in {@link QbusMessageMap}, + * {@link QbusMessageListMap}, {@link QbusMessageCmd}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +abstract class QbusMessageBase { + + private @Nullable String ctd; + protected @Nullable String cmd; + protected @Nullable String type; + protected @Nullable Integer id; + protected @Nullable Integer state; + protected @Nullable Integer mode; + protected @Nullable Double setpoint; + protected @Nullable Double measured; + protected @Nullable Integer slatState; + + @Nullable + String getSn() { + return this.ctd; + } + + void setSn(String ctd) { + this.ctd = ctd; + } + + @Nullable + String getCmd() { + return this.cmd; + } + + void setCmd(String cmd) { + this.cmd = cmd; + } + + @Nullable + public Integer getId() { + return id; + } + + public void setType(String type) { + this.type = type; + } + + @Nullable + public String getType() { + return type; + } + + public void setId(Integer id) { + this.id = id; + } + + @Nullable + public Integer getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + @Nullable + public Integer getMode() { + return mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + @Nullable + public Double getSetPoint() { + return setpoint; + } + + public void setSetPoint(Double setpoint) { + this.setpoint = setpoint; + } + + @Nullable + public Double getMeasured() { + return measured; + } + + public void setMeasured(Double measured) { + this.measured = measured; + } + + @Nullable + public Integer getSlatState() { + return slatState; + } + + public void setSlatState(int slatState) { + this.slatState = slatState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java new file mode 100644 index 0000000000000..f7e7d031e1684 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends + * {@link QbusMessageBase}. + *

+ * Example: {"cmd":"executebistabiel","id":1,"value1":0} + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +class QbusMessageCmd extends QbusMessageBase { + + QbusMessageCmd(String CTD) { + super.setSn(CTD); + } + + QbusMessageCmd(String CTD, String cmd) { + this(CTD); + this.cmd = cmd; + } + + QbusMessageCmd withId(Integer id) { + this.setId(id); + return this; + } + + QbusMessageCmd withState(int state) { + this.setState(state); + return this; + } + + QbusMessageCmd withMode(int mode) { + this.setMode(mode); + return this; + } + + QbusMessageCmd withSetPoint(Double setpoint) { + this.setSetPoint(setpoint); + return this; + } + + QbusMessageCmd withSlatState(int slatState) { + this.setSlatState(slatState); + return this; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java new file mode 100644 index 0000000000000..2d06f72cb39c0 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Class {@link QbusMessageDeserializer} deserializes all json messages from Qbus. Various json + * message formats are supported. The format is selected based on the content of the cmd and event json objects. + * + * @author Koen Schockaert - Initial Contribution + * + */ + +@NonNullByDefault +class QbusMessageDeserializer implements JsonDeserializer { + + @Override + public @Nullable QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, + final JsonDeserializationContext context) throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); + + String ctd = null; + String cmd = null; + Integer id = null; + Integer state = null; + Integer mode = null; + Double measured = null; + Double setpoint = null; + Integer slats = null; + + QbusMessageBase message = null; + + JsonElement jsonOutputs = null; + try { + if (jsonObject.has("CTD")) { + ctd = jsonObject.get("CTD").getAsString(); + } + + if (jsonObject.has("cmd")) { + cmd = jsonObject.get("cmd").getAsString(); + } + + if (jsonObject.has("id")) { + id = jsonObject.get("id").getAsInt(); + } + + if (jsonObject.has("state")) { + state = jsonObject.get("state").getAsInt(); + } + + if (jsonObject.has("mode")) { + mode = jsonObject.get("mode").getAsInt(); + } + + if (jsonObject.has("measured")) { + measured = jsonObject.get("measured").getAsDouble(); + } + + if (jsonObject.has("setpoint")) { + setpoint = jsonObject.get("setpoint").getAsDouble(); + } + + if (jsonObject.has("slats")) { + slats = jsonObject.get("slats").getAsInt(); + } + + if (jsonObject.has("outputs")) { + jsonOutputs = jsonObject.get("outputs"); + + } + + if (ctd != null && cmd != null) { + if (jsonOutputs != null) { + if (jsonOutputs.isJsonArray()) { + JsonArray jsonOutputsArray = jsonOutputs.getAsJsonArray(); + message = new QbusMessageListMap(); + message.setCmd(cmd); + message.setSn(ctd); + + List> outputsList = new ArrayList<>(); + for (int i = 0; i < jsonOutputsArray.size(); i++) { + JsonObject jsonOutputsObject = jsonOutputsArray.get(i).getAsJsonObject(); + + Map outputs = new HashMap<>(); + for (Entry entry : jsonOutputsObject.entrySet()) { + outputs.put(entry.getKey(), entry.getValue().getAsString()); + } + outputsList.add(outputs); + } + ((QbusMessageListMap) message).setOutputs(outputsList); + } + + } else { + message = new QbusMessageMap(); + + message.setCmd(cmd); + message.setSn(ctd); + + if (id != null) { + message.setId(id); + } + + if (state != null) { + message.setState(state); + } + + if (slats != null) { + message.setSlatState(slats); + } + + if (mode != null) { + message.setMode(mode); + } + + if (measured != null) { + message.setMeasured(measured); + } + + if (setpoint != null) { + message.setSetPoint(setpoint); + } + + } + } + return message; + } catch (IllegalStateException e) { + String mess = e.getMessage(); + throw new JsonParseException("Unexpected Json format " + mess + " for " + jsonObject.toString()); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java new file mode 100644 index 0000000000000..01fefbd74ba87 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +class QbusMessageListMap extends QbusMessageBase { + + private List> outputs = new ArrayList<>(); + + List> getOutputs() { + return this.outputs; + } + + void setOutputs(List> outputs) { + this.outputs = outputs; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java new file mode 100644 index 0000000000000..097d0841429ca --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is a simple json string. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +class QbusMessageMap extends QbusMessageBase { + + private Map outputs = new HashMap<>(); + + Map getData() { + return this.outputs; + } + + void setOutputs(Map outputs) { + this.outputs = outputs; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java new file mode 100644 index 0000000000000..4358e7e27176b --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; + +/** + * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusRol { + + private @Nullable QbusCommunication qComm; + + private Integer id; + + private @Nullable Integer state; + + private @Nullable Integer slats; + + private @Nullable QbusRolHandler thingHandler; + + QbusRol(Integer id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is + * initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the shutter/slat receives an update from the Qbus client. + * + * @param qbusRolHandler + */ + public void setThingHandler(QbusRolHandler qbusRolHandler) { + this.thingHandler = qbusRolHandler; + } + + /** + * This method sets a pointer to the qComm Shutter/Slats of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Update the value of the Shutter. + * + * @param Shutter value + */ + public void updateState(@Nullable Integer state) { + this.state = state; + QbusRolHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Update the value of the Slats. + * + * @param Slat value + */ + public void updateSlats(@Nullable Integer Slats) { + this.slats = Slats; + QbusRolHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Get the value of the Shutter. + * + * @return shutter value + */ + public @Nullable Integer getState() { + return this.state; + } + + /** + * Get the value of the Slats. + * + * @return slats value + */ + public @Nullable Integer getStateSlats() { + return this.slats; + } + + /** + * Sends shutter state to Qbus. + * + * @throws IOException + * @throws InterruptedException + */ + public void execute(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } + + /** + * Sends slats state to Qbus. + * + * @throws IOException + * @throws InterruptedException + */ + public void executeSlats(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withState(value); + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java new file mode 100644 index 0000000000000..bc5fac0c40726 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; + +/** + * The {@link QbusScene} class represents the action Qbus Scene output. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusScene { + + private @Nullable QbusCommunication qComm; + + public @Nullable QbusSceneHandler thingHandler; + + private @Nullable Integer state; + + private Integer id; + + QbusScene(Integer id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the scene output receives an update from the Qbus client. + * + * @param handler + */ + public void setThingHandler(QbusSceneHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm SCENE of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus client. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Get the value of the Scene. + * + * @return Scene value + */ + public @Nullable Integer getState() { + return this.state; + } + + /** + * Sends Scene state to Qbus. + * + * @param value + * @param sn + * @throws InterruptedException + * @throws IOException + */ + public void execute(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java new file mode 100644 index 0000000000000..493d6b6b548b5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2021 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.qbus.internal.protocol; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; + +/** + * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all + * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and + * receive thermostat updates. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public final class QbusThermostat { + + private @Nullable QbusCommunication qComm; + + private Integer id; + private double measured = 0.0; + private double setpoint = 0.0; + private @Nullable Integer mode; + + private @Nullable QbusThermostatHandler thingHandler; + + QbusThermostat(Integer id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the thermostat receives an update from the Qbus client. + * + * @param handler + */ + public void setThingHandler(QbusThermostatHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm THERMOSTAT of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus client. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Update all values of the Thermostat + * + * @param measured current temperature in 1°C multiples + * @param setpoint the setpoint temperature in 1°C multiples + * @param mode 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night" + */ + public void updateState(Double measured, Double setpoint, Integer mode) { + this.measured = measured; + this.setpoint = setpoint; + this.mode = mode; + + QbusThermostatHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Get measured temperature of the Thermostat. + * + * @return measured temperature in 0.5°C multiples + */ + public @Nullable Double getMeasured() { + return this.measured; + } + + /** + * Get setpoint temperature of the Thermostat. + * + * @return the setpoint temperature in 0.5°C multiples + */ + public @Nullable Double getSetpoint() { + return this.setpoint; + } + + /** + * Get the Thermostat mode. + * + * @return the mode: 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night" + */ + public @Nullable Integer getMode() { + return this.mode; + } + + /** + * Sends Thermostat mode to Qbus. + * + * @param mode + * @param sn + * @throws InterruptedException + * @throws IOException + */ + public void executeMode(int mode, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); + QbusCommunication comm = this.qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } + + /** + * Sends Thermostat setpoint to Qbus. + * + * @param setpoint + * @throws IOException + * @throws InterruptedException + */ + public void executeSetpoint(double setpoint, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); + QbusCommunication comm = this.qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..2457b9d1b1a7d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Qbus Binding + This is the binding for the Qbus home automation system. Qbus is a system made and developed in Belgium + (https://www.qbus.be/nl-nl) + + diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties new file mode 100644 index 0000000000000..c1e57f9cd1066 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -0,0 +1,85 @@ +# binding +binding.qbus.name = Qbus Binding +binding.qbus.description = Deze binding maakt via een server applicate verbinding met de Qbus controller. + +# thing types +thing-type.qbus.bridge.label = Qbus Bridge +thing-type.qbus.bridge.description = De Qbus Bridge Maakt Verbinding Met de Qbus Server. +thing-type.config.qbus.bridge.addr.label = IP Adres of Host Naam +thing-type.config.qbus.bridge.addr.description = IP adres van de Qbus server, meestal 'localhost' +thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller +thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller +thing-type.config.qbus.bridge.port.label = Poort +thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447) +thing-type.config.qbus.bridge.serverCheck.label = Server Connectie +thing-type.config.qbus.bridge.serverCheck.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met de Qbus server gecontroleerd worden en indien nodig herstart. + +thing-type.qbus.onOff.label = Aan/uit +thing-type.qbus.onOff.description = Alle Bistabiel-Mono-Timer-Interval uitgangen +thing-type.config.qbus.onOff.bistabielId.label = Qbus ID +thing-type.config.qbus.onOff.bistabielId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.scene.label = Sfeer +thing-type.qbus.scene.description = Alle sferen +thing-type.config.qbus.scene.sceneId.label = Qbus ID +thing-type.config.qbus.scene.sceneId.description = Identificatienummer van de sfeer (zie SMIII) + +thing-type.qbus.co2.label = CO2 +thing-type.qbus.co2.description = Alle CO2 Uitgangen +thing-type.config.qbus.co2.co2Id.label = Qbus ID +thing-type.config.qbus.co2.co2Id.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.dimmer.label = Dimmer +thing-type.qbus.dimmer.description = Alle Dimbare Uitgangen +thing-type.config.qbus.dimmer.dimmerId.label = Qbus ID +thing-type.config.qbus.dimmer.dimmerId.description = Identificatienummer van de uitgang (zie SMIII) +thing-type.config.qbus.dimmer.step.label = Stappenwaarde +thing-type.config.qbus.dimmer.step.description = Waarde gebruikt voor het dimmen in stappen (standaard 10%) + +thing-type.qbus.rollershutter.label = Rolluik +thing-type.qbus.rollershutter.description = Alle Rolluik (ROL02P) Uitgangen +thing-type.config.qbus.rollershutter.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.rollershutter_slats.label = Rolluik (met lamellen) +thing-type.qbus.rollershutter_slats.description = Alle schermen met lamellen (ROL02P) uitgang +thing-type.config.qbus.rollershutter_slats.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter_slats.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.thermostat.label = Thermostaat +thing-type.qbus.thermostat.description = Alle thermostaten +thing-type.config.qbus.thermostat.thermostatId.label = Qbus ID +thing-type.config.qbus.thermostat.thermostatId.description = Identificatienummer van de uitgang (zie SMIII) + +channel-type.qbus.scene.label = Sfeer +channel-type.qbus.scene.description = Bediening van de sfeer + +channel-type.qbus.co2.label = CO2 +channel-type.qbus.co2.description = Uitlezing van de CO2 waarde + +channel-type.qbus.switch.label = Schakelaar +channel-type.qbus.switch.description = Schakelaar bediening van de uitgangen + +channel-type.qbus.brightness.label = Helderheid +channel-type.qbus.brightness.description = Helderheid bediening van de uitgangen + +channel-type.qbus.measured.label = Gemeten Temperatuur +channel-type.qbus.measured.description = Uitlezing van de gemeten Temperatuur + +channel-type.qbus.setpoint.label = Ingestelde Temperatuur +channel-type.qbus.setpoint.description = Ingestelde temperatuur bediening van de uitgangen + +channel-type.qbus.mode.label = Ingesteld Regime +channel-type.qbus.mode.description = Regime bediening van de uitgangen +channel-type.qbus.mode.state.option.0 = Manueel +channel-type.qbus.mode.state.option.1 = Vorst +channel-type.qbus.mode.state.option.2 = Economisch +channel-type.qbus.mode.state.option.3 = Comfort +channel-type.qbus.mode.state.option.4 = Nacht + +channel-type.qbus.rollershutter.label = Rolluik Bediening +channel-type.qbus.rollershutter.description = Rolluik bediening van de uitgangen + +channel-type.qbus.slats.label = Lamellen Bediening +channel-type.qbus.slats.description = Lamellen bediening van de uitgangen + diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..0b8a00cd906b3 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,231 @@ + + + + + + This bridge represents a Qbus client + + + + IP address or hostname of Qbus server, usually 'localhost' + localhost + network-address + + + + Serial number of the CTD controller + + + + Port to communicate with Qbus server, default 8447 + 8447 + true + + + + Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no refresh + will be scheduled + 10 + true + + + + + + + + + + Bistabiel-Mono-Timer-Interval Output + + + + + + + Qbus Bistabiel ID + + + + + + + + + + Qbus Scene + + + + + + + Qbus Scene ID + + + + + + + + + + Qbus CO2 + + + + + + + Qbus CO2 ID + + + + + + + + + + Qbus Dimmer Output + + + + + + + Qbus Dimmer ID + + + + Step value used for increase/decrease of dimmer brightness, default 10% + 10 + true + + + + + + + + + + Qbus shutter (ROL02P) control + + + + + + + Qbus Rol Id + + + + + + + + + + Qbus shutter with slats control + + + + + + + + Qbus Rol Id + + + + + + + + + + Qbus Thermostat + + + + + + + + + Qbus Thermostat ID + + + + + + Switch + + Scene Control for Qbus + Scene + + + + Number:Temperature + + Temperature Measured by Thermostat + Temperature + + CurrentTemperature + + + + + + Number:Temperature + + Setpoint Temperature of Thermostat + Temperature + + TargetTemperature + + + + + + Number + + Thermostat Mode + Number + + + + + + + + + + + + + Number + + CO2 value for Qbus + CO2 + + + + Rollershutter + + Rollershutter Control for Qbus + Blinds + + + + Dimmer + + Slatcontrol for Qbus + Blinds + + + diff --git a/bundles/org.openhab.binding.radiothermostat/README.md b/bundles/org.openhab.binding.radiothermostat/README.md index 74987466aae42..e5244c0de7aca 100644 --- a/bundles/org.openhab.binding.radiothermostat/README.md +++ b/bundles/org.openhab.binding.radiothermostat/README.md @@ -1,5 +1,7 @@ # RadioThermostat Binding +![RadioThermostat logo](doc/index.jpg) + This binding connects RadioThermostat/3M Filtrete models CT30, CT50/3M50, CT80, etc. with built-in Wi-Fi module to openHAB. The binding retrieves and periodically updates all basic system information from the thermostat. @@ -45,26 +47,27 @@ The thing has a few configuration parameters: The thermostat information that is retrieved is available as these channels: -| Channel ID | Item Type | Description | -|------------------------|----------------------|---------------------------------------------------------------------------| -| temperature | Number:Temperature | The current temperature reading of the thermostat | -| humidity | Number:Dimensionless | The current humidity reading of the thermostat (CT80 only) | -| mode | Number | The current operating mode of the HVAC system | -| fan_mode | Number | The current operating mode of the fan | -| program_mode | Number | The program schedule that the thermostat is running (CT80 Rev B only) | -| set_point | Number:Temperature | The current temperature set point of the thermostat | -| status | Number | Indicates the current running status of the HVAC system | -| fan_status | Number | Indicates the current fan status of the HVAC system | -| override | Number | Indicates if the normal program set-point has been manually overridden | -| hold | Switch | Indicates if the current set point temperature is to be held indefinitely | -| day | Number | The current day of the week reported by the thermostat (0 = Monday) | -| hour | Number | The current hour of the day reported by the thermostat (24 hr) | -| minute | Number | The current minute past the hour reported by the thermostat | -| dt_stamp | String | The current day of the week and time reported by the thermostat (E HH:mm) | -| today_heat_runtime | Number:Time | The total number of minutes of heating run-time today | -| today_cool_runtime | Number:Time | The total number of minutes of cooling run-time today | -| yesterday_heat_runtime | Number:Time | The total number of minutes of heating run-time yesterday | -| yesterday_cool_runtime | Number:Time | The total number of minutes of cooling run-time yesterday | +| Channel ID | Item Type | Description | +|------------------------|----------------------|------------------------------------------------------------------------------------------------------------------------------------| +| temperature | Number:Temperature | The current temperature reading of the thermostat | +| humidity | Number:Dimensionless | The current humidity reading of the thermostat (CT80 only) | +| mode | Number | The current operating mode of the HVAC system | +| fan_mode | Number | The current operating mode of the fan | +| program_mode | Number | The program schedule that the thermostat is running (CT80 Rev B only) | +| set_point | Number:Temperature | The current temperature set point of the thermostat | +| status | Number | Indicates the current running status of the HVAC system | +| fan_status | Number | Indicates the current fan status of the HVAC system | +| override | Number | Indicates if the normal program set-point has been manually overridden | +| hold | Switch | Indicates if the current set point temperature is to be held indefinitely | +| remote_temp | Number:Temperature | Override the internal temperature as read by the thermostat's temperature sensor; Set to -1 to return to internal temperature mode | +| day | Number | The current day of the week reported by the thermostat (0 = Monday) | +| hour | Number | The current hour of the day reported by the thermostat (24 hr) | +| minute | Number | The current minute past the hour reported by the thermostat | +| dt_stamp | String | The current day of the week and time reported by the thermostat (E HH:mm) | +| today_heat_runtime | Number:Time | The total number of minutes of heating run-time today | +| today_cool_runtime | Number:Time | The total number of minutes of cooling run-time today | +| yesterday_heat_runtime | Number:Time | The total number of minutes of heating run-time yesterday | +| yesterday_cool_runtime | Number:Time | The total number of minutes of cooling run-time yesterday | ## Full Example @@ -145,6 +148,9 @@ Number:Time Therm_todaycool "Today's Cooling Runtime [%d %unit%]" { channe Number:Time Therm_yesterdayheat "Yesterday's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_heat_runtime" } Number:Time Therm_yesterdaycool "Yesterday's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_cool_runtime" } +// Override the thermostat's temperature reading with a value from an external sensor, set to -1 to revert to internal temperature mode +Number:Temperature Therm_Rtemp "Remote Temperature [%d]" { channel="radiothermostat:rtherm:mytherm1:remote_temp" } + // A virtual switch used to trigger a rule to send a json command to the thermostat Switch Therm_mysetting "Send my preferred setting" ``` @@ -167,9 +173,12 @@ sitemap radiotherm label="My Thermostat" { Text item=Therm_Override icon="smoke" Switch item=Therm_Hold icon="smoke" + // Example of overriding the thermostat's temperature reading + Switch item=Therm_Rtemp label="Remote Temp" icon="temperature" mappings=[60="60", 75="75", 80="80", -1="Reset"] + // Virtual switch/button to trigger a rule to send a custom command // The ON value displays in the button - Switch item=Therm_mysetting mappings=[ON="Heat, 58, hold"] + Switch item=Therm_mysetting mappings=[ON="Heat, 68, hold"] Text item=Therm_Day Text item=Therm_Hour @@ -198,6 +207,6 @@ then } // JSON to send directly to the thermostat's '/tstat' endpoint // See RadioThermostat_CT50_Honeywell_Wifi_API_V1.3.pdf for more detail - actions.sendRawCommand('{"hold":1, "t_heat":' + "58" + ', "tmode":1}') + actions.sendRawCommand('{"hold":1, "t_heat":' + "68" + ', "tmode":1}') end ``` diff --git a/bundles/org.openhab.binding.radiothermostat/doc/index.jpg b/bundles/org.openhab.binding.radiothermostat/doc/index.jpg new file mode 100644 index 0000000000000..963f5295621ef Binary files /dev/null and b/bundles/org.openhab.binding.radiothermostat/doc/index.jpg differ diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java index 64bed1aed37bb..daf02d99e749f 100644 --- a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java +++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java @@ -47,6 +47,7 @@ public class RadioThermostatBindingConstants { public static final String DEFAULT_RESOURCE = "tstat"; public static final String RUNTIME_RESOURCE = "tstat/datalog"; public static final String HUMIDITY_RESOURCE = "tstat/humidity"; + public static final String REMOTE_TEMP_RESOURCE = "tstat/remote_temp"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_RTHERM = new ThingTypeUID(BINDING_ID, "rtherm"); @@ -70,11 +71,12 @@ public class RadioThermostatBindingConstants { public static final String TODAY_COOL_RUNTIME = "today_cool_runtime"; public static final String YESTERDAY_HEAT_RUNTIME = "yesterday_heat_runtime"; public static final String YESTERDAY_COOL_RUNTIME = "yesterday_cool_runtime"; + public static final String REMOTE_TEMP = "remote_temp"; public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_RTHERM); public static final Set SUPPORTED_CHANNEL_IDS = Stream.of(TEMPERATURE, HUMIDITY, MODE, FAN_MODE, PROGRAM_MODE, SET_POINT, OVERRIDE, HOLD, STATUS, FAN_STATUS, DAY, HOUR, MINUTE, DATE_STAMP, - TODAY_HEAT_RUNTIME, TODAY_COOL_RUNTIME, YESTERDAY_HEAT_RUNTIME, YESTERDAY_COOL_RUNTIME) + TODAY_HEAT_RUNTIME, TODAY_COOL_RUNTIME, YESTERDAY_HEAT_RUNTIME, YESTERDAY_COOL_RUNTIME, REMOTE_TEMP) .collect(Collectors.toSet()); // Units of measurement of the data delivered by the API diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java index fb25804f435fc..78646d41416b3 100644 --- a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java +++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java @@ -105,10 +105,11 @@ public void onComplete(@Nullable Result result) { * * @param the JSON attribute key for the value to be updated * @param the value to be updated in the thermostat + * @param the end point URI to use for the command * @return the JSON response string from the thermostat */ - public String sendCommand(String cmdKey, @Nullable String cmdVal) { - return sendCommand(cmdKey, cmdVal, null); + public String sendCommand(String cmdKey, @Nullable String cmdVal, String resource) { + return sendCommand(cmdKey, cmdVal, null, resource); } /** @@ -117,12 +118,14 @@ public String sendCommand(String cmdKey, @Nullable String cmdVal) { * @param the JSON attribute key for the value to be updated * @param the value to be updated in the thermostat * @param JSON string to send directly to the thermostat instead of a key/value pair + * @param the end point URI to use for the command * @return the JSON response string from the thermostat */ - public String sendCommand(@Nullable String cmdKey, @Nullable String cmdVal, @Nullable String cmdJson) { + public String sendCommand(@Nullable String cmdKey, @Nullable String cmdVal, @Nullable String cmdJson, + String resource) { // if we got a cmdJson string send that, otherwise build the json from the key and val params String postJson = cmdJson != null ? cmdJson : "{\"" + cmdKey + "\":" + cmdVal + "}"; - String urlStr = buildRequestURL(DEFAULT_RESOURCE); + String urlStr = buildRequestURL(resource); String output = ""; diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java index 84848eeb8d879..250f9251cea1c 100644 --- a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java +++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java @@ -50,12 +50,12 @@ /** * The {@link RadioThermostatDiscoveryService} is responsible for discovery of * RadioThermostats on the local network - * + * * @author William Welliver - Initial contribution * @author Dan Cunningham - Refactoring and Improvements * @author Bill Forsyth - Modified for the RadioThermostat's peculiar discovery mode * @author Michael Lobstein - Cleanup for RadioThermostat - * + * */ @NonNullByDefault @@ -81,11 +81,12 @@ protected void startBackgroundDiscovery() { TimeUnit.SECONDS); } - @SuppressWarnings("null") @Override protected void stopBackgroundDiscovery() { - if (scheduledFuture != null && !scheduledFuture.isCancelled()) { + ScheduledFuture scheduledFuture = this.scheduledFuture; + if (scheduledFuture != null) { scheduledFuture.cancel(true); + this.scheduledFuture = null; } } @@ -245,7 +246,7 @@ protected void parseResponse(String response) { try { // Run the HTTP request and get the JSON response from the thermostat sysinfo = HttpUtil.executeUrl("GET", url, 20000); - content = new JsonParser().parse(sysinfo).getAsJsonObject(); + content = JsonParser.parseString(sysinfo).getAsJsonObject(); uuid = content.get("uuid").getAsString(); } catch (IOException | JsonSyntaxException e) { logger.debug("Cannot get system info from thermostat {} {}", ip, e.getMessage()); @@ -254,7 +255,7 @@ protected void parseResponse(String response) { try { String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000); - content = new JsonParser().parse(nameinfo).getAsJsonObject(); + content = JsonParser.parseString(nameinfo).getAsJsonObject(); name = content.get("name").getAsString(); } catch (IOException | JsonSyntaxException e) { logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage()); diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java index b9b2bbaee80c3..6ab451829938d 100644 --- a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java +++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java @@ -46,6 +46,7 @@ import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -216,7 +217,7 @@ public void dispose() { } public void handleRawCommand(@Nullable String rawCommand) { - connector.sendCommand(null, null, rawCommand); + connector.sendCommand(null, null, rawCommand, DEFAULT_RESOURCE); } @Override @@ -226,21 +227,19 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else { Integer cmdInt = -1; String cmdStr = command.toString(); - if (cmdStr != null) { - try { - // parse out an Integer from the string - // ie '70.5 F' becomes 70, also handles negative numbers - cmdInt = NumberFormat.getInstance().parse(cmdStr).intValue(); - } catch (ParseException e) { - logger.debug("Command: {} -> Not an integer", cmdStr); - } + try { + // parse out an Integer from the string + // ie '70.5 F' becomes 70, also handles negative numbers + cmdInt = NumberFormat.getInstance().parse(cmdStr).intValue(); + } catch (ParseException e) { + logger.debug("Command: {} -> Not an integer", cmdStr); } switch (channelUID.getId()) { case MODE: // only do if commanded mode is different than current mode if (!cmdInt.equals(rthermData.getThermostatData().getMode())) { - connector.sendCommand("tmode", cmdStr); + connector.sendCommand("tmode", cmdStr, DEFAULT_RESOURCE); // set the new operating mode, reset everything else, // because refreshing the tstat data below is really slow. @@ -253,26 +252,26 @@ public void handleCommand(ChannelUID channelUID, Command command) { rthermData.getThermostatData().setProgramMode(-1); updateChannel(PROGRAM_MODE, rthermData); - // now just trigger a refresh of the thermost to get the new active setpoint + // now just trigger a refresh of the thermostat to get the new active setpoint // this takes a while for the JSON request to complete (async). connector.getAsyncThermostatData(DEFAULT_RESOURCE); } break; case FAN_MODE: rthermData.getThermostatData().setFanMode(cmdInt); - connector.sendCommand("fmode", cmdStr); + connector.sendCommand("fmode", cmdStr, DEFAULT_RESOURCE); break; case PROGRAM_MODE: rthermData.getThermostatData().setProgramMode(cmdInt); - connector.sendCommand("program_mode", cmdStr); + connector.sendCommand("program_mode", cmdStr, DEFAULT_RESOURCE); break; case HOLD: if (command instanceof OnOffType && command == OnOffType.ON) { rthermData.getThermostatData().setHold(1); - connector.sendCommand("hold", "1"); + connector.sendCommand("hold", "1", DEFAULT_RESOURCE); } else if (command instanceof OnOffType && command == OnOffType.OFF) { rthermData.getThermostatData().setHold(0); - connector.sendCommand("hold", "0"); + connector.sendCommand("hold", "0", DEFAULT_RESOURCE); } break; case SET_POINT: @@ -287,7 +286,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { // don't do anything if we are not in heat or cool mode break; } - connector.sendCommand(cmdKey, cmdInt.toString()); + connector.sendCommand(cmdKey, cmdInt.toString(), DEFAULT_RESOURCE); + break; + case REMOTE_TEMP: + if (cmdInt != -1) { + QuantityType remoteTemp = ((QuantityType) command) + .toUnit(ImperialUnits.FAHRENHEIT); + connector.sendCommand("rem_temp", String.valueOf(remoteTemp.intValue()), REMOTE_TEMP_RESOURCE); + } else { + connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE); + } break; default: logger.warn("Unsupported command: {}", command.toString()); @@ -320,7 +328,10 @@ public void onNewMessageEvent(RadioThermostatEvent event) { updateAllChannels(); break; case HUMIDITY_RESOURCE: - rthermData.setHumidity(gson.fromJson(evtVal, RadioThermostatHumidityDTO.class).getHumidity()); + RadioThermostatHumidityDTO dto = gson.fromJson(evtVal, RadioThermostatHumidityDTO.class); + if (dto != null) { + rthermData.setHumidity(dto.getHumidity()); + } updateChannel(HUMIDITY, rthermData); break; case RUNTIME_RESOURCE: @@ -382,7 +393,7 @@ private void updateChannel(String channelId, RadioThermostatDTO rthermData) { /** * Update a given channelId from the thermostat data - * + * * @param the channel id to be updated * @param data the RadioThermostat dto * @return the value to be set in the state @@ -456,7 +467,7 @@ private void updateAllChannels() { /** * Build a list of fan modes based on what model thermostat is used - * + * * @return list of state options for thermostat fan modes */ private List getFanModeOptions() { diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.radiothermostat/src/main/resources/OH-INF/thing/thing-types.xml index 557d36c560bbe..a33622dbdc557 100644 --- a/bundles/org.openhab.binding.radiothermostat/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.radiothermostat/src/main/resources/OH-INF/thing/thing-types.xml @@ -22,6 +22,7 @@ + @@ -157,6 +158,14 @@ + + Number:Temperature + + The remote temperature takes the place of the ambient temperature as read by the local thermostat + temperature sensor + + + Number diff --git a/bundles/org.openhab.binding.remoteopenhab/README.md b/bundles/org.openhab.binding.remoteopenhab/README.md index 9d56fc1edcfc5..433a910281d38 100644 --- a/bundles/org.openhab.binding.remoteopenhab/README.md +++ b/bundles/org.openhab.binding.remoteopenhab/README.md @@ -42,12 +42,18 @@ The `server` thing has the following configuration parameters: | useHttps | no | Set to true if you want to use HTTPS to communicate with the remote openHAB server. Default is false. | | port | yes | The HTTP port to use to communicate with the remote openHAB server. Default is 8080. | | trustedCertificate | no | Set to true if you want to use HTTPS even without a valid SSL certificate provided by your remote server. | -| restPath | yes | The subpath of the REST API on the remote openHAB server. Default is /rest | +| restPath | yes | The subpath of the REST API on the remote openHAB server. Default is "/rest/" | | token | no | The token to use when the remote openHAB server is setup to require authorization to run its REST API. | +| username | no | The username to use when the remote openHAB server is setup to require basic authorization to run its REST API. | +| password | no | The password to use when the remote openHAB server is setup to require basic authorization to run its REST API. | +| authenticateAnyway | no | Set it to true in case you want to pass authentication information even when the communicate with the remote openHAB server is not secured (only HTTP). This is of course not recommended especially if your connection is over the Internet. Default is false. | | accessibilityInterval | no | Minutes between checking the remote server accessibility. 0 to disable the check. Default is 3. | | aliveInterval | no | Number of last minutes to consider when monitoring the receipt of events from the remote server. If an event is received during this interval, the remote server is considered alive and its accessibility will not be verified. Use 0 to disable this feature. Default is 5. | | restartIfNoActivity | no | Set it to true if you want to restart the connection (SSE) to the remote server when no events are received in the monitored interval. It is not necessary if the goal is to properly handle a short network outage (few seconds). This can be useful if you want to deal with a long network outage. Do not enable it if you remote server does not send events during the monitored interval under normal conditions, it will cause frequent restart of the connection and potential loss of events. Default is false. | +Please note that even though the default configuration is based on insecure communication over HTTP, it is recommended to adjust the configuration to be based on secure communication over HTTPS. +This is of course essential if your connection to the remote openHAB server is over the Internet. + The `thing` thing has the following configuration parameters: | Parameter | Required | Description | @@ -55,7 +61,8 @@ The `thing` thing has the following configuration parameters: | thingUID | yes | The thing UID in the remote openHAB server. | | buildTriggerChannels | no | If set to true, a trigger channel will be automatically created and linked to each trigger channel from the remote thing. Default is true. | -Please note that if your remote server is an openHAB v3 server, you will need to define a valid token on your bridge thing to have your things correctly initialized. +Please note that if your remote server is an openHAB v3 server, in order for all of your things to be properly initialized, you will need to define on your bridge thing a valid API token in the parameter `token` and also define the parameter `authenticateAnyway` to true in case you are using an unsecured connection (HTTP). +This API token can be created on your remote server using Main UI. Setting the `buildTriggerChannels` parameter to false is for the main following advanced usages: diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/feature/feature.xml b/bundles/org.openhab.binding.remoteopenhab/src/main/feature/feature.xml index 61bb75b03f034..88c5d8ca06c4c 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/feature/feature.xml @@ -5,7 +5,6 @@ openhab-runtime-base - mvn:org.openhab.addons.bundles/org.openhab.binding.remoteopenhab/${project.version} - + mvn:org.openhab.addons.bundles/org.openhab.binding.remoteopenhab/${project.version} diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/config/RemoteopenhabServerConfiguration.java b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/config/RemoteopenhabServerConfiguration.java index f139ff690bd7c..1ee9e90f5073a 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/config/RemoteopenhabServerConfiguration.java +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/config/RemoteopenhabServerConfiguration.java @@ -31,8 +31,11 @@ public class RemoteopenhabServerConfiguration { public boolean useHttps = false; public int port = 8080; public boolean trustedCertificate = false; - public String restPath = "/rest"; + public String restPath = "/rest/"; public String token = ""; + public String username = ""; + public String password = ""; + public boolean authenticateAnyway = false; public int accessibilityInterval = 3; public int aliveInterval = 5; public boolean restartIfNoActivity = false; diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/handler/RemoteopenhabBridgeHandler.java b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/handler/RemoteopenhabBridgeHandler.java index 7aef255bb0413..f7ec33b018946 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/handler/RemoteopenhabBridgeHandler.java +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/handler/RemoteopenhabBridgeHandler.java @@ -150,13 +150,10 @@ public void initialize() { } String urlStr = url.toString(); - if (urlStr.endsWith("/")) { - urlStr = urlStr.substring(0, urlStr.length() - 1); - } logger.debug("REST URL = {}", urlStr); restClient.setRestUrl(urlStr); - restClient.setAccessToken(config.token); + restClient.setAuthenticationData(config.authenticateAnyway, config.token, config.username, config.password); if (config.useHttps && config.trustedCertificate) { restClient.setHttpClient(httpClientTrustingCert); restClient.setTrustedCertificate(true); diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabRestClient.java b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabRestClient.java index a294e2215d2c3..b6ebe5d1a3307 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabRestClient.java +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabRestClient.java @@ -14,9 +14,11 @@ import java.io.ByteArrayOutputStream; import java.io.EOFException; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; @@ -89,7 +92,9 @@ public class RemoteopenhabRestClient { private @Nullable String restApiVersion; private Map apiEndPointsUrls = new HashMap<>(); private @Nullable String topicNamespace; + private boolean authenticateAnyway; private String accessToken; + private String credentialToken; private boolean trustedCertificate; private boolean connected; private boolean completed; @@ -104,6 +109,7 @@ public RemoteopenhabRestClient(final HttpClient httpClient, final ClientBuilder this.eventSourceFactory = eventSourceFactory; this.jsonParser = jsonParser; this.accessToken = ""; + this.credentialToken = ""; } public void setHttpClient(HttpClient httpClient) { @@ -122,8 +128,16 @@ public void setRestUrl(String restUrl) { this.restUrl = restUrl; } - public void setAccessToken(String accessToken) { + public void setAuthenticationData(boolean authenticateAnyway, String accessToken, String username, + String password) { + this.authenticateAnyway = authenticateAnyway; this.accessToken = accessToken; + if (username.isBlank() || password.isBlank()) { + this.credentialToken = ""; + } else { + String token = username + ":" + password; + this.credentialToken = Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)); + } } public void setTrustedCertificate(boolean trustedCertificate) { @@ -132,7 +146,7 @@ public void setTrustedCertificate(boolean trustedCertificate) { public void tryApi() throws RemoteopenhabException { try { - String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false); + String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false, false); if (jsonResponse.isEmpty()) { throw new RemoteopenhabException("JSON response is empty"); } @@ -160,7 +174,7 @@ public List getRemoteItems(@Nullable String fields) throws Re url += "&fields=" + fields; } boolean asyncReading = fields == null || Arrays.asList(fields.split(",")).contains("state"); - String jsonResponse = executeGetUrl(url, "application/json", asyncReading); + String jsonResponse = executeGetUrl(url, "application/json", false, asyncReading); if (jsonResponse.isEmpty()) { throw new RemoteopenhabException("JSON response is empty"); } @@ -174,7 +188,7 @@ public List getRemoteItems(@Nullable String fields) throws Re public String getRemoteItemState(String itemName) throws RemoteopenhabException { try { String url = String.format("%s/%s/state", getRestApiUrl("items"), itemName); - return executeGetUrl(url, "text/plain", true); + return executeGetUrl(url, "text/plain", false, true); } catch (RemoteopenhabException e) { throw new RemoteopenhabException("Failed to get the state of remote item " + itemName + " using the items REST API: " + e.getMessage(), e); @@ -184,7 +198,8 @@ public String getRemoteItemState(String itemName) throws RemoteopenhabException public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException { try { String url = String.format("%s/%s", getRestApiUrl("items"), itemName); - executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, true); + executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, false, + true); } catch (RemoteopenhabException e) { throw new RemoteopenhabException("Failed to send command to the remote item " + itemName + " using the items REST API: " + e.getMessage(), e); @@ -193,7 +208,7 @@ public void sendCommandToRemoteItem(String itemName, Command command) throws Rem public List getRemoteThings() throws RemoteopenhabException { try { - String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", false); + String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", true, false); if (jsonResponse.isEmpty()) { throw new RemoteopenhabException("JSON response is empty"); } @@ -207,7 +222,7 @@ public List getRemoteThings() throws RemoteopenhabException public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabException { try { String url = String.format("%s/%s", getRestApiUrl("things"), uid); - String jsonResponse = executeGetUrl(url, "application/json", false); + String jsonResponse = executeGetUrl(url, "application/json", true, false); if (jsonResponse.isEmpty()) { throw new RemoteopenhabException("JSON response is empty"); } @@ -224,7 +239,14 @@ public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabExcepti private String getRestApiUrl(String endPoint) throws RemoteopenhabException { String url = apiEndPointsUrls.get(endPoint); - return url != null ? url : getRestUrl() + "/" + endPoint; + if (url == null) { + url = getRestUrl(); + if (!url.endsWith("/")) { + url += "/"; + } + url += endPoint; + } + return url; } public String getTopicNamespace() { @@ -250,6 +272,7 @@ public void stop(boolean waitingForCompletion) { } private SseEventSource createEventSource(String restSseUrl) { + String credentialToken = restSseUrl.startsWith("https:") || authenticateAnyway ? this.credentialToken : ""; Client client; // Avoid a timeout exception after 1 minute by setting the read timeout to 0 (infinite) if (trustedCertificate) { @@ -259,11 +282,11 @@ private SseEventSource createEventSource(String restSseUrl) { public boolean verify(@Nullable String hostname, @Nullable SSLSession session) { return true; } - }).readTimeout(0, TimeUnit.SECONDS).register(new RemoteopenhabStreamingRequestFilter(accessToken)) - .build(); + }).readTimeout(0, TimeUnit.SECONDS) + .register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build(); } else { client = clientBuilder.readTimeout(0, TimeUnit.SECONDS) - .register(new RemoteopenhabStreamingRequestFilter(accessToken)).build(); + .register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build(); } SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl)); eventSource.register(this::onEvent, this::onError, this::onComplete); @@ -471,18 +494,31 @@ private String extractThingUIDFromTopic(String topic, String eventType, String f return parts[2]; } - public String executeGetUrl(String url, String acceptHeader, boolean asyncReading) throws RemoteopenhabException { - return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, asyncReading, true); + public String executeGetUrl(String url, String acceptHeader, boolean provideAccessToken, boolean asyncReading) + throws RemoteopenhabException { + return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, provideAccessToken, asyncReading, true); } public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, @Nullable String content, - @Nullable String contentType, boolean asyncReading, boolean retryIfEOF) throws RemoteopenhabException { - final Request request = httpClient.newRequest(url).method(httpMethod).timeout(REQUEST_TIMEOUT, - TimeUnit.MILLISECONDS); - - request.header(HttpHeaders.ACCEPT, acceptHeader); - if (!accessToken.isEmpty()) { - request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + @Nullable String contentType, boolean provideAccessToken, boolean asyncReading, boolean retryIfEOF) + throws RemoteopenhabException { + final Request request = httpClient.newRequest(url).method(httpMethod) + .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false) + .header(HttpHeaders.ACCEPT, acceptHeader); + + if (url.startsWith("https:") || authenticateAnyway) { + boolean useAlternativeHeader = false; + if (!credentialToken.isEmpty()) { + request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken); + useAlternativeHeader = true; + } + if (provideAccessToken && !accessToken.isEmpty()) { + if (useAlternativeHeader) { + request.header("X-OPENHAB-TOKEN", accessToken); + } else { + request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + } + } } if (content != null && (HttpMethod.POST.equals(httpMethod) || HttpMethod.PUT.equals(httpMethod)) @@ -490,6 +526,8 @@ public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, request.content(new StringContentProvider(content), contentType); } + logger.debug("Request {} {}", request.getMethod(), request.getURI()); + try { if (asyncReading) { InputStreamResponseListener listener = new InputStreamResponseListener(); @@ -509,7 +547,17 @@ public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, } else { ContentResponse response = request.send(); int statusCode = response.getStatus(); - if (statusCode >= HttpStatus.BAD_REQUEST_400) { + if (statusCode == HttpStatus.MOVED_PERMANENTLY_301 || statusCode == HttpStatus.FOUND_302) { + String locationHeader = response.getHeaders().get(HttpHeaders.LOCATION); + if (locationHeader != null && !locationHeader.isBlank()) { + logger.debug("The remopte server redirected the request to this URL: {}", locationHeader); + return executeUrl(httpMethod, locationHeader, acceptHeader, content, contentType, + provideAccessToken, asyncReading, retryIfEOF); + } else { + String statusLine = statusCode + " " + response.getReason(); + throw new RemoteopenhabException("HTTP call failed: " + statusLine); + } + } else if (statusCode >= HttpStatus.BAD_REQUEST_400) { String statusLine = statusCode + " " + response.getReason(); throw new RemoteopenhabException("HTTP call failed: " + statusLine); } @@ -525,11 +573,15 @@ public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, Throwable cause = e.getCause(); if (retryIfEOF && cause instanceof EOFException) { logger.debug("EOFException - retry the request"); - return executeUrl(httpMethod, url, acceptHeader, content, contentType, asyncReading, false); + return executeUrl(httpMethod, url, acceptHeader, content, contentType, provideAccessToken, asyncReading, + false); } else { throw new RemoteopenhabException(e); } - } catch (Exception e) { + } catch (IOException | TimeoutException e) { + throw new RemoteopenhabException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RemoteopenhabException(e); } } diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabStreamingRequestFilter.java b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabStreamingRequestFilter.java index fbeae9da021e0..8e9b03eef5ace 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabStreamingRequestFilter.java +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/java/org/openhab/binding/remoteopenhab/internal/rest/RemoteopenhabStreamingRequestFilter.java @@ -30,18 +30,18 @@ @NonNullByDefault public class RemoteopenhabStreamingRequestFilter implements ClientRequestFilter { - private final String accessToken; + private final String credentialToken; - public RemoteopenhabStreamingRequestFilter(String accessToken) { - this.accessToken = accessToken; + public RemoteopenhabStreamingRequestFilter(String credentialToken) { + this.credentialToken = credentialToken; } @Override public void filter(@Nullable ClientRequestContext requestContext) throws IOException { if (requestContext != null) { MultivaluedMap headers = requestContext.getHeaders(); - if (!accessToken.isEmpty()) { - headers.putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + if (!credentialToken.isEmpty()) { + headers.putSingle(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken); } headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache"); } diff --git a/bundles/org.openhab.binding.remoteopenhab/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.remoteopenhab/src/main/resources/OH-INF/thing/thing-types.xml index 5d7c0fe6b0036..f055776a37169 100644 --- a/bundles/org.openhab.binding.remoteopenhab/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.remoteopenhab/src/main/resources/OH-INF/thing/thing-types.xml @@ -43,7 +43,7 @@ The subpath of the REST API on the remote openHAB server. - /rest + /rest/ true @@ -54,6 +54,30 @@ true + + + The username to use when the remote openHAB server is setup to require basic authorization to run its + REST API. + true + + + + password + + The password to use when the remote openHAB server is setup to require basic authorization to run its + REST API. + true + + + + + Set it to true in case you want to pass authentication information even when the communicate with the + remote openHAB server is not secured (only HTTP). This is of course not recommended especially if your connection + is over the Internet. Default is false. + false + true + + Minutes between checking the remote server accessibility. 0 to disable the check. Default is 3. diff --git a/bundles/org.openhab.binding.resol/NOTICE b/bundles/org.openhab.binding.resol/NOTICE new file mode 100644 index 0000000000000..7338f9fae8801 --- /dev/null +++ b/bundles/org.openhab.binding.resol/NOTICE @@ -0,0 +1,20 @@ +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 + +== Third-party Content + +resol-vbus-java +* License: MIT License +* Project: https://github.com/danielwippermann/resol-vbus-java +* Source: https://github.com/danielwippermann/resol-vbus-java \ No newline at end of file diff --git a/bundles/org.openhab.binding.resol/README.md b/bundles/org.openhab.binding.resol/README.md new file mode 100644 index 0000000000000..23322032765dc --- /dev/null +++ b/bundles/org.openhab.binding.resol/README.md @@ -0,0 +1,251 @@ +# Resol Binding + +Resol Binding connects to Solar and System Controllers of RESOL - Elektronische Regelungen GmbH, also including branded versions from Viessmann, SOLEX, COSMO, SOLTEX, DeDietrich and many more. + +![Resol](doc/RESOL_Logo_de.png) + +This binding is based on and includes the [Resol-VBUS-Java library](https://github.com/danielwippermann/resol-vbus-java), developed by Daniel Wippermann. + + +## Supported Things + +### Bridge + +For the connection of the VBUS devices a network interface is required. +Supported interfaces are VBUS-LAN, KM1, KM2, DataLogger DL2 and DL3 as live data interface between openHAB and the Resol VBUS via network. +On the DL3 currently there is only the first VBUS channel supported and the sensors directly connected to the DL3 are not accessible via this binding. +Currently only network based bridges are supported and if a USB-VBUS interface shall be used a serial-network proxy has to be used. +In the binding might support USB-type bridges in future. + +### Device + +On top of the bridge things, which enable access to the VBUS, many - if not all - Resol Controllers and Modules like WMZ heat meters, HKM Heating circuit extensions etc. are supported including branded versions from different suppliers as thing type *device*. +The supported devices include + + * Solar controller DeltaSol® A/AX/AX HE + * Solar controller DeltaSol® AL E HE + * Solar controller DeltaSol® CS (Plus) + * Solar controller DeltaSol® B + * Solar controller DeltaSol® BS series + * Solar controller DeltaSol® SLL + * Solar controller DeltaSol® SL + * Solar controller DeltaSol® BX series + * System controller DeltaSol® SLT + * System controller DeltaSol® MX + * System controller DeltaSol® E + * DeltaSol Fresh® + * DeltaSol® Pool + * DeltaSol® Minipool + * DeltaSol® ES + * Frista + * DeltaTherm® HC + * DeltaTherm® FK + * Deltatherm® HT + * DeltaTherm® DH + * Sonnenkraft SKSC3 + * Sonnenkraft STRG BX PLUS + * Sonnenkraft SKSC3+ + * Sonnenkraft SKSC3HE + * Sonnenkraft SKSR 1/2/3 + * Vitosolic 200 + * COSMO Multi + * Drainback DeDietrich + * Diemasol C + +A more complete list can be found in the doc of the [resol-vbus-java library](http://danielwippermann.github.io/resol-vbus/vbus-packets.html). + +### Emulated Extension Module EM + +Some controllers like the Deltasol MX can be connected to an extension module, which can be emulated by the thing type *emulatedEM*. +The emulated EM is a virtual device, visible on the VBUS to a Resol controller and provides an interface between openHAB and the controller. +Relay channels are outputs from the controller point of view and therefore read-only in OH. +The sensor channels as inputs for the solar or system controller and intended to be written by OH. + + +## Discovery + +Discovery is tested for VBus-LAN adapters DL2, DL3 and KM2 devices, it should also work for other devices providing a live data port. +After a bridge is detected in the local network the password needs to be given and the things on the VBUS will popup in the inbox. + + +## Bridge Configuration + +The bridge is the device connecting the Resol VBUS to the network, usually a VBus-LAN adapter or integrated in some of the solar controllers like DL3. +For the connection from the Resol binding the bridge requires the configuration of the following parameters: + +| Parameter | Type | Required | Description | +|----------------------|---------|----------|------------------------------------------------------------| +| ipAddress | String | yes | IP address or hostname of the VBUS adapter | +| password | String | yes | Password, defaults to 'vbus' for factory setting devices | +| port | Number | no | Port for the TCP connection, defaults to 7053 | +| adapterSerial | String | no | Serialnumber of the device (informative only) | + +## Device Configuration + +Depending on the solar/heating controller you have attached to your VBUS there will be a "controller" and several other things like heat quantity meters, heating circuit controls, etc. +These do not require any configuration parameters and will pop up in your inbox after the bridge has received data from them. +The name of the devices is usually the Resol name with spaced replaced by _ and a "-Controller" suffix like "DeltaSol_MX-Controller". +For configuration in files you can enable the logging with at least DEBUG level for the resol binding and search the logs for "ThingHandler for (.*) not registered." to identify the names of the things you can add for your VBUS devices. + +## Emulated EM Configuration + +*emulatedEM* devices cannot be auto-discovered and require beside the bridge the following configuration: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-----------------------------------------------------------------------------------------------------------------| +| moduleID | int | yes | The module ID on the VBUS in range 0-15, but further restrictions might apply depending on the resol controller. | + + +## Device Channels + +The channels of a thing are determined automatically based on the received VBUS data and are highly dependent on the used device. +Here is a list of the channels of a DeltaSol MX with a heat quantity meter (HQM) and an extension module EM. +The channels supported for your device can be seen in the UI or in the logs if DEBUG logging is enabled for this binding after data is received from the physical device. + +| Channel | Type | Description | +|-----------------------------------|--------------------------|----------------------------------------------------| +| pump_speed_relay_x | Number:Dimensionless | Percentage of the output state of relay 'x' | +| temperature_sensor_x | Number:Temperature | Temperature sensor 'x' of the controller | +| temperature_module_y_sensor_x | Number:Temperature | Temperature sensor 'x' of the extension module 'y' | +| pressure_sensor_x | Number:Pressure | Pressure sensor 'x' | +| humidity_sensor_x | Number:Dimensionless | Humidity sensor 'x' | +| irradiation_sensor_x | Number:Intensity | Sunlight intensity sensor | +| output_m | Number:Dimensionless | PWM/0-10V level value of the output 'm' | +| system_date | DateTime | Date and time of the controller clock | +| error_mask | Number | Bitmask for the different errors | +| error_sensor_line_broken | Number | Sensor line broken status (details for error_mask) | +| error_sensor_line_short-circuited | Number | Sensor short circuit status (details for error_mask) | +| flow_rate_sensor_x | Number:VolumetricFlowRate| Flow rate of sensor 'x' | +| flow_set_temperature | Number:Temperature | Heating circuit set temperature | +| operating_state | Number | Heating circuit operating state | +| heat_quantity | Number:Energy | Total heat quantity (of a HQM) | +| heat_quantity_today | Number:Energy | Todays heat quantity (of a HQM) | +| heat_quantity_week | Number:Energy | This weeks heat quantity (of a HQM) | +| heat_quantity_month | Number:Energy | This months heat quantity (of a HQM) | +| volume_in_total | Number:Volume | Total volume (of a HQM) | +| volume_today | Number:Volume | Todays volume (of a HQM) | +| volume_week | Number:Volume | This weeks volume (of a HQM) | +| volume_month | Number:Volume | This months volume (of a HQM) | +| power | Number:Power | Current power (of a HQM) | + + +Channels are dynamically created dependent on the devices connected to the VBus. +So far only reading is supported. +The classical channels are for temperature sensors and the like, but also relay outputs with the output level (0-100%) are visible as numerical values with the corresponding unit. + +String values are localized as far as possible, but only French, German and English are supported by the underlaying library which is based on the vbus-specification file from Resol. + + +## EmulatedEM Channels + +The channels of an emulated EM modules are as for physical EMs 5 relay channels and 6 input channels. +The relays are virtual outputs and read-only in OH. +The sensors support different types like temperature input which are simulated by a PT1000 resistance value, a switch and the raw resistance value. +Additionally the virtual input device for adjusting the heating circuits as a *BAS* is supported by two different channels for temperature and mode adjustment. +The type of the sensor inputs must be configured in the Resol Controller accordingly. +From all possible sensor channels (temperatureX, switchX, etc.) only one shall be linked to an item at a time, except for BAS which emulates a RCP12 room control unit where both, BasTempAdjustmentX and BasModeX shall be written from OH. + +| Channel | Type | Description | +|----------------------|---------------------------|----------------------------------------------------| +| relayX | Number:Dimensionless | Read-only percentage of the virtual output state of relay 'x' as set by the Resol Controller. | +| temperatureX | Number:Temperature | Writable temperature value for the virtual input for sensor 'x'. | +| resistorX | Number:ElectricResistance | Writable resistance value for the virtual input for sensor 'x'. | +| switchX | Switch | Writable switch state for the virtual input for sensor 'x'. | +| BasTempAdjustmentX | Number:Temperature | Writable temperature adjustment for the virtual room control module BAS on the for the virtual input for sensor 'x'. Use together with BasModeX, not effective if BasModeX is OFF or Party. | +| BasModeX | Number | Writable heating circuit mode for the virtual room control module BAS on the for the virtual input for sensor 'x'. Use together with BasTempAdjustmentX.| + + + +## Full Example + +For a DeltaSol MX system controller with on extension module EM you can use this example: + +resol.things + +``` +Bridge resol:vbuslan:VBUS "VBUSLAN" [ ipAddress="192.168.0.2", password="vbus", port=7053] { + Thing device DeltaSol_MX-Controller "DeltaSol MX [Controller]" [] + Thing device DeltaSol_MX-Heating_circuit-1 "DeltaSol MX [Heating Circuit]" [] + Thing device DeltaSol_MX-HQM-1 "DeltaSol MX [WMZ 1] Solar" [] + Thing device DeltaSol_MX-Modules "DeltaSol MX [Modules]" [] + Thing emulatedEM EM2 "Emulated EM2" [deviceId=2] +} +``` + +resol.items + +``` +/*************************************************/ +/* Solar system */ +/*************************************************/ +Number:Temperature SolarTemperature "Solar Collector Temperature [%.1f %unit%]" { channel="resol:device:VBUS:DeltaSol_MX-Controller:temperature_sensor_1" } +Number:Temperature TankTemperature "Solar Tank Temperature [%.1f %unit%]" { channel="resol:device:VBUS:DeltaSol_MX-Controller:temperature_sensor_2" } +Number:Intensity Irradiation "Irradiation [%.1f %unit%]" {channel="resol:device:VBUS:DeltaSol_MX-Controller:irradiation_sensor_16"} +Number SolarPump "Solar pump [%.0f %%]" {channel="resol:device:VBUS:DeltaSol_MX-Controller:pump_speed_relay_1"} + + +/*************************************************/ +/* Heating circuit */ +/*************************************************/ +Number:Temperature FlowSetTemperature "Flow Set Temperature [%.1f %unit%]" {channel="resol:device:VBUS:DeltaSol_MX-Heating_circuit-1:flow_set_temperature"} + +String HeatCircuit_OperatingState "HeatCircuit OperatingState [%s]" {channel="resol:device:VBUS:DeltaSol_MX-Heating_circuit-1:operating_state"} + + +/*************************************************/ +/* Heat quantity meter */ +/*************************************************/ +Number:Energy SolarEnergy_today "Solar Energy (today) [%.1f %unit%]" {channel="resol:device:VBUS:DeltaSol_MX-HQM-1:heat_quantity_today"} +Number:Power SolarPower "Solar Power [%.0f %unit%]" {channel="resol:device:VBUS:DeltaSol_MX-HQM-1:power"} + + +/*************************************************/ +/* Physical EM Module 1 */ +/*************************************************/ +Number:Temperature EM_Temperature_1 "Temperature EM sensor 1 [%.1f %unit%]" {channel="resol:device:VBUS:DeltaSol_MX-Modules:temperature_module_1_sensor_1"} + + +/*************************************************/ +/* Virtual EM Module 2, simulated by openHAB */ +/*************************************************/ +Number:Dimensionless Relay_1 "Virtual Relay 1 on EM2 [%d %%] {channel="resol:emulatedEM:VBUS:EM2:relay_1"} +Number:Dimensionless Emu_Temperature_1 "Virtual temperature input 1 on EM2 [%.1f %unit%] {channel="resol:emulatedEM:VBUS:EM2:temperature_1"} +Switch Emu_Switch_2 "Virtual switch input 2 on EM2 " {channel="resol:emulatedEM:VBUS:EM2:switch_2"} +Number:Temperature EM_BAS_Set_Temperature_3 "Set Temperature of virtual room control unit on EM2 sensor 3 [%.1f %unit%]" {channel="resol:emulatedEM:VBUS:EM2:bas_temp_adjust_3"} +Number EM_BAS_Mode "Mode of virtual room control unit on EM2 sensor 3 [%.1f %unit%]" {channel="resol:emulatedEM:VBUS:EM2:bas_mode_3"} + + +/*************************************************/ +/* Failure handling */ +/*************************************************/ +Number Errormask "Error mask [%.0f]" {channel="resol:device:VBUS:DeltaSol_MX-Controller:error_mask"} +Number Warningmask "Warning mask [%.0f]" {channel="resol:device:VBUS:DeltaSol_MX-Controller:warning_mask"} +String BrokenSensor "Broken Sensor [%s]" {channel="resol:device:VBUS:DeltaSol_MX-Controller:error_Sensor_line_broken"} + + +``` + +resol.sitemap + +``` +sitemap resol label="DeltaSol MX" { + Frame label="Solar" { + Text item=SolarTemperature valuecolor=[<0="white", <20="blue", <50="green", <80="orange", <120="red", >=120="black"] + Text item=TankTemperature valuecolor=[<30="blue", <50="yellow", <70="orange", >=70="red"] + Text item=Irradiation + Text item=SolarPump valuecolor=[==0="red",==100="green", >50="orange", >0="yellow"] + Text item=SolarPower + Text item=SolarEnergy_today + } + Frame label="Status" { + Text item=Errormask valuecolor=[==0="green", !=0="red"] + Text item=Warningmask valuecolor=[==0="green", !=0="red"] + Text item=BrokenSensor valuecolor=[=="Okay"="green", !="Okay"="red"] + } + Frame label="Emulated EM" { + Default item=Emu_Switch_2 + Setpoint item=EM_BAS_Set_Temperature_3 label="Room Temperature Adjust [%.1f °C]" step=0.5 minValue=-15 maxValue=15 + } +} + +``` diff --git a/bundles/org.openhab.binding.resol/doc/RESOL_Logo_de.png b/bundles/org.openhab.binding.resol/doc/RESOL_Logo_de.png new file mode 100644 index 0000000000000..55756264b0f1e Binary files /dev/null and b/bundles/org.openhab.binding.resol/doc/RESOL_Logo_de.png differ diff --git a/bundles/org.openhab.binding.resol/pom.xml b/bundles/org.openhab.binding.resol/pom.xml new file mode 100644 index 0000000000000..096a60285f724 --- /dev/null +++ b/bundles/org.openhab.binding.resol/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.resol + + openHAB Add-ons :: Bundles :: Resol Binding + + + + de.resol + vbus + 0.7.0 + compile + + + + diff --git a/bundles/org.openhab.binding.resol/src/main/feature/feature.xml b/bundles/org.openhab.binding.resol/src/main/feature/feature.xml new file mode 100644 index 0000000000000..17c12aea3826d --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.resol/${project.version} + + diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBaseThingHandler.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBaseThingHandler.java new file mode 100644 index 0000000000000..487acfc91836a --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBaseThingHandler.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 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.resol.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.BaseThingHandler; + +import de.resol.vbus.Packet; +import de.resol.vbus.Specification; +import de.resol.vbus.SpecificationFile.Language; + +/** + * The {@link ResolBaseThingHandler} class is a common ancestor for Resol thing handlers, capabale of handling vbus + * packets + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public abstract class ResolBaseThingHandler extends BaseThingHandler { + + public ResolBaseThingHandler(Thing thing) { + super(thing); + } + + protected abstract void packetReceived(Specification spec, Language lang, Packet packet); +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBridgeHandler.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBridgeHandler.java new file mode 100644 index 0000000000000..e0d8e6c2057b4 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolBridgeHandler.java @@ -0,0 +1,341 @@ +/** + * Copyright (c) 2010-2021 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.resol.handler; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.internal.ResolBindingConstants; +import org.openhab.binding.resol.internal.ResolBridgeConfiguration; +import org.openhab.binding.resol.internal.discovery.ResolDeviceDiscoveryService; +import org.openhab.core.i18n.LocaleProvider; +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.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.resol.vbus.Connection; +import de.resol.vbus.Connection.ConnectionState; +import de.resol.vbus.ConnectionAdapter; +import de.resol.vbus.Packet; +import de.resol.vbus.Specification; +import de.resol.vbus.SpecificationFile; +import de.resol.vbus.SpecificationFile.Language; +import de.resol.vbus.TcpDataSource; +import de.resol.vbus.TcpDataSourceProvider; + +/** + * The {@link ResolBridgeHandler} class handles the connection to the VBUS/LAN adapter. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(ResolBridgeHandler.class); + + private String ipAddress = ""; + private String password = ""; + private int refreshInterval; + private boolean isConnected = false; + private String unconnectedReason = ""; + + // Background Runnable + private @Nullable ScheduledFuture pollingJob; + + private @Nullable Connection tcpConnection; + private final Specification spec; + + // Managing Thing Discovery Service + private @Nullable ResolDeviceDiscoveryService discoveryService = null; + + private boolean scanning; + + private final @Nullable LocaleProvider localeProvider; + + public ResolBridgeHandler(Bridge bridge, @Nullable LocaleProvider localeProvider) { + super(bridge); + spec = Specification.getDefaultSpecification(); + this.localeProvider = localeProvider; + } + + public void updateStatus() { + if (isConnected) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, unconnectedReason); + } + } + + public void registerDiscoveryService(ResolDeviceDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + public void unregisterDiscoveryService() { + discoveryService = null; + } + + @Override + public Collection> getServices() { + return Collections.singleton(ResolDeviceDiscoveryService.class); + } + + public void registerResolThingListener(ResolEmuEMThingHandler resolEmuEMThingHandler) { + synchronized (this) { + Connection con = tcpConnection; + if (con != null) { + resolEmuEMThingHandler.useConnection(con); + } + } + } + + private void pollingRunnable() { + if (!isConnected) { + synchronized (ResolBridgeHandler.this) { + Connection connection = tcpConnection; + /* first cleanup in case there is an old but failed TCP connection around */ + try { + if (connection != null) { + connection.disconnect(); + + getThing().getThings().stream().forEach(thing -> { + ThingHandler th = thing.getHandler(); + if (th instanceof ResolEmuEMThingHandler) { + ((ResolEmuEMThingHandler) th).stop(); + } + }); + + connection = null; + tcpConnection = null; + } + } catch (IOException e) { + logger.warn("TCP disconnect failed: {}", e.getMessage()); + } + TcpDataSource source = null; + /* now try to establish a new TCP connection */ + try { + source = TcpDataSourceProvider.fetchInformation(InetAddress.getByName(ipAddress), 500); + if (source != null) { + source.setLivePassword(password); + } + } catch (IOException e) { + isConnected = false; + unconnectedReason = Objects.requireNonNullElse(e.getMessage(), ""); + } + if (source != null) { + try { + logger.debug("Opening a new connection to {} {} @{}", source.getProduct(), + source.getDeviceName(), source.getAddress()); + connection = source.connectLive(0, 0x0020); + tcpConnection = connection; + } catch (Exception e) { + // this generic Exception catch is required, as TcpDataSource.connectLive throws this + // generic type + isConnected = false; + unconnectedReason = Objects.requireNonNullElse(e.getMessage(), ""); + } + + if (connection != null) { + // Add a listener to the Connection to monitor state changes and + // read incoming frames + connection.addListener(new ResolConnectorAdapter()); + } + } + // Establish the connection + if (connection != null) { + try { + connection.connect(); + final Connection c = connection; + // now set the connection the thing handlers for the emulated EMs + + getThing().getThings().stream().forEach(thing -> { + ThingHandler th = thing.getHandler(); + if (th instanceof ResolEmuEMThingHandler) { + ((ResolEmuEMThingHandler) th).useConnection(c); + } + }); + } catch (IOException e) { + unconnectedReason = Objects.requireNonNullElse(e.getMessage(), ""); + isConnected = false; + } + } else { + isConnected = false; + } + if (!isConnected) { + logger.debug("Cannot establish connection to {} ({})", ipAddress, unconnectedReason); + } else { + unconnectedReason = ""; + } + updateStatus(); + } + } + } + + private synchronized void startAutomaticRefresh() { + ScheduledFuture job = pollingJob; + if (job == null || job.isCancelled()) { + pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval, TimeUnit.SECONDS); + } + } + + public ThingStatus getStatus() { + return getThing().getStatus(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // No commands supported - nothing to do + } + + @Override + public void initialize() { + updateStatus(); + ResolBridgeConfiguration configuration = getConfigAs(ResolBridgeConfiguration.class); + ipAddress = configuration.ipAddress; + refreshInterval = configuration.refreshInterval; + password = configuration.password; + startAutomaticRefresh(); + } + + @Override + public void dispose() { + ScheduledFuture job = pollingJob; + if (job != null) { + job.cancel(true); + pollingJob = null; + } + try { + Connection connection = tcpConnection; + if (connection != null) { + connection.disconnect(); + getThing().getThings().stream().forEach(thing -> { + ThingHandler th = thing.getHandler(); + if (th instanceof ResolEmuEMThingHandler) { + ((ResolEmuEMThingHandler) th).stop(); + } + }); + + } + } catch (IOException ioe) { + // we don't care about exceptions on disconnect in dispose + } + } + + Locale getLocale() { + if (localeProvider != null) { + return localeProvider.getLocale(); + } else { + return Locale.getDefault(); + } + } + + /* adapter to react on connection state changes and handle received packets */ + private class ResolConnectorAdapter extends ConnectionAdapter { + @Override + public void connectionStateChanged(@Nullable Connection connection) { + synchronized (ResolBridgeHandler.this) { + if (connection == null) { + isConnected = false; + } else { + ConnectionState connState = connection.getConnectionState(); + if (ConnectionState.CONNECTED.equals(connState)) { + isConnected = true; + } else if (ConnectionState.DISCONNECTED.equals(connState) + || ConnectionState.INTERRUPTED.equals(connState)) { + isConnected = false; + } + logger.debug("Connection state changed to: {}", connState.toString()); + + if (isConnected) { + unconnectedReason = ""; + } else { + unconnectedReason = "TCP connection failed: " + connState.toString(); + } + } + updateStatus(); + } + } + + @Override + public void packetReceived(@Nullable Connection connection, @Nullable Packet packet) { + if (connection == null || packet == null) { + return; + } + Language lang = SpecificationFile.getLanguageForLocale(getLocale()); + boolean packetHandled = false; + String thingType = spec.getSourceDeviceSpec(packet).getName(); // use En here + + thingType = thingType.replace(" [", "-"); + thingType = thingType.replace("]", ""); + thingType = thingType.replace(" #", "-"); + thingType = thingType.replace(" ", "_"); + thingType = thingType.replace("/", "_"); + thingType = thingType.replaceAll("[^A-Za-z0-9_-]+", "_"); + + /* + * It would be nice for the combination of MX and EM devices to filter only those with a peerAddress of + * 0x10, because the MX redelivers the data from the EM to the DFA. + * But the MX is the exception in this case and many other controllers do not redeliver data, so we keep it. + */ + if (logger.isTraceEnabled()) { + logger.trace("Received Data from {} (0x{}/0x{}) naming it {}", + spec.getSourceDeviceSpec(packet).getName(lang), + Integer.toHexString(spec.getSourceDeviceSpec(packet).getSelfAddress()), + Integer.toHexString(spec.getSourceDeviceSpec(packet).getPeerAddress()), thingType); + } + + for (Thing t : getThing().getThings()) { + ResolBaseThingHandler th = (ResolBaseThingHandler) t.getHandler(); + boolean isEM = t instanceof ResolEmuEMThingHandler; + + if (t.getUID().getId().contentEquals(thingType) + || (isEM && th != null && spec.getSourceDeviceSpec(packet) + .getPeerAddress() == ((ResolEmuEMThingHandler) th).getVbusAddress())) { + if (th != null) { + th.packetReceived(spec, lang, packet); + packetHandled = true; + } + } + } + ResolDeviceDiscoveryService discovery = discoveryService; + if (!packetHandled && scanning && discovery != null) { + // register the seen device + discovery.addThing(getThing().getUID(), ResolBindingConstants.THING_ID_DEVICE, thingType, + spec.getSourceDeviceSpec(packet).getName(lang)); + } + } + } + + public void startScan() { + scanning = true; + } + + public void stopScan() { + scanning = false; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolEmuEMThingHandler.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolEmuEMThingHandler.java new file mode 100644 index 0000000000000..d2b7449ba4a5c --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolEmuEMThingHandler.java @@ -0,0 +1,298 @@ +/** + * Copyright (c) 2010-2021 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.resol.handler; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.internal.ResolEmuEMConfiguration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +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.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.resol.vbus.Connection; +import de.resol.vbus.Connection.ConnectionState; +import de.resol.vbus.Packet; +import de.resol.vbus.Specification; +import de.resol.vbus.SpecificationFile.Language; +import de.resol.vbus.deviceemulators.EmDeviceEmulator; + +/** + * The {@link ResolEmuEMThingHandler} is responsible for emulating a EM device + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolEmuEMThingHandler extends ResolBaseThingHandler implements PropertyChangeListener { + public static final String CHANNEL_RELAY = "relay_"; + public static final String CHANNEL_TEMP = "temperature_"; + public static final String CHANNEL_RESIST = "resistor_"; + public static final String CHANNEL_SWITCH = "switch_"; + public static final String CHANNEL_TEMP_ADJUST = "bas_temp_adjust_"; + public static final String CHANNEL_MODE = "bas_mode_"; + + private final Logger logger = LoggerFactory.getLogger(ResolEmuEMThingHandler.class); + + private int vbusAddress = 0x6650; + private int deviceId = 1; + private @Nullable EmDeviceEmulator device; + + private @Nullable ResolBridgeHandler bridgeHandler; + + private static class BasSetting { + float temperatureOffset = 0.0f; + int mode = 4; + } + + private BasSetting[] basValues = { new BasSetting(), new BasSetting(), new BasSetting(), new BasSetting(), + new BasSetting(), new BasSetting() }; + private long lastTime = System.currentTimeMillis(); + + // Background Runnable + private @Nullable ScheduledFuture updateJob; + + public ResolEmuEMThingHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + ResolEmuEMConfiguration configuration = getConfigAs(ResolEmuEMConfiguration.class); + deviceId = configuration.deviceId; + vbusAddress = 0x6650 + deviceId; + + bridgeHandler = getBridgeHandler(); + registerResolThingListener(bridgeHandler); + } + + @Override + public void dispose() { + EmDeviceEmulator dev = device; + ScheduledFuture job = updateJob; + if (job != null) { + job.cancel(true); + } + if (dev != null) { + dev.stop(); + dev.removePropertyChangeListener(this); + } + } + + private void updateRunnable() { + EmDeviceEmulator d = device; + if (d != null) { + long now = System.currentTimeMillis(); + int diff = (int) (now - lastTime); + lastTime = now; + + d.update(diff); + } + } + + private void startAutomaticUpdate() { + ScheduledFuture job = updateJob; + if (job == null || job.isCancelled()) { + updateJob = scheduler.scheduleWithFixedDelay(this::updateRunnable, 0, 1, TimeUnit.SECONDS); + } + } + + private synchronized @Nullable ResolBridgeHandler getBridgeHandler() { + Bridge bridge = getBridge(); + if (bridge == null) { + logger.debug("Required bridge not defined for thing {}.", thing.getThingTypeUID()); + return null; + } else { + return getBridgeHandler(bridge); + } + } + + private synchronized @Nullable ResolBridgeHandler getBridgeHandler(Bridge bridge) { + ResolBridgeHandler bridgeHandler = null; + + ThingHandler handler = bridge.getHandler(); + if (handler instanceof ResolBridgeHandler) { + bridgeHandler = (ResolBridgeHandler) handler; + } else { + logger.debug("No available bridge handler found yet. Bridge: {} .", bridge.getUID()); + } + return bridgeHandler; + } + + private void registerResolThingListener(@Nullable ResolBridgeHandler bridgeHandler) { + if (bridgeHandler != null) { + bridgeHandler.registerResolThingListener(this); + } else { + logger.debug("Can't register {} at bridge as bridgeHandler is null.", this.getThing().getUID()); + } + } + + public int getVbusAddress() { + return vbusAddress; + } + + public void useConnection(Connection connection) { + EmDeviceEmulator device = this.device; + if (device != null) { + device.stop(); + device.removePropertyChangeListener(this); + } + device = new EmDeviceEmulator(connection, deviceId); + this.device = device; + device.addPropertyChangeListener(this); + device.start(); + for (int i = 1; i <= 5; i++) { + setRelayChannelValue(i, device.getRelayValueByNr(i)); + } + startAutomaticUpdate(); + } + + public void stop() { + EmDeviceEmulator device = this.device; + if (device != null) { + device.stop(); + } + ScheduledFuture updateJob = this.updateJob; + if (updateJob != null) { + updateJob.cancel(false); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String chID = channelUID.getId(); + int channel = chID.charAt(chID.length() - 1) - '0'; + float value = 0; + int intValue = 0; + + if (command instanceof QuantityType) { + value = Objects.requireNonNullElse(((QuantityType) command).toUnit(SIUnits.CELSIUS), + new QuantityType<>(888.8, SIUnits.CELSIUS)).floatValue(); + } else if (command instanceof OnOffType) { + intValue = ((OnOffType) command).equals(OnOffType.ON) ? 1 : 0; + } else if (command instanceof DecimalType) { + intValue = ((DecimalType) command).intValue(); + value = intValue; + } else { + /* nothing to do */ + return; + } + + EmDeviceEmulator dev = device; + if (dev != null) { + if (chID.startsWith(CHANNEL_TEMP)) { + dev.setResistorValueByNrAndPt1000Temperatur(channel, value); + updateState(channelUID, new QuantityType<>(value, SIUnits.CELSIUS)); + } else if (chID.startsWith(CHANNEL_SWITCH)) { + if (intValue == 0) { + /* switch is open => 1 megaohm */ + dev.setResistorValueByNr(channel, 1000000000); + updateState(channelUID, OnOffType.OFF); + } else { + /* switch is closed */ + dev.setResistorValueByNr(channel, 0); + updateState(channelUID, OnOffType.ON); + } + } else if (chID.startsWith(CHANNEL_RESIST)) { + dev.setResistorValueByNr(channel, (int) (value * 1000.0)); + updateState(channelUID, new QuantityType<>(intValue, Units.OHM)); + } else if (chID.startsWith(CHANNEL_TEMP_ADJUST)) { + basValues[channel - 1].temperatureOffset = value; + updateBas(channel); + updateState(channelUID, new QuantityType<>(value, SIUnits.CELSIUS)); + } else if (chID.startsWith(CHANNEL_MODE)) { + basValues[channel - 1].mode = intValue; + updateBas(channel); + updateState(channelUID, new QuantityType<>(intValue, Units.ONE)); + } else { + /* set resistor value for Open Connection, 1 megaohm */ + dev.setResistorValueByNr(channel, 1000000000); + updateState(channelUID, new QuantityType<>(1000000, Units.OHM)); + } + } + } + + private void updateBas(int channel) { + int resistor = 0; /* in milliohm */ + int delta = (int) ((basValues[channel - 1].temperatureOffset * 210.0f / 15.0f) * 1000.0f); + switch (basValues[channel - 1].mode) { + case 4: /* Automatic range 76 - 496 ohm */ + resistor = 286 * 1000 + delta; + break; + case 0: /* OFF range 1840 - 2260 ohm */ + resistor = 2050 * 1000 + delta; + break; + case 2: /* Night range 660 - 1080 ohm */ + resistor = 870 * 1000 + delta; + break; + case 3: /* Party is automatic mode with +15K */ + resistor = 286 * 1000 + 210 * 1000; + break; + case 1: /* Summer range 1240 - 1660 ohm */ + resistor = 1450 * 1000 + delta; + break; + default: + /* signal a shortcut as error */ + resistor = 0; + break; + } + EmDeviceEmulator device = this.device; + if (device != null) { + device.setResistorValueByNr(channel, resistor); + } + } + + @Override + public void propertyChange(@Nullable PropertyChangeEvent evt) { + if (evt != null) { + String s = evt.getPropertyName(); + if (s.startsWith("relay") && s.endsWith("Value")) { + int v = (Integer) evt.getNewValue(); + int i = Integer.parseInt(s.substring(5, 6)); + setRelayChannelValue(i, v); + } else if (s.contentEquals("connectionState")) { + ConnectionState ste = (ConnectionState) evt.getNewValue(); + if (ste.equals(ConnectionState.CONNECTED)) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ste.toString()); + } + } + } + } + + private void setRelayChannelValue(int relay, double value) { + String channelId = CHANNEL_RELAY + relay; + updateState(channelId, new DecimalType(value)); + } + + @Override + public void packetReceived(Specification spec, Language lang, Packet packet) { + /* nothing to do here */ + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolThingHandler.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolThingHandler.java new file mode 100644 index 0000000000000..f57afd763492a --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/handler/ResolThingHandler.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2010-2021 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.resol.handler; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.internal.ResolBindingConstants; +import org.openhab.binding.resol.internal.ResolStateDescriptionOptionProvider; +import org.openhab.binding.resol.internal.providers.ResolChannelTypeProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.resol.vbus.Packet; +import de.resol.vbus.Specification; +import de.resol.vbus.Specification.PacketFieldSpec; +import de.resol.vbus.Specification.PacketFieldValue; +import de.resol.vbus.SpecificationFile; +import de.resol.vbus.SpecificationFile.Enum; +import de.resol.vbus.SpecificationFile.EnumVariant; +import de.resol.vbus.SpecificationFile.Language; + +/** + * The {@link ResolThingHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolThingHandler extends ResolBaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(ResolThingHandler.class); + + private ResolStateDescriptionOptionProvider stateDescriptionProvider; + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat( + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS_GENERAL); + + static { + synchronized (DATE_FORMAT) { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + } + + public ResolThingHandler(Thing thing, ResolStateDescriptionOptionProvider stateDescriptionProvider) { + super(thing); + this.stateDescriptionProvider = stateDescriptionProvider; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + /* we ignore the commands for now on purpose */ + } + + @Override + public void initialize() { + ResolBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null) { + updateStatus(bridgeHandler.getStatus()); + } + } + + private synchronized @Nullable ResolBridgeHandler getBridgeHandler() { + Bridge bridge = getBridge(); + if (bridge == null) { + logger.debug("Required bridge not defined for device."); + return null; + } else { + return getBridgeHandler(bridge); + } + } + + private synchronized @Nullable ResolBridgeHandler getBridgeHandler(Bridge bridge) { + ResolBridgeHandler bridgeHandler = null; + + ThingHandler handler = bridge.getHandler(); + if (handler instanceof ResolBridgeHandler) { + bridgeHandler = (ResolBridgeHandler) handler; + } else { + logger.debug("No available bridge handler found yet. Bridge: {} .", bridge.getUID()); + } + return bridgeHandler; + } + + @Override + protected void packetReceived(Specification spec, Language lang, Packet packet) { + PacketFieldValue[] pfvs = spec.getPacketFieldValuesForHeaders(new Packet[] { packet }); + for (PacketFieldValue pfv : pfvs) { + logger.trace("Id: {}, Name: {}, Raw: {}, Text: {}", pfv.getPacketFieldId(), pfv.getName(lang), + pfv.getRawValueDouble(), pfv.formatTextValue(null, Locale.getDefault())); + + String channelId = pfv.getName(); // use English name as channel + channelId = channelId.replace(" [", "-"); + channelId = channelId.replace("]", ""); + channelId = channelId.replace("(", "-"); + channelId = channelId.replace(")", ""); + channelId = channelId.replace(" #", "-"); + channelId = channelId.replaceAll("[^A-Za-z0-9_-]+", "_"); + + channelId = channelId.toLowerCase(Locale.ENGLISH); + + ChannelTypeUID channelTypeUID; + + if (pfv.getPacketFieldSpec().getUnit().getUnitId() >= 0) { + channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, + pfv.getPacketFieldSpec().getUnit().getUnitCodeText()); + } else if (pfv.getPacketFieldSpec().getType() == SpecificationFile.Type.DateTime) { + channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "DateTime"); + } else { + /* used for enums and the numeric types without unit */ + channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "None"); + } + + String acceptedItemType; + + Thing thing = getThing(); + switch (pfv.getPacketFieldSpec().getType()) { + case DateTime: + acceptedItemType = "DateTime"; + break; + case WeekTime: + case Number: + acceptedItemType = ResolChannelTypeProvider.itemTypeForUnit(pfv.getPacketFieldSpec().getUnit()); + break; + case Time: + default: + acceptedItemType = "String"; + break; + } + Channel a = thing.getChannel(channelId); + + if (a == null) { + /* channel doesn't exit, let's create it */ + ThingBuilder thingBuilder = editThing(); + ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); + + if (pfv.getEnumVariant() != null) { + /* create a state option channel */ + List options = new ArrayList<>(); + PacketFieldSpec ff = pfv.getPacketFieldSpec(); + Enum e = ff.getEnum(); + for (long l : e.getValues()) { + EnumVariant v = e.getEnumVariantForValue(l); + options.add(new StateOption(Long.toString(l), v.getText(lang))); + } + + stateDescriptionProvider.setStateOptions(channelUID, options); + + Channel channel = ChannelBuilder.create(channelUID, "Number").withType(channelTypeUID) + .withLabel(pfv.getName(lang)).build(); + + thingBuilder.withChannel(channel).withLabel(thing.getLabel()); + updateThing(thingBuilder.build()); + } else if (pfv.getRawValueDouble() != null) { + /* a number channel */ + Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID) + .withLabel(pfv.getName(lang)).build(); + + thingBuilder.withChannel(channel).withLabel(thing.getLabel()); + updateThing(thingBuilder.build()); + } + logger.debug("Creating channel: {}", channelUID); + } + + if (pfv.getEnumVariant() != null) { + /* update the enum / State channel */ + this.updateState(channelId, new StringType(Long.toString(pfv.getRawValueLong()))); + + } else { + switch (pfv.getPacketFieldSpec().getType()) { + case Number: + Double dd = pfv.getRawValueDouble(); + if (dd != null) { + if (!isSpecialValue(dd)) { + /* only set the value if no error occurred */ + + String str = pfv.formatText(); + if (str.endsWith("RH")) { + /* unit %RH for relative humidity is not known in openHAB UoM, so we remove it */ + str = str.substring(0, str.length() - 2); + } + if (str.endsWith("Ω")) { + QuantityType q = new QuantityType<>(dd, Units.OHM); + this.updateState(channelId, q); + } else { + try { + QuantityType q = new QuantityType<>(str); + this.updateState(channelId, q); + } catch (IllegalArgumentException e) { + logger.debug("unit of '{}' unknown in openHAB", str); + QuantityType q = new QuantityType<>(dd.toString()); + this.updateState(channelId, q); + } + } + } + } + /* + * else { + * field not available in this packet, e. g. old firmware version not (yet) transmitting it + * } + */ + break; + case DateTime: + synchronized (DATE_FORMAT) { + DateTimeType d = new DateTimeType(DATE_FORMAT.format(pfv.getRawValueDate())); + this.updateState(channelId, d); + } + break; + case WeekTime: + case Time: + default: + Bridge b = getBridge(); + if (b != null) { + String value = pfv.formatTextValue(pfv.getPacketFieldSpec().getUnit(), + ((ResolBridgeHandler) b).getLocale()); + try { + QuantityType q = new QuantityType<>(value); + this.updateState(channelId, q); + } catch (IllegalArgumentException e) { + this.updateState(channelId, new StringType(value)); + logger.debug("unit of '{}' unknown in openHAB, using string", value); + } + } + } + } + } + } + + /* check if the given value is a special one like 888.8 or 999.9 for shortcut or open load on a sensor wire */ + private boolean isSpecialValue(Double dd) { + if ((Math.abs(dd - 888.8) < 0.1) || (Math.abs(dd - (-888.8)) < 0.1)) { + /* value out of range */ + return true; + } + if (Math.abs(dd - 999.9) < 0.1) { + /* sensor not reachable */ + return true; + } + return false; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBindingConstants.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBindingConstants.java new file mode 100644 index 0000000000000..8fc78a9f3d3d0 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBindingConstants.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link ResolBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolBindingConstants { + + private static final String BRIDGE_VBUSLAN = "vbuslan"; + + public static final String BINDING_ID = "resol"; + + // List of all ChannelTypeUIDs is empty, as we got totally rid of static channel types. + // ChannelTypeUIDs are constructed from the BINDING_ID and the UnitCodeTextIndex from the VSF + + // List of all Thing Type + public static final String THING_ID_DEVICE = "device"; + public static final String THING_ID_EMU_EM = "emulatedEM"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_UID_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE_VBUSLAN); + + public static final ThingTypeUID THING_TYPE_UID_DEVICE = new ThingTypeUID(BINDING_ID, THING_ID_DEVICE); + + public static final ThingTypeUID THING_TYPE_UID_EMU_EM = new ThingTypeUID(BINDING_ID, THING_ID_EMU_EM); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_UID_BRIDGE, + THING_TYPE_UID_DEVICE, THING_TYPE_UID_EMU_EM); + + public static final Set SUPPORTED_BRIDGE_THING_TYPES_UIDS = Set.of(THING_TYPE_UID_BRIDGE); +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBridgeConfiguration.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBridgeConfiguration.java new file mode 100644 index 0000000000000..1a9f4383f16bb --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolBridgeConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ResolBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolBridgeConfiguration { + + public String ipAddress = ""; + public String password = "vbus"; + public Integer port = 7053; + public String adapterSerial = ""; + public Integer refreshInterval = 300; +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolEmuEMConfiguration.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolEmuEMConfiguration.java new file mode 100644 index 0000000000000..eddc5550dd1a7 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolEmuEMConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ResolEmuEMConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolEmuEMConfiguration { + public Integer deviceId = 1; +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolHandlerFactory.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolHandlerFactory.java new file mode 100644 index 0000000000000..fc1c33b59eb72 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolHandlerFactory.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal; + +import static org.openhab.binding.resol.internal.ResolBindingConstants.SUPPORTED_THING_TYPES_UIDS; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.handler.ResolBridgeHandler; +import org.openhab.binding.resol.handler.ResolEmuEMThingHandler; +import org.openhab.binding.resol.handler.ResolThingHandler; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link ResolHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.resol", service = ThingHandlerFactory.class) +public class ResolHandlerFactory extends BaseThingHandlerFactory { + + private final LocaleProvider localeProvider; + + private final ResolStateDescriptionOptionProvider stateDescriptionProvider; + + @Activate + public ResolHandlerFactory(final @Reference ResolStateDescriptionOptionProvider stateDescriptionProvider, + final @Reference LocaleProvider localeProvider) { + this.stateDescriptionProvider = stateDescriptionProvider; + this.localeProvider = localeProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(ResolBindingConstants.THING_TYPE_UID_DEVICE)) { + return new ResolThingHandler(thing, stateDescriptionProvider); + } + + if (thingTypeUID.equals(ResolBindingConstants.THING_TYPE_UID_EMU_EM)) { + return new ResolEmuEMThingHandler(thing); + } + + if (thingTypeUID.equals(ResolBindingConstants.THING_TYPE_UID_BRIDGE)) { + return new ResolBridgeHandler((Bridge) thing, localeProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolStateDescriptionOptionProvider.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolStateDescriptionOptionProvider.java new file mode 100644 index 0000000000000..ad0955cad5a30 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/ResolStateDescriptionOptionProvider.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Dynamic provider of state options for the Resol binding. + * + * @author Raphael Mack - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, ResolStateDescriptionOptionProvider.class }) +@NonNullByDefault +public class ResolStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider { + @Reference + protected void setChannelTypeI18nLocalizationService( + final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } + + protected void unsetChannelTypeI18nLocalizationService( + final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.channelTypeI18nLocalizationService = null; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolDeviceDiscoveryService.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolDeviceDiscoveryService.java new file mode 100644 index 0000000000000..e486ffb70d159 --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolDeviceDiscoveryService.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal.discovery; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.handler.ResolBridgeHandler; +import org.openhab.binding.resol.internal.ResolBindingConstants; +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.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ResolDeviceDiscoveryService} class handles the discovery of things. + * + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class ResolDeviceDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + + private static final String THING_PROPERTY_TYPE = "type"; + + private final Logger logger = LoggerFactory.getLogger(ResolDeviceDiscoveryService.class); + + private @Nullable ResolBridgeHandler resolBridgeHandler; + + public ResolDeviceDiscoveryService() throws IllegalArgumentException { + super(Set.of(ResolBindingConstants.THING_TYPE_UID_DEVICE), 15, false); + } + + public void addThing(ThingUID bridgeUID, String thingType, String type, String name) { + logger.trace("Adding new Resol thing: {}", type); + ThingUID thingUID = null; + switch (thingType) { + case ResolBindingConstants.THING_ID_DEVICE: + thingUID = new ThingUID(ResolBindingConstants.THING_TYPE_UID_DEVICE, bridgeUID, type); + break; + } + + if (thingUID != null) { + logger.trace("Adding new Discovery thingType: {} bridgeType: {}", thingUID.getAsString(), + bridgeUID.getAsString()); + + Map properties = new HashMap<>(1); + properties.put(THING_PROPERTY_TYPE, type); + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withRepresentationProperty(THING_PROPERTY_TYPE).withProperties(properties).withLabel(name).build(); + logger.trace("call register: {} label: {}", discoveryResult.getBindingId(), discoveryResult.getLabel()); + thingDiscovered(discoveryResult); + } else { + logger.debug("Discovered Thing is unsupported: type '{}'", type); + } + } + + @Override + public void activate() { + ResolBridgeHandler resolBridgeHandler = this.resolBridgeHandler; + if (resolBridgeHandler != null) { + resolBridgeHandler.registerDiscoveryService(this); + } + } + + @Override + public void deactivate() { + ResolBridgeHandler resolBridgeHandler = this.resolBridgeHandler; + if (resolBridgeHandler != null) { + resolBridgeHandler.unregisterDiscoveryService(); + } + } + + @Override + protected void startScan() { + ResolBridgeHandler resolBridgeHandler = this.resolBridgeHandler; + if (resolBridgeHandler != null) { + resolBridgeHandler.startScan(); + } + } + + @Override + protected void stopScan() { + ResolBridgeHandler resolBridgeHandler = this.resolBridgeHandler; + if (resolBridgeHandler != null) { + resolBridgeHandler.stopScan(); + } + super.stopScan(); + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof ResolBridgeHandler) { + resolBridgeHandler = (ResolBridgeHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return resolBridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolVBusBridgeDiscovery.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolVBusBridgeDiscovery.java new file mode 100644 index 0000000000000..02448864376ee --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/discovery/ResolVBusBridgeDiscovery.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal.discovery; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Future; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.internal.ResolBindingConstants; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.resol.vbus.TcpDataSource; +import de.resol.vbus.TcpDataSourceProvider; + +/** + * The {@link ResolVBusBridgeDiscovery} class provides the DiscoverySerivce to + * discover Resol VBus-LAN adapters + * + * @author Raphael Mack - Initial contribution + */ +@Component(service = DiscoveryService.class) +@NonNullByDefault +public class ResolVBusBridgeDiscovery extends AbstractDiscoveryService { + public static final String THING_PROPERTY_IPADDRESS = "ipAddress"; + public static final String THING_PROPERTY_PORT = "port"; + public static final String THING_PROPERTY_ADAPTER_SERIAL = "adapterSerial"; + + private final Logger logger = LoggerFactory.getLogger(ResolVBusBridgeDiscovery.class); + + private volatile boolean discoveryRunning = false; + private @Nullable Future searchFuture; + + public ResolVBusBridgeDiscovery() throws IllegalArgumentException { + super(ResolBindingConstants.SUPPORTED_BRIDGE_THING_TYPES_UIDS, 35, false); + } + + @Override + protected void startScan() { + discoveryRunning = true; + searchFuture = scheduler.submit(this::searchRunnable); + } + + @Override + protected void stopScan() { + discoveryRunning = false; + if (searchFuture != null) { + searchFuture.cancel(true); + } + } + + /* + * The runnable for the search routine. + */ + public void searchRunnable() { + try { + InetAddress broadcastAddress = InetAddress + .getByAddress(new byte[] { (byte) 255, (byte) 255, (byte) 255, (byte) 255 }); + + TcpDataSource[] dataSources = TcpDataSourceProvider.discoverDataSources(broadcastAddress, 3, 500, false); + + Map currentDataSourceById = new HashMap(); + for (TcpDataSource ds : dataSources) { + if (!discoveryRunning) { + break; + } + InetAddress address = ds.getAddress(); + String addressId = address.getHostAddress(); + TcpDataSource dsWithInfo; + try { + dsWithInfo = TcpDataSourceProvider.fetchInformation(address, 1500); + logger.trace("Discovered Resol VBus-LAN interface @{} {} ({})", addressId, + dsWithInfo.getDeviceName(), dsWithInfo.getSerial()); + + currentDataSourceById.put(addressId, dsWithInfo); + addAdapter(addressId, dsWithInfo); + // here we can add the detection of Multi-Channel interfaces like DL3 + } catch (InterruptedIOException ex) { + /* openHAB interrupted the io thread and wants to shutdown */ + break; + } catch (IOException ex) { + /* address is no valid adapter */ + } + + } + } catch (UnknownHostException e) { + logger.debug("Could not resolve IPv4 broadcast address"); + } + } + + private void addAdapter(String remoteIP, TcpDataSource dsWithInfo) { + String adapterSerial = dsWithInfo.getSerial(); + Map properties = new HashMap<>(3); + properties.put(THING_PROPERTY_IPADDRESS, remoteIP); + properties.put(THING_PROPERTY_PORT, dsWithInfo.getLivePort()); + properties.put(THING_PROPERTY_ADAPTER_SERIAL, adapterSerial); + + ThingUID uid = new ThingUID(ResolBindingConstants.THING_TYPE_UID_BRIDGE, adapterSerial); + thingDiscovered(DiscoveryResultBuilder.create(uid).withRepresentationProperty(THING_PROPERTY_IPADDRESS) + .withProperties(properties).withLabel(dsWithInfo.getName()).build()); + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/providers/ResolChannelTypeProvider.java b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/providers/ResolChannelTypeProvider.java new file mode 100644 index 0000000000000..f51975b428ddb --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/java/org/openhab/binding/resol/internal/providers/ResolChannelTypeProvider.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2021 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.resol.internal.providers; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.resol.internal.ResolBindingConstants; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeProvider; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.osgi.service.component.annotations.Component; + +import de.resol.vbus.Specification; +import de.resol.vbus.SpecificationFile.Unit; + +/** + * @author Raphael Mack - Initial Contribution + * + */ +@Component(service = { ChannelTypeProvider.class, ResolChannelTypeProvider.class }) +@NonNullByDefault +public class ResolChannelTypeProvider implements ChannelTypeProvider { + private Map channelTypes = new ConcurrentHashMap(); + + public ResolChannelTypeProvider() { + // let's add all channel types from known by the resol-vbus java library + + Specification spec = Specification.getDefaultSpecification(); + + Unit[] units = spec.getUnits(); + for (Unit u : units) { + ChannelTypeUID channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, u.getUnitCodeText()); + + // maybe we could use pfv.getPacketFieldSpec().getPrecision() here + int precision = 1; + if (u.getUnitId() >= 0) { + ChannelType ctype = ChannelTypeBuilder + .state(channelTypeUID, u.getUnitFamily().toString(), itemTypeForUnit(u)) + .withStateDescriptionFragment(StateDescriptionFragmentBuilder.create() + .withPattern("%." + precision + "f " + u.getUnitTextText().replace("%", "%%")) + .withReadOnly(true).build()) + .build(); + + channelTypes.put(channelTypeUID, ctype); + } + } + } + + @Override + public Collection getChannelTypes(@Nullable Locale locale) { + return channelTypes.values(); + } + + @Override + public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) { + if (channelTypes.containsKey(channelTypeUID)) { + return channelTypes.get(channelTypeUID); + } else { + return null; + } + } + + public static String itemTypeForUnit(Unit u) { + String itemType = "Number"; + switch (u.getUnitFamily()) { + case Temperature: + itemType += ":Temperature"; + break; + case Energy: + itemType += ":Energy"; + break; + case VolumeFlow: + itemType += ":VolumetricFlowRate"; + break; + case Pressure: + itemType += ":Pressure"; + break; + case Volume: + itemType += ":Volume"; + break; + case Time: + itemType += ":Time"; + break; + case Power: + itemType += ":Power"; + break; + case None: + switch (u.getUnitCodeText()) { + case "Hertz": + itemType += ":Frequency"; + break; + case "Hectopascals": + itemType += ":Pressure"; + break; + case "MetersPerSecond": + itemType += ":Speed"; + break; + case "Milliamperes": + itemType += ":ElectricCurrent"; + break; + case "Milliseconds": + itemType += ":Time"; + break; + case "Ohms": + itemType += ":ElectricResistance"; + break; + case "Percent": + itemType += ":Dimensionless"; + break; + case "PercentRelativeHumidity": + itemType += ":Dimensionless"; + break; + case "Volts": + itemType += ":ElectricPotential"; + break; + case "WattsPerSquareMeter": + itemType += ":Intensity"; + break; + } + break; + default: + } + return itemType; + } +} diff --git a/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..23e95b0feb38e --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Resol Binding + This is the binding for Resol solar and system controllers (including branded versions). + + diff --git a/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/i18n/resol_de.properties b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/i18n/resol_de.properties new file mode 100644 index 0000000000000..bebe6480f7a6c --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/i18n/resol_de.properties @@ -0,0 +1,80 @@ +# binding +binding.resol.name = Resol Binding +binding.resol.description = Verbindet Solar- und Systemregler des Herstellers Resol und weitere, die für andere Marken von Resol produziert werden. + +# thing types +thing-type.resol.device.label = Resol Gerät +thing-type.resol.device.description = Solar- oder Systemregler oder ein anderes Gerät welches mit dem Resol VBus verbunden ist. +thing-type.resol.emulatedEM.label = Emuliertes EM Modul +thing-type.resol.emulatedEM.description = Emulation eines Erweiterungs-Modules (EM) welches über den VBus an dafür ausgelegten Resol Reglern angebunden wird. Es ersetzt ein physikalisch vorhandenes EM durch openHAB. +thing-type.resol.emulatedEM.channel.relay_1.label = Relais Status 1 +thing-type.resol.emulatedEM.channel.relay_2.label = Relais Status 2 +thing-type.resol.emulatedEM.channel.relay_3.label = Relais Status 3 +thing-type.resol.emulatedEM.channel.relay_4.label = Relais Status 4 +thing-type.resol.emulatedEM.channel.relay_5.label = Relais Status 5 +thing-type.resol.emulatedEM.channel.switch_1.label = Schalter 1 +thing-type.resol.emulatedEM.channel.switch_2.label = Schalter 2 +thing-type.resol.emulatedEM.channel.switch_3.label = Schalter 3 +thing-type.resol.emulatedEM.channel.switch_4.label = Schalter 4 +thing-type.resol.emulatedEM.channel.switch_5.label = Schalter 5 +thing-type.resol.emulatedEM.channel.switch_6.label = Schalter 6 +thing-type.resol.emulatedEM.channel.temperature_1.label = Temperatur 1 +thing-type.resol.emulatedEM.channel.temperature_2.label = Temperatur 2 +thing-type.resol.emulatedEM.channel.temperature_3.label = Temperatur 3 +thing-type.resol.emulatedEM.channel.temperature_4.label = Temperatur 4 +thing-type.resol.emulatedEM.channel.temperature_5.label = Temperatur 5 +thing-type.resol.emulatedEM.channel.temperature_6.label = Temperatur 6 +thing-type.resol.emulatedEM.channel.resistor_1.label = Widerstand 1 +thing-type.resol.emulatedEM.channel.resistor_2.label = Widerstand 2 +thing-type.resol.emulatedEM.channel.resistor_3.label = Widerstand 3 +thing-type.resol.emulatedEM.channel.resistor_4.label = Widerstand 4 +thing-type.resol.emulatedEM.channel.resistor_5.label = Widerstand 5 +thing-type.resol.emulatedEM.channel.resistor_6.label = Widerstand 6 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_1.label = Temperatur Anpassung 1 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_2.label = Temperatur Anpassung 2 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_3.label = Temperatur Anpassung 3 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_4.label = Temperatur Anpassung 4 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_5.label = Temperatur Anpassung 5 +thing-type.resol.emulatedEM.channel.bas_temp_adjust_6.label = Temperatur Anpassung 6 +thing-type.resol.emulatedEM.channel.bas_mode_1.label = Betriebsart 1 +thing-type.resol.emulatedEM.channel.bas_mode_2.label = Betriebsart 2 +thing-type.resol.emulatedEM.channel.bas_mode_3.label = Betriebsart 3 +thing-type.resol.emulatedEM.channel.bas_mode_4.label = Betriebsart 4 +thing-type.resol.emulatedEM.channel.bas_mode_5.label = Betriebsart 5 +thing-type.resol.emulatedEM.channel.bas_mode_6.label = Betriebsart 6 +thing-type.resol.vbuslan.label = VBusLAN Adapter +thing-type.resol.vbuslan.description = Diese bridge verwendet ein Gerät mit TCP/IP live port als Schnittstelle zum Resol VBus. Dies kann als eigenständiger VBus-LAN Adapter aber auch ein andere Gerät mit integriertem VBus live port wie einem KM2, DL2/3 oder sonstiges sein. +. +# thing type config description +# thing-type.config.resol.device does not have configuration parameters +thing-type.config.resol.emulatedEM.deviceId.label = Modul ID +thing-type.config.resol.emulatedEM.deviceId.description = Subaddress des emulierten EM Moduls. Der verwendbare Bereich hängt vom verwendeten Regler ab. +thing-type.config.resol.vbuslan.ipAddress.label = IP-Adresse +thing-type.config.resol.vbuslan.ipAddress.description = IP-Adresse der VBus-LAN Schnittstelle. +thing-type.config.resol.vbuslan.port.label = Live Data Port +thing-type.config.resol.vbuslan.port.description = TCP-Port der Live-Data-Schnittstelle des VBus Gateways. +thing-type.config.resol.vbuslan.adapterSerial.label = Adapter Seriennummer +thing-type.config.resol.vbuslan.adapterSerial.description = Seriennummer des VBus-LAN-Schnittstellengerätes (nur zur Information). +thing-type.config.resol.vbuslan.password.label = Passwort +thing-type.config.resol.vbuslan.password.description = Passwort für die VBus-LAN Schnittstelle. +thing-type.config.resol.vbuslan.refreshInterval.label = Aktualisierungsintervall +thing-type.config.resol.vbuslan.refreshInterval.description = Zeitintervall in Sekunden um die Verbindung zur VBus-LAN-Schnittstelle zu prüfen. Die Aktualisierung der Daten geschieht unabhängig hiervon sobald sie auf dem VBus empfangen werden. + +# channel types +channel-type.resol.relay.label = Relais Zustand +channel-type.resol.relay.description = Virtueller Relay Ausgang welcher vom Regler gesetzt wird und verwendet werden kann um den Zustand vom Resol Regler zu openHAB zu übertragen. +channel-type.resol.temperature.label = Temperatur +channel-type.resol.temperature.description = Virtueller Temperatur Sensor Eingang. +channel-type.resol.resistance.label = Widerstand +channel-type.resol.resistance.description = Virtueller Widerstandseingang. +channel-type.resol.switch.label = Schalter +channel-type.resol.switch.description = Virtueller Schaltereingang. +channel-type.resol.temperatureAdjust.label = Temperatur Anpassung +channel-type.resol.temperatureAdjust.description = Virtueller Eingang zur Parallelverschiebung der Temperatur eines Heizkreises an einem emulierten BAS (Raumbediengerät RCP12). +channel-type.resol.operationmode.label = Betriebsart +channel-type.resol.operationmode.description = Virtueller Eingang zur Betriebsartumschaltung eines Heizkreises an einem emulierten BAS (Raumbediengerät RCP12). +channel-type.resol.operationmode.state.option.0 = Aus +channel-type.resol.operationmode.state.option.1 = Sommer +channel-type.resol.operationmode.state.option.2 = Nacht +channel-type.resol.operationmode.state.option.3 = Party +channel-type.resol.operationmode.state.option.4 = Automatik diff --git a/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..98c8d90b08bfa --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,41 @@ + + + + + This bridge represents the Resol VBus-LAN adapter which can be any device with a TCP/IP live port, either + the standalone device VBus-LAN Adapter, KM2, DL2/3 or similar. + + ipAddress + + + + network-address + + The IP address of the of the VBus-LAN gateway. + + + + Port for live data on the VBUS-LAN gateway. + 7053 + + + + The serial number of the adapter (informative). + + + + The password for the VBusLAN connection. + password + + + + Refresh time in seconds to check the connection to the VBus gateway. Data updates are propagated to + openHAB independently from this setting as soon as they are received on the VBus. + 300 + + + + diff --git a/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..f17db0e122cea --- /dev/null +++ b/bundles/org.openhab.binding.resol/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,212 @@ + + + + + + + + + + Solar or system controller (or any other real device on the VBus) from Resol. + + + + + + + + + Emulation of an Extension Module (EM) device which can be connected through the VBUS to Resol controllers + which support the EM devices. Replaces a physically available EM by openHAB. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The (sub-)address of the emulated EM device, usable range depends on the used controller. + 1 + + + + + + + Number + + + + + + Number + + + + + + Number:Dimensionless + + Virtual relay output, will be set by the controller and can be used to communicate data from the Resol + controller to openHAB. + Switch + + + + + Number:Temperature + + Virtual temperature sensor input. + Temperature + + + + + Number:ElectricResistance + + Virtual resistance input. + + + + + Switch + + Virtual switch input. + + + + Number:Temperature + + Virtual temperature offset on heating circuit of an emulated BAS (RCP12 room control unit). + Temperature + + + + + Number + + Virtual operating mode of the heating circuit controlled by the emulated BAS (RCP12 room control unit). + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java index ffb61d589a581..5ecc29d266818 100644 --- a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java +++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java @@ -17,7 +17,6 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.jetbrains.annotations.NotNull; import org.openhab.binding.revogi.internal.udp.UdpResponseDTO; import org.openhab.binding.revogi.internal.udp.UdpSenderService; import org.slf4j.Logger; @@ -56,7 +55,6 @@ public CompletableFuture queryStatus(String serialNumber, String ipAd return responses.thenApply(this::getStatus); } - @NotNull private StatusDTO getStatus(final List singleResponse) { return singleResponse.stream() .filter(response -> !response.getAnswer().isEmpty() && response.getAnswer().contains(VERSION_STRING)) diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java index 042bfeccba8eb..2f7ac6817b634 100644 --- a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java +++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java @@ -17,7 +17,6 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.jetbrains.annotations.NotNull; import org.openhab.binding.revogi.internal.udp.UdpResponseDTO; import org.openhab.binding.revogi.internal.udp.UdpSenderService; import org.slf4j.Logger; @@ -66,7 +65,6 @@ public CompletableFuture switchPort(String serialNumber, Stri return responses.thenApply(this::getSwitchResponse); } - @NotNull private SwitchResponseDTO getSwitchResponse(final List singleResponse) { return singleResponse.stream().filter(response -> !response.getAnswer().isEmpty()) .map(response -> deserializeString(response.getAnswer())) diff --git a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting2Message.java b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting2Message.java index 5c660cd620cd4..2c84bcbf71b6a 100644 --- a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting2Message.java +++ b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting2Message.java @@ -18,6 +18,7 @@ import static org.openhab.binding.rfxcom.internal.messages.RFXComLighting2Message.Commands.*; import java.math.BigDecimal; +import java.math.RoundingMode; import org.openhab.binding.rfxcom.internal.exceptions.RFXComException; import org.openhab.binding.rfxcom.internal.exceptions.RFXComUnsupportedChannelException; @@ -155,7 +156,7 @@ public String getDeviceId() { */ public static int getDimLevelFromPercentType(PercentType pt) { return pt.toBigDecimal().multiply(BigDecimal.valueOf(15)) - .divide(PercentType.HUNDRED.toBigDecimal(), 0, BigDecimal.ROUND_UP).intValue(); + .divide(PercentType.HUNDRED.toBigDecimal(), 0, RoundingMode.UP).intValue(); } /** @@ -168,7 +169,7 @@ public static PercentType getPercentTypeFromDimLevel(int value) { value = Math.min(value, 15); return new PercentType(BigDecimal.valueOf(value).multiply(BigDecimal.valueOf(100)) - .divide(BigDecimal.valueOf(15), 0, BigDecimal.ROUND_UP).intValue()); + .divide(BigDecimal.valueOf(15), 0, RoundingMode.UP).intValue()); } @Override diff --git a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting5Message.java b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting5Message.java index adb91f3c70049..3602810be1ac8 100644 --- a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting5Message.java +++ b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComLighting5Message.java @@ -17,6 +17,7 @@ import static org.openhab.binding.rfxcom.internal.messages.RFXComLighting5Message.SubType.*; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.List; @@ -216,7 +217,7 @@ public String getDeviceId() { */ public static int getDimLevelFromPercentType(PercentType pt) { return pt.toBigDecimal().multiply(BigDecimal.valueOf(31)) - .divide(PercentType.HUNDRED.toBigDecimal(), 0, BigDecimal.ROUND_UP).intValue(); + .divide(PercentType.HUNDRED.toBigDecimal(), 0, RoundingMode.UP).intValue(); } /** @@ -229,7 +230,7 @@ public static PercentType getPercentTypeFromDimLevel(int value) { value = Math.min(value, 31); return new PercentType(BigDecimal.valueOf(value).multiply(BigDecimal.valueOf(100)) - .divide(BigDecimal.valueOf(31), 0, BigDecimal.ROUND_UP).intValue()); + .divide(BigDecimal.valueOf(31), 0, RoundingMode.UP).intValue()); } @Override diff --git a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComRFXSensorMessage.java b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComRFXSensorMessage.java index 776b06ff36efe..e5af3962a5eb4 100644 --- a/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComRFXSensorMessage.java +++ b/bundles/org.openhab.binding.rfxcom/src/main/java/org/openhab/binding/rfxcom/internal/messages/RFXComRFXSensorMessage.java @@ -12,8 +12,7 @@ */ package org.openhab.binding.rfxcom.internal.messages; -import static java.math.BigDecimal.*; -import static java.math.RoundingMode.HALF_DOWN; +import static java.math.RoundingMode.*; import static org.openhab.binding.rfxcom.internal.RFXComBindingConstants.*; import static org.openhab.binding.rfxcom.internal.messages.ByteEnumUtil.fromByte; @@ -257,12 +256,12 @@ private State handleHumidity(DeviceState deviceState) { BigDecimal supplyVoltage = ((DecimalType) referenceVoltageState).toBigDecimal(); // RH = (((A/D voltage / supply voltage) - 0.16) / 0.0062) / (1.0546 - 0.00216 * temperature) - BigDecimal belowTheDivider = adVoltage.divide(supplyVoltage, 4, ROUND_HALF_DOWN) - .subtract(HUMIDITY_VOLTAGE_SUBTRACTION).divide(HUMIDITY_VOLTAGE_DIVIDER, 4, ROUND_HALF_DOWN); + BigDecimal belowTheDivider = adVoltage.divide(supplyVoltage, 4, HALF_DOWN) + .subtract(HUMIDITY_VOLTAGE_SUBTRACTION).divide(HUMIDITY_VOLTAGE_DIVIDER, 4, HALF_DOWN); BigDecimal underTheDivider = HUMIDITY_TEMPERATURE_CORRECTION .subtract(HUMIDITY_TEMPERATURE_MULTIPLIER.multiply(temperature)); - return new DecimalType(belowTheDivider.divide(underTheDivider, 4, ROUND_HALF_DOWN)); + return new DecimalType(belowTheDivider.divide(underTheDivider, 4, HALF_DOWN)); } private State handlePressure(DeviceState deviceState) { @@ -277,14 +276,14 @@ private State handlePressure(DeviceState deviceState) { // hPa = ((A/D voltage / supply voltage) + 0.095) / 0.0009 return new DecimalType((adVoltage.divide(supplyVoltage, 4, HALF_DOWN).add(PRESSURE_ADDITION)) - .divide(PRESSURE_DIVIDER, 4, ROUND_HALF_DOWN)); + .divide(PRESSURE_DIVIDER, 4, HALF_DOWN)); } private BigDecimal getVoltage() { if (miliVoltageTimesTen == null) { return null; } - return miliVoltageTimesTen.divide(ONE_HUNDRED, 100, ROUND_CEILING); + return miliVoltageTimesTen.divide(ONE_HUNDRED, 100, CEILING); } @Override diff --git a/bundles/org.openhab.binding.rme/src/main/java/org/openhab/binding/rme/internal/handler/RMEThingHandler.java b/bundles/org.openhab.binding.rme/src/main/java/org/openhab/binding/rme/internal/handler/RMEThingHandler.java index 09b7fe731a0f5..a95dfd7d175ee 100644 --- a/bundles/org.openhab.binding.rme/src/main/java/org/openhab/binding/rme/internal/handler/RMEThingHandler.java +++ b/bundles/org.openhab.binding.rme/src/main/java/org/openhab/binding/rme/internal/handler/RMEThingHandler.java @@ -15,7 +15,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.openhab.binding.rme.internal.RMEBindingConstants.DataField; import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.library.types.DecimalType; diff --git a/bundles/org.openhab.binding.rme/src/main/resources/OH-INF/thing/rme.xml b/bundles/org.openhab.binding.rme/src/main/resources/OH-INF/thing/rme.xml index b11b176b1d5d0..2fe46bdf3e009 100644 --- a/bundles/org.openhab.binding.rme/src/main/resources/OH-INF/thing/rme.xml +++ b/bundles/org.openhab.binding.rme/src/main/resources/OH-INF/thing/rme.xml @@ -36,12 +36,11 @@ - + serial-port false Serial Port the RME Rain Manager is connected to - true diff --git a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectClient.java b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectClient.java index c8582ae055519..30f458e757bff 100644 --- a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectClient.java +++ b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectClient.java @@ -14,11 +14,11 @@ import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.api.AuthenticationStore; @@ -26,7 +26,6 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.util.B64Code; import org.openhab.binding.robonect.internal.model.ErrorList; import org.openhab.binding.robonect.internal.model.ModelParser; import org.openhab.binding.robonect.internal.model.MowerInfo; @@ -142,14 +141,17 @@ public BasicResult(HttpHeader header, URI uri, String value) { this.value = value; } + @Override public URI getURI() { return this.uri; } + @Override public void apply(Request request) { request.header(this.header, this.value); } + @Override public String toString() { return String.format("Basic authentication result for %s", this.uri); } @@ -175,8 +177,9 @@ public RobonectClient(HttpClient httpClient, RobonectEndpoint endpoint) { private void addPreemptiveAuthentication(HttpClient httpClient, RobonectEndpoint endpoint) { AuthenticationStore auth = httpClient.getAuthenticationStore(); URI uri = URI.create(baseUrl); - auth.addAuthenticationResult(new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic " - + B64Code.encode(endpoint.getUser() + ":" + endpoint.getPassword(), StandardCharsets.ISO_8859_1))); + auth.addAuthenticationResult( + new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic " + Base64.getEncoder().encodeToString( + (endpoint.getUser() + ":" + endpoint.getPassword()).getBytes(StandardCharsets.ISO_8859_1)))); } /** @@ -291,7 +294,7 @@ private String sendCommand(Command command) { String responseString = null; // jetty uses UTF-8 as default encoding. However, HTTP 1.1 specifies ISO_8859_1 - if (StringUtils.isBlank(response.getEncoding())) { + if (response.getEncoding() == null || response.getEncoding().isBlank()) { responseString = new String(response.getContent(), StandardCharsets.ISO_8859_1); } else { // currently v0.9e Robonect does not specifiy the encoding. But if later versions will diff --git a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/model/ModelParserTest.java b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/model/ModelParserTest.java index 64d2206b441f0..f22f522f45286 100644 --- a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/model/ModelParserTest.java +++ b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/model/ModelParserTest.java @@ -22,7 +22,7 @@ /** * The goal of this class is to test the model parser to make sure the structures * returned from the module can be handled. - * + * * @author Marco Meyer - Initial contribution */ public class ModelParserTest { @@ -48,12 +48,12 @@ public void shouldParseErrorResponseOnAllResponseTypes() { String correctModel = "{\"successful\": false, \"error_code\": 7, \"error_message\": \"Automower already stopped\"}"; RobonectAnswer answer = subject.parse(correctModel, RobonectAnswer.class); assertFalse(answer.isSuccessful()); - assertEquals(new Integer(7), answer.getErrorCode()); + assertEquals(Integer.valueOf(7), answer.getErrorCode()); assertEquals("Automower already stopped", answer.getErrorMessage()); MowerInfo info = subject.parse(correctModel, MowerInfo.class); assertFalse(info.isSuccessful()); - assertEquals(new Integer(7), info.getErrorCode()); + assertEquals(Integer.valueOf(7), info.getErrorCode()); assertEquals("Automower already stopped", info.getErrorMessage()); } @@ -143,7 +143,7 @@ public void shouldParseCorrectErrorModelInErrorState() { assertTrue(mowerInfo.getStatus().isStopped()); assertNotNull(mowerInfo.getError()); assertEquals("Mein Automower ist angehoben", mowerInfo.getError().getErrorMessage()); - assertEquals(new Integer(15), mowerInfo.getError().getErrorCode()); + assertEquals(Integer.valueOf(15), mowerInfo.getError().getErrorCode()); assertEquals("02.05.2017", mowerInfo.getError().getDate()); assertEquals("20:36:43", mowerInfo.getError().getTime()); assertEquals("1493757403", mowerInfo.getError().getUnix()); @@ -155,7 +155,7 @@ public void shouldParseErrorsList() { ErrorList errorList = subject.parse(errorsListResponse, ErrorList.class); assertTrue(errorList.isSuccessful()); assertEquals(10, errorList.getErrors().size()); - assertEquals(new Integer(15), errorList.getErrors().get(0).getErrorCode()); + assertEquals(Integer.valueOf(15), errorList.getErrors().get(0).getErrorCode()); assertEquals("Grasi ist angehoben", errorList.getErrors().get(0).getErrorMessage()); assertEquals("02.05.2017", errorList.getErrors().get(0).getDate()); assertEquals("20:36:43", errorList.getErrors().get(0).getTime()); diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java index 3333ef96d6205..a454430847601 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java @@ -87,7 +87,7 @@ public enum RotelCommand { SOURCE_VIDEO8("Source Video 8", "video8", "video8"), SOURCE_PHONO("Source Phono", RotelConnector.PRIMARY_CMD, (byte) 0x35, "phono", "phono"), SOURCE_USB("Source Front USB", RotelConnector.PRIMARY_CMD, (byte) 0x8E, "usb", "usb"), - SOURCE_PCUSB("Source PC USB", "pc_usb", "pc_usb"), + SOURCE_PCUSB("Source PC USB", "pc_usb", "pcusb"), SOURCE_MULTI_INPUT("Source Multi Input", RotelConnector.PRIMARY_CMD, (byte) 0x15, "multi_input", "multi_input"), SOURCE_AUX("Source Aux", "aux", "aux"), SOURCE_AUX1("Source Aux 1", "aux1", "aux1"), @@ -336,9 +336,9 @@ public byte getHexKey() { * * @throws RotelException - If no command is associated to the searched textual command */ - public static RotelCommand getFromAsciiCommandV2(String text) throws RotelException { + public static RotelCommand getFromAsciiCommand(String text) throws RotelException { for (RotelCommand value : RotelCommand.values()) { - if (text.equals(value.getAsciiCommandV2())) { + if (text.equals(value.getAsciiCommandV1()) || text.equals(value.getAsciiCommandV2())) { return value; } } diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java index 943b82fe3bfd8..062c1ad6b36e4 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java @@ -858,6 +858,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { scheduleReconnectJob(); } catch (InterruptedException e) { logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage()); + Thread.currentThread().interrupt(); } } } @@ -1293,28 +1294,28 @@ public void onNewMessageEvent(EventObject event) { updateChannelState(CHANNEL_MAIN_TREBLE); break; case RotelConnector.KEY_SOURCE: - source = connector.getModel().getSourceFromCommand(RotelCommand.getFromAsciiCommandV2(value)); + source = connector.getModel().getSourceFromCommand(RotelCommand.getFromAsciiCommand(value)); updateChannelState(CHANNEL_SOURCE); updateChannelState(CHANNEL_MAIN_SOURCE); break; case RotelConnector.KEY_RECORD: recordSource = connector.getModel() - .getRecordSourceFromCommand(RotelCommand.getFromAsciiCommandV2(value)); + .getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value)); updateChannelState(CHANNEL_MAIN_RECORD_SOURCE); break; case RotelConnector.KEY_SOURCE_ZONE2: sourceZone2 = connector.getModel() - .getZone2SourceFromCommand(RotelCommand.getFromAsciiCommandV2(value)); + .getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value)); updateChannelState(CHANNEL_ZONE2_SOURCE); break; case RotelConnector.KEY_SOURCE_ZONE3: sourceZone3 = connector.getModel() - .getZone3SourceFromCommand(RotelCommand.getFromAsciiCommandV2(value)); + .getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value)); updateChannelState(CHANNEL_ZONE3_SOURCE); break; case RotelConnector.KEY_SOURCE_ZONE4: sourceZone4 = connector.getModel() - .getZone4SourceFromCommand(RotelCommand.getFromAsciiCommandV2(value)); + .getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value)); updateChannelState(CHANNEL_ZONE4_SOURCE); break; case RotelConnector.KEY_DSP_MODE: @@ -1686,6 +1687,7 @@ private void schedulePowerOnJob() { closeConnection(); } catch (InterruptedException e) { logger.debug("Init sequence interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); } } }, 2500, TimeUnit.MILLISECONDS); @@ -1729,6 +1731,7 @@ private void schedulePowerOnZone2Job() { closeConnection(); } catch (InterruptedException e) { logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); } } }, 2500, TimeUnit.MILLISECONDS); @@ -1772,6 +1775,7 @@ private void schedulePowerOnZone3Job() { closeConnection(); } catch (InterruptedException e) { logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); } } }, 2500, TimeUnit.MILLISECONDS); @@ -1815,6 +1819,7 @@ private void schedulePowerOnZone4Job() { closeConnection(); } catch (InterruptedException e) { logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); } } }, 2500, TimeUnit.MILLISECONDS); diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDeviceDiscoveryService.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDeviceDiscoveryService.java index bb418b6662a1c..dc418c9c54e0b 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDeviceDiscoveryService.java @@ -16,7 +16,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.russound.internal.RussoundHandlerFactory; import org.openhab.binding.russound.internal.net.SocketChannelSession; import org.openhab.binding.russound.internal.net.SocketSession; @@ -148,7 +148,7 @@ public void scanDevice() { private void discoverControllers() { for (int c = 1; c < 7; c++) { final String type = sendAndGet("GET C[" + c + "].type", RSP_CONTROLLERNOTIFICATION, 3); - if (StringUtils.isNotEmpty(type)) { + if (type != null && !type.isEmpty()) { logger.debug("Controller #{} found - {}", c, type); final ThingUID thingUID = new ThingUID(RioConstants.BRIDGE_TYPE_CONTROLLER, @@ -172,7 +172,7 @@ private void discoverControllers() { private void discoverSources() { for (int s = 1; s < 9; s++) { final String type = sendAndGet("GET S[" + s + "].type", RSP_SRCNOTIFICATION, 3); - if (StringUtils.isNotEmpty(type)) { + if (type != null && !type.isEmpty()) { final String name = sendAndGet("GET S[" + s + "].name", RSP_SRCNOTIFICATION, 3); logger.debug("Source #{} - {}/{}", s, type, name); @@ -181,8 +181,8 @@ private void discoverSources() { final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) .withProperty(RioSourceConfig.SOURCE, s).withBridge(sysHandler.getThing().getUID()) - .withLabel((StringUtils.isEmpty(name) || name.equalsIgnoreCase("null") ? "Source" : name) + " (" - + s + ")") + .withLabel((name == null || name.isEmpty() || name.equalsIgnoreCase("null") ? "Source" : name) + + " (" + s + ")") .build(); thingDiscovered(discoveryResult); } @@ -207,7 +207,7 @@ private void discoverZones(ThingUID controllerUID, int c) { } for (int z = 1; z < 9; z++) { final String name = sendAndGet("GET C[" + c + "].Z[" + z + "].name", RSP_ZONENOTIFICATION, 4); - if (StringUtils.isNotEmpty(name)) { + if (name != null && !name.isEmpty()) { logger.debug("Controller #{}, Zone #{} found - {}", c, z, name); final ThingUID thingUID = new ThingUID(RioConstants.THING_TYPE_ZONE, controllerUID, String.valueOf(z)); @@ -232,8 +232,8 @@ private void discoverZones(ThingUID controllerUID, int c) { * @throws IllegalArgumentException if message is null or empty, if the pattern is null * @throws IllegalArgumentException if groupNum is less than 0 */ - private String sendAndGet(String message, Pattern respPattern, int groupNum) { - if (StringUtils.isEmpty(message)) { + private @Nullable String sendAndGet(String message, Pattern respPattern, int groupNum) { + if (message == null || message.isEmpty()) { throw new IllegalArgumentException("message cannot be a null or empty string"); } if (respPattern == null) { diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDiscovery.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDiscovery.java index c1efcf390c974..a34f0b54b453e 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDiscovery.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/discovery/RioSystemDiscovery.java @@ -26,7 +26,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.apache.commons.net.util.SubnetUtils; import org.openhab.binding.russound.internal.net.SocketChannelSession; import org.openhab.binding.russound.internal.net.SocketSession; @@ -152,7 +151,7 @@ protected synchronized void stopScan() { * @param ipAddress a possibly null, possibly empty ip address (null/empty addresses will be ignored) */ private void scanAddress(String ipAddress) { - if (StringUtils.isEmpty(ipAddress)) { + if (ipAddress == null || ipAddress.isEmpty()) { return; } @@ -175,7 +174,7 @@ private void scanAddress(String ipAddress) { continue; } final String type = resp.substring(13, resp.length() - 1); - if (!StringUtils.isBlank(type)) { + if (!type.isBlank()) { logger.debug("Found a RIO type #{}", type); addResult(ipAddress, type); break; @@ -202,10 +201,10 @@ private void scanAddress(String ipAddress) { * @throws IllegalArgumentException if ipaddress or type is null or empty */ private void addResult(String ipAddress, String type) { - if (StringUtils.isEmpty(ipAddress)) { + if (ipAddress == null || ipAddress.isEmpty()) { throw new IllegalArgumentException("ipAddress cannot be null or empty"); } - if (StringUtils.isEmpty(type)) { + if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type cannot be null or empty"); } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractBridgeHandler.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractBridgeHandler.java index 3cc3cf9ff8067..2790d065b046a 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractBridgeHandler.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractBridgeHandler.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.core.library.types.StringType; @@ -165,7 +164,7 @@ protected void refreshNamedHandler(Gson gson, Class< if (clazz == null) { throw new IllegalArgumentException("clazz cannot be null"); } - if (StringUtils.isEmpty(channelId)) { + if (channelId == null || channelId.isEmpty()) { throw new IllegalArgumentException("channelId cannot be null or empty"); } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractRioHandlerCallback.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractRioHandlerCallback.java index b7af1c4b68455..ee9339f954c46 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractRioHandlerCallback.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/AbstractRioHandlerCallback.java @@ -14,7 +14,6 @@ import java.util.concurrent.CopyOnWriteArrayList; -import org.apache.commons.lang.StringUtils; import org.openhab.core.types.State; /** @@ -56,7 +55,7 @@ public void removeListener(String channelId, RioHandlerCallbackListener listener * @throws IllegalArgumentException if state is null */ protected void fireStateUpdated(String channelId, State state) { - if (StringUtils.isEmpty(channelId)) { + if (channelId == null || channelId.isEmpty()) { throw new IllegalArgumentException("channelId cannot be null or empty)"); } if (state == null) { diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioPresetsProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioPresetsProtocol.java index 20116794feff6..0b9b5cfae0bce 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioPresetsProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioPresetsProtocol.java @@ -14,13 +14,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.binding.russound.internal.rio.models.GsonUtilities; @@ -258,7 +259,7 @@ public String getJson(int source) { * @throws IllegalArgumentException if source is < 1 or > 8 * @throws IllegalArgumentException if presetJson contains more than one preset */ - public void setZonePresets(int controller, int zone, int source, String presetJson) { + public void setZonePresets(int controller, int zone, int source, @Nullable String presetJson) { if (controller < 1 || controller > 6) { throw new IllegalArgumentException("Controller must be between 1 and 6"); } @@ -271,7 +272,7 @@ public void setZonePresets(int controller, int zone, int source, String presetJs throw new IllegalArgumentException("Source must be between 1 and 8"); } - if (StringUtils.isEmpty(presetJson)) { + if (presetJson == null || presetJson.isEmpty()) { return; } @@ -299,11 +300,11 @@ public void setZonePresets(int controller, int zone, int source, String presetJs // re-retrieve to see if the save/delete worked (saving on a zone that's off - valid won't be set to // true) - if (!StringUtils.equals(myPreset.getName(), presetName) || myPreset.isValid() != presetValid) { + if (!Objects.equals(myPreset.getName(), presetName) || myPreset.isValid() != presetValid) { myPreset.setName(presetName); myPreset.setValid(presetValid); if (presetValid) { - if (StringUtils.isEmpty(presetName)) { + if (presetName == null || presetName.isEmpty()) { sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!savePreset " + presetId); } else { sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!savePreset \"" + presetName @@ -438,8 +439,8 @@ private void handlerSourceTypeNotification(Matcher m, String resp) { * @param a possibly null, possibly empty response */ @Override - public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + public void responseReceived(@Nullable String response) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioSystemFavoritesProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioSystemFavoritesProtocol.java index 3fa4dba701939..cea05bdb66b93 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioSystemFavoritesProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/RioSystemFavoritesProtocol.java @@ -14,13 +14,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.binding.russound.internal.rio.models.GsonUtilities; @@ -199,7 +200,7 @@ public String getJson() { * @throws IllegalArgumentException if controller is < 1 or > 6 * @throws IllegalArgumentException if zone is < 1 or > 8 */ - public void setSystemFavorites(int controller, int zone, String favJson) { + public void setSystemFavorites(int controller, int zone, @Nullable String favJson) { if (controller < 1 || controller > 6) { throw new IllegalArgumentException("Controller must be between 1 and 6"); } @@ -208,7 +209,7 @@ public void setSystemFavorites(int controller, int zone, String favJson) { throw new IllegalArgumentException("Zone must be between 1 and 8"); } - if (StringUtils.isEmpty(favJson)) { + if (favJson == null || favJson.isEmpty()) { return; } @@ -242,7 +243,7 @@ public void setSystemFavorites(int controller, int zone, String favJson) { } else { sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!deleteSystemFavorite " + favId); } - } else if (!StringUtils.equals(myFav.getName(), favName)) { + } else if (!Objects.equals(myFav.getName(), favName)) { myFav.setName(favName); sendCommand("SET System.favorite[" + favId + "]." + FAV_NAME + "=\"" + favName + "\""); } @@ -311,8 +312,8 @@ private void handleSystemFavoriteNotification(Matcher m, String resp) { * @param a possibly null, possibly empty response */ @Override - public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + public void responseReceived(@Nullable String response) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/StatefulHandlerCallback.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/StatefulHandlerCallback.java index e34fc6d80c5f2..e68159458b0db 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/StatefulHandlerCallback.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/StatefulHandlerCallback.java @@ -17,7 +17,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.apache.commons.lang.StringUtils; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.State; @@ -89,7 +88,7 @@ public void statusChanged(ThingStatus status, ThingStatusDetail detail, String m */ @Override public void stateChanged(String channelId, State newState) { - if (StringUtils.isEmpty(channelId)) { + if (channelId == null || channelId.isEmpty()) { return; } @@ -117,7 +116,7 @@ public void stateChanged(String channelId, State newState) { * @param channelId the channel id to remove state */ public void removeState(String channelId) { - if (StringUtils.isEmpty(channelId)) { + if (channelId == null || channelId.isEmpty()) { return; } state.remove(channelId); diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/controller/RioControllerProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/controller/RioControllerProtocol.java index e3a006ce9de72..2913ef687cd21 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/controller/RioControllerProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/controller/RioControllerProtocol.java @@ -15,7 +15,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.binding.russound.internal.rio.AbstractRioProtocol; @@ -155,8 +155,8 @@ private void handleControllerNotification(Matcher m, String resp) { * @param a possibly null, possibly empty response */ @Override - public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + public void responseReceived(@Nullable String response) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/AtomicStringTypeAdapter.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/AtomicStringTypeAdapter.java index 81f96a8069edb..726361b999cd0 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/AtomicStringTypeAdapter.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/AtomicStringTypeAdapter.java @@ -38,8 +38,7 @@ public class AtomicStringTypeAdapter extends TypeAdapter public AtomicReference read(JsonReader in) throws IOException { AtomicReference value = null; - JsonParser jsonParser = new JsonParser(); - JsonElement je = jsonParser.parse(in); + JsonElement je = JsonParser.parseReader(in); if (je instanceof JsonPrimitive) { value = new AtomicReference<>(); diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioBank.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioBank.java index 345b5f7bec51d..dfc7eba0db5d1 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioBank.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioBank.java @@ -14,7 +14,7 @@ import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; /** * Simple model of a RIO Bank and it's attributes. Please note this class is used to serialize/deserialize to JSON. @@ -50,12 +50,12 @@ public RioBank(int id) { * @param name a possibly null, possibly empty bank name (null or empty will result in a bank name of "Bank "+ id) * @throws IllegalArgumentException if id is < 1 or > 6 */ - public RioBank(int id, String name) { + public RioBank(int id, @Nullable String name) { if (id < 1 || id > 6) { throw new IllegalArgumentException("Bank ID can only be between 1 and 6"); } this.id = id; - this.name.set(StringUtils.isEmpty(name) ? "Bank " + id : name); + this.name.set(name == null || name.isEmpty() ? "Bank " + id : name); } /** @@ -81,7 +81,7 @@ public String getName() { * * @param bankName a possibly null, possibly empty bank name */ - public void setName(String bankName) { - name.set(StringUtils.isEmpty(bankName) ? "Bank " + getId() : bankName); + public void setName(@Nullable String bankName) { + name.set(bankName == null || bankName.isEmpty() ? "Bank " + getId() : bankName); } } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioFavorite.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioFavorite.java index cc982e4e851bb..53d3548ba4b6c 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioFavorite.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioFavorite.java @@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; /** * Simple model of a RIO Favorite (both system and zone) and it's attributes. Please note this class is used to @@ -59,18 +59,14 @@ public RioFavorite(int id) { * @param name a possibly null, possibly empty favorite name * @throws IllegalArgumentException if id < 1 or > 32 */ - public RioFavorite(int id, boolean isValid, String name) { + public RioFavorite(int id, boolean isValid, @Nullable String name) { if (id < 1 || id > 32) { throw new IllegalArgumentException("Favorite ID must be between 1 and 32"); } - if (StringUtils.isEmpty(name)) { - name = "Favorite " + id; - } - this.id = id; this.valid.set(isValid); - this.name.set(name); + this.name.set(name == null || name.isEmpty() ? "Favorite " + id : name); } /** @@ -105,8 +101,8 @@ public void setValid(boolean favValid) { * * @param favName a possibly null, possibly empty favorite name */ - public void setName(String favName) { - name.set(StringUtils.isEmpty(favName) ? "Favorite " + getId() : favName); + public void setName(@Nullable String favName) { + name.set(favName == null || favName.isEmpty() ? "Favorite " + getId() : favName); } /** diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioPreset.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioPreset.java index a6820696bb814..96d7b8f259620 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioPreset.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/models/RioPreset.java @@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; /** * Simple model of a RIO Preset and it's attributes. Please note this class is used to @@ -59,18 +59,14 @@ public RioPreset(int id) { * @param name a possibly null, possibly empty preset name * @throws IllegalArgumentException if id < 1 or > 32 */ - public RioPreset(int id, boolean valid, String name) { + public RioPreset(int id, boolean valid, @Nullable String name) { if (id < 1 || id > 36) { throw new IllegalArgumentException("Preset ID can only be between 1 and 36"); } - if (StringUtils.isEmpty(name)) { - name = "Preset " + id; - } - this.id = id; this.valid.set(valid); - this.name.set(name); + this.name.set(name == null || name.isEmpty() ? "Preset " + id : name); } /** @@ -123,8 +119,8 @@ public void setValid(boolean presetValid) { * * @param presetName a possibly null, possibly empty preset name */ - public void setName(String presetName) { - name.set(StringUtils.isEmpty(presetName) ? "Preset " + getId() : presetName); + public void setName(@Nullable String presetName) { + name.set(presetName == null || presetName.isEmpty() ? "Preset " + getId() : presetName); } /** diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceHandler.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceHandler.java index 2ab342eb31bff..5ca7dc53daaa7 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceHandler.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceHandler.java @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.rio.AbstractRioHandlerCallback; import org.openhab.binding.russound.internal.rio.AbstractThingHandler; @@ -84,7 +83,7 @@ public int getId() { @Override public String getName() { final String name = sourceName.get(); - return StringUtils.isEmpty(name) ? ("Source " + getId()) : name; + return name == null || name.isEmpty() ? "Source " + getId() : name; } /** diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceProtocol.java index 4f5b02969a756..755f987db75c7 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/source/RioSourceProtocol.java @@ -15,14 +15,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.NullArgumentException; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; @@ -367,7 +367,7 @@ void refreshBanks() { */ Runnable setBanks(String bankJson) { // If null or empty - simply return a do nothing runnable - if (StringUtils.isEmpty(bankJson)) { + if (bankJson == null || bankJson.isEmpty()) { return () -> { }; } @@ -387,7 +387,7 @@ Runnable setBanks(String bankJson) { } else { final RioBank myBank = banks[bankId - 1]; - if (!StringUtils.equals(myBank.getName(), bank.getName())) { + if (!Objects.equals(myBank.getName(), bank.getName())) { myBank.setName(bank.getName()); sendCommand( "SET S[" + source + "].B[" + bankId + "]." + BANK_NAME + "=\"" + bank.getName() + "\""); @@ -431,8 +431,8 @@ void watchSource(boolean watch) { * @throws IllegalArgumentException if channelID is null or empty */ private void handleMMChange(String channelId, String value) { - if (StringUtils.isEmpty(channelId)) { - throw new NullArgumentException("channelId cannot be null or empty"); + if (channelId == null || channelId.isEmpty()) { + throw new IllegalArgumentException("channelId cannot be null or empty"); } final AtomicInteger ai = mmSeqNbrs.get(channelId); @@ -688,8 +688,8 @@ private void handleBankNotification(Matcher m, String resp) { * @param a possibly null, possibly empty response */ @Override - public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + public void responseReceived(@Nullable String response) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/system/RioSystemProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/system/RioSystemProtocol.java index 2badcb8f5c582..edd990e1a5705 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/system/RioSystemProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/system/RioSystemProtocol.java @@ -16,7 +16,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.binding.russound.internal.rio.AbstractRioProtocol; @@ -245,8 +245,8 @@ private void handleFailureNotification(Matcher m, String resp) { * @param a possibly null, possibly empty response */ @Override - public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + public void responseReceived(@Nullable String response) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneHandler.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneHandler.java index 3fcaa5e7f2419..23832a19db1c0 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneHandler.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneHandler.java @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.rio.AbstractBridgeHandler; import org.openhab.binding.russound.internal.rio.AbstractRioHandlerCallback; @@ -108,7 +107,7 @@ public int getId() { @Override public String getName() { final String name = zoneName.get(); - return StringUtils.isEmpty(name) ? ("Zone " + getId()) : name; + return name == null || name.isEmpty() ? "Zone " + getId() : name; } /** diff --git a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneProtocol.java b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneProtocol.java index 02e85516ef533..5749f19b4e33a 100644 --- a/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneProtocol.java +++ b/bundles/org.openhab.binding.russound/src/main/java/org/openhab/binding/russound/internal/rio/zone/RioZoneProtocol.java @@ -14,11 +14,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; import org.openhab.binding.russound.internal.net.SocketSession; import org.openhab.binding.russound.internal.net.SocketSessionListener; import org.openhab.binding.russound.internal.rio.AbstractRioProtocol; @@ -529,7 +529,7 @@ void setSystemFavorites(String favJson) { * @return a non-null {@link Runnable} that should be run after the call */ Runnable setZoneFavorites(String favJson) { - if (StringUtils.isEmpty(favJson)) { + if (favJson.isEmpty()) { return () -> { }; } @@ -550,7 +550,7 @@ Runnable setZoneFavorites(String favJson) { final boolean favValid = fav.isValid(); final String favName = fav.getName(); - if (!StringUtils.equals(myFav.getName(), favName) || myFav.isValid() != favValid) { + if (!Objects.equals(myFav.getName(), favName) || myFav.isValid() != favValid) { myFav.setName(favName); myFav.setValid(favValid); if (favValid) { @@ -923,7 +923,7 @@ public void presetsUpdated(int sourceIdUpdated, String jsonString) { */ @Override public void responseReceived(String response) { - if (StringUtils.isEmpty(response)) { + if (response == null || response.isEmpty()) { return; } diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/command/ControlCommand.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/command/ControlCommand.java index c6d44986943e9..bf5a85edae31e 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/command/ControlCommand.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/command/ControlCommand.java @@ -14,7 +14,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.satel.internal.protocol.SatelMessage; -import org.openhab.binding.satel.internal.util.StringUtils; /** * Base class for all commands that return result code in the response. @@ -59,7 +58,7 @@ protected static byte[] appendUserCode(byte[] payload, String userCode) { } protected static byte[] userCodeToBytes(String userCode) { - if (StringUtils.isEmpty(userCode)) { + if (userCode.isEmpty()) { throw new IllegalArgumentException("User code is empty"); } if (userCode.length() > 8) { diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/Ethm1Config.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/Ethm1Config.java index bb7ff9933758c..3df0f3e7c19b4 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/Ethm1Config.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/Ethm1Config.java @@ -25,7 +25,7 @@ public class Ethm1Config extends SatelBridgeConfig { public static final String HOST = "host"; - private @Nullable String host; + private String host = ""; private int port; private @Nullable String encryptionKey; @@ -33,8 +33,7 @@ public class Ethm1Config extends SatelBridgeConfig { * @return IP or hostname of the bridge */ public String getHost() { - final String host = this.host; - return host == null ? "" : host; + return host; } /** diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/IntRSConfig.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/IntRSConfig.java index 2b92a2b09d6aa..d785d40201c6f 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/IntRSConfig.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/config/IntRSConfig.java @@ -13,7 +13,6 @@ package org.openhab.binding.satel.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; /** * The {@link IntRSConfig} contains configuration values for Satel INT-RS bridge. @@ -25,12 +24,12 @@ public class IntRSConfig extends SatelBridgeConfig { public static final String PORT = "port"; - private @Nullable String port; + private String port = ""; /** * @return serial port to which the module is connected */ - public @Nullable String getPort() { + public String getPort() { return port; } } diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/Ethm1BridgeHandler.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/Ethm1BridgeHandler.java index 2d795d6c7bd8d..9644f0b2ae317 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/Ethm1BridgeHandler.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/Ethm1BridgeHandler.java @@ -23,7 +23,6 @@ import org.openhab.binding.satel.internal.config.Ethm1Config; import org.openhab.binding.satel.internal.protocol.Ethm1Module; import org.openhab.binding.satel.internal.protocol.SatelModule; -import org.openhab.binding.satel.internal.util.StringUtils; import org.openhab.core.config.core.status.ConfigStatusMessage; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; @@ -54,7 +53,7 @@ public void initialize() { logger.debug("Initializing handler"); Ethm1Config config = getConfigAs(Ethm1Config.class); - if (StringUtils.isNotBlank(config.getHost())) { + if (!config.getHost().isBlank()) { SatelModule satelModule = new Ethm1Module(config.getHost(), config.getPort(), config.getTimeout(), config.getEncryptionKey(), config.hasExtCommandsSupport()); super.initialize(satelModule); @@ -71,7 +70,7 @@ public Collection getConfigStatus() { Collection configStatusMessages; // Check whether an IP address is provided - if (StringUtils.isBlank(host)) { + if (host.isBlank()) { configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(HOST) .withMessageKeySuffix("hostEmpty").withArguments(HOST).build()); } else { diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/IntRSBridgeHandler.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/IntRSBridgeHandler.java index 0718a3234db89..2489f589d6717 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/IntRSBridgeHandler.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/IntRSBridgeHandler.java @@ -23,7 +23,6 @@ import org.openhab.binding.satel.internal.config.IntRSConfig; import org.openhab.binding.satel.internal.protocol.IntRSModule; import org.openhab.binding.satel.internal.protocol.SatelModule; -import org.openhab.binding.satel.internal.util.StringUtils; import org.openhab.core.config.core.status.ConfigStatusMessage; import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.Bridge; @@ -59,7 +58,7 @@ public void initialize() { final IntRSConfig config = getConfigAs(IntRSConfig.class); final String port = config.getPort(); - if (port != null && StringUtils.isNotBlank(port)) { + if (!port.isBlank()) { SatelModule satelModule = new IntRSModule(port, serialPortManager, config.getTimeout(), config.hasExtCommandsSupport()); super.initialize(satelModule); @@ -76,7 +75,7 @@ public Collection getConfigStatus() { Collection configStatusMessages; // Check whether a serial port is provided - if (StringUtils.isBlank(port)) { + if (port.isBlank()) { configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(PORT) .withMessageKeySuffix("portEmpty").withArguments(PORT).build()); } else { diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelBridgeHandler.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelBridgeHandler.java index f06ba231c0089..aca68ab2a8d87 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelBridgeHandler.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelBridgeHandler.java @@ -26,7 +26,6 @@ import org.openhab.binding.satel.internal.event.SatelEventListener; import org.openhab.binding.satel.internal.protocol.SatelModule; import org.openhab.binding.satel.internal.types.IntegraType; -import org.openhab.binding.satel.internal.util.StringUtils; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -164,7 +163,7 @@ public IntegraType getIntegraType() { * @return current user code, either from the configuration or set later using {@link #setUserCode(String)} */ public String getUserCode() { - if (StringUtils.isNotEmpty(userCodeOverride)) { + if (!userCodeOverride.isEmpty()) { return userCodeOverride; } else { return config.getUserCode(); diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelStateThingHandler.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelStateThingHandler.java index 19eedacd0fe52..704b4452c916b 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelStateThingHandler.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelStateThingHandler.java @@ -25,7 +25,6 @@ import org.openhab.binding.satel.internal.event.IntegraStateEvent; import org.openhab.binding.satel.internal.event.NewStatesEvent; import org.openhab.binding.satel.internal.types.StateType; -import org.openhab.binding.satel.internal.util.StringUtils; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -59,7 +58,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { this.requiresRefresh.set(true); } else { withBridgeHandlerPresent(bridgeHandler -> { - if (StringUtils.isEmpty(bridgeHandler.getUserCode())) { + if (bridgeHandler.getUserCode().isEmpty()) { logger.info("Cannot control devices without providing valid user code. Command has not been sent."); } else { convertCommand(channelUID, command) diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java index 457f08e865a3a..074d92cbefd33 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java @@ -23,7 +23,6 @@ import java.util.Random; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.satel.internal.util.StringUtils; import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,17 +66,17 @@ public Ethm1Module(String host, int port, int timeout, String encryptionKey, boo @Override protected CommunicationChannel connect() throws ConnectionFailureException { - logger.info("Connecting to ETHM-1 module at {}:{}", this.host, this.port); + logger.info("Connecting to ETHM-1 module at {}:{}", host, port); try { Socket socket = new Socket(); - socket.connect(new InetSocketAddress(this.host, this.port), this.getTimeout()); + socket.connect(new InetSocketAddress(host, port), this.getTimeout()); logger.info("ETHM-1 module connected successfully"); - if (StringUtils.isBlank(this.encryptionKey)) { + if (encryptionKey.isBlank()) { return new TCPCommunicationChannel(socket); } else { - return new EncryptedCommunicationChannel(socket, this.encryptionKey); + return new EncryptedCommunicationChannel(socket, encryptionKey); } } catch (SocketTimeoutException e) { throw new ConnectionFailureException("Connection timeout", e); diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/StringUtils.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/StringUtils.java deleted file mode 100644 index 0661632868325..0000000000000 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/StringUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.satel.internal.util; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Replacement class for Apache's StringUtils. - * - * @author Krzysztof Goworek - Initial contribution - * - */ -@NonNullByDefault -public class StringUtils { - - /** - * Checks if a string is empty or null. - * - * @param str the string to check - * @return true if given string is empty or null - */ - public static boolean isEmpty(@Nullable String str) { - return str == null || str.isEmpty(); - } - - /** - * Checks if a string is not empty and not null. - * - * @param str the string to check - * @return true if given string is not empty and not null - */ - public static boolean isNotEmpty(@Nullable String str) { - return !isEmpty(str); - } - - /** - * Checks if a string is null or empty or all characters are whitespace. - * - * @param str the string to check - * @return true if given string is blank - */ - public static boolean isBlank(@Nullable String str) { - return str == null || str.isBlank(); - } - - /** - * Checks if a string is not null, not empty and contains at least one non-whitespace character. - * - * @param str the string to check - * @return true if given string is not blank - */ - public static boolean isNotBlank(@Nullable String str) { - return !isBlank(str); - } -} diff --git a/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/StringUtilsTest.java b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/StringUtilsTest.java deleted file mode 100644 index 4d35c367b7d42..0000000000000 --- a/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/StringUtilsTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.satel.internal.util; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -/** - * @author Krzysztof Goworek - Initial contribution - */ -public class StringUtilsTest { - - @Test - public void testIsEmpty() { - assertFalse(StringUtils.isEmpty("foobar")); - assertFalse(StringUtils.isEmpty(" ")); - assertTrue(StringUtils.isEmpty("")); - assertTrue(StringUtils.isEmpty(null)); - } - - @Test - public void testIsNotEmpty() { - assertTrue(StringUtils.isNotEmpty("foobar")); - assertTrue(StringUtils.isNotEmpty(" ")); - assertFalse(StringUtils.isNotEmpty("")); - assertFalse(StringUtils.isNotEmpty(null)); - } - - @Test - public void testIsBlank() { - assertFalse(StringUtils.isBlank("foobar")); - assertTrue(StringUtils.isBlank(" ")); - assertTrue(StringUtils.isBlank("")); - assertTrue(StringUtils.isBlank(null)); - } - - @Test - public void testIsNotBlank() { - assertTrue(StringUtils.isNotBlank("foobar")); - assertFalse(StringUtils.isNotBlank(" ")); - assertFalse(StringUtils.isNotBlank("")); - assertFalse(StringUtils.isNotBlank(null)); - } -} diff --git a/bundles/org.openhab.binding.seneye/src/main/java/org/openhab/binding/seneye/internal/SeneyeService.java b/bundles/org.openhab.binding.seneye/src/main/java/org/openhab/binding/seneye/internal/SeneyeService.java index a1efb1df25b11..7488dc4c2f513 100644 --- a/bundles/org.openhab.binding.seneye/src/main/java/org/openhab/binding/seneye/internal/SeneyeService.java +++ b/bundles/org.openhab.binding.seneye/src/main/java/org/openhab/binding/seneye/internal/SeneyeService.java @@ -43,7 +43,7 @@ public class SeneyeService { public int seneyeType; private boolean isInitialized; private final Gson gson; - private HttpClient httpClient = new HttpClient(new SslContextFactory()); + private HttpClient httpClient = new HttpClient(new SslContextFactory.Client()); private ScheduledFuture scheduledJob; public SeneyeService(SeneyeConfigurationParameters config) throws CommunicationException { diff --git a/bundles/org.openhab.binding.sensebox/src/main/java/org/openhab/binding/sensebox/internal/SenseBoxAPIConnection.java b/bundles/org.openhab.binding.sensebox/src/main/java/org/openhab/binding/sensebox/internal/SenseBoxAPIConnection.java index 907fa8782e8a6..0e14c975b9c8c 100644 --- a/bundles/org.openhab.binding.sensebox/src/main/java/org/openhab/binding/sensebox/internal/SenseBoxAPIConnection.java +++ b/bundles/org.openhab.binding.sensebox/src/main/java/org/openhab/binding/sensebox/internal/SenseBoxAPIConnection.java @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * The {@link SenseBoxAPIConnection} is responsible for fetching data from the senseBox API server. @@ -63,8 +64,9 @@ public SenseBoxData reallyFetchDataFromServer(String senseBoxId) { // the caching layer does not like null values SenseBoxData result = new SenseBoxData(); + String body = null; try { - String body = HttpUtil.executeUrl(METHOD, query, HEADERS, null, null, TIMEOUT); + body = HttpUtil.executeUrl(METHOD, query, HEADERS, null, null, TIMEOUT); logger.trace("Fetched Data: {}", body); SenseBoxData parsedData = gson.fromJson(body, SenseBoxData.class); @@ -148,6 +150,10 @@ public SenseBoxData reallyFetchDataFromServer(String senseBoxId) { logger.trace("================================="); result = parsedData; + } catch (JsonSyntaxException e) { + logger.debug("An error occurred while parsing the data into desired class: {} / {} / {}", body, + SenseBoxData.class.getName(), e.getMessage()); + result.setStatus(ThingStatus.OFFLINE); } catch (IOException e) { logger.debug("IO problems while fetching data: {} / {}", query, e.getMessage()); result.setStatus(ThingStatus.OFFLINE); diff --git a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/client/RequestLogger.java b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/client/RequestLogger.java index 70cdb8f1b742e..4fd5c66689615 100644 --- a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/client/RequestLogger.java +++ b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/client/RequestLogger.java @@ -41,12 +41,10 @@ public final class RequestLogger { private final Logger logger = LoggerFactory.getLogger(RequestLogger.class); private final AtomicLong nextId = new AtomicLong(); - private final JsonParser parser; private final Gson gson; private final String prefix; public RequestLogger(final String prefix, final Gson gson) { - parser = new JsonParser(); this.gson = gson; this.prefix = prefix; } @@ -133,7 +131,7 @@ public Request listenTo(final Request request, String[] stringToRemove) { private String reformatJson(final String jsonString) { try { - final JsonElement json = parser.parse(jsonString); + final JsonElement json = JsonParser.parseString(jsonString); return gson.toJson(json); } catch (final JsonSyntaxException e) { logger.debug("Could not reformat malformed JSON due to '{}'", e.getMessage()); diff --git a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboAccountHandler.java b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboAccountHandler.java index 56f1130f55f33..abf94484a0e3c 100644 --- a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboAccountHandler.java +++ b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboAccountHandler.java @@ -85,7 +85,6 @@ public class SensiboAccountHandler extends BaseBridgeHandler { public static String API_ENDPOINT = "https://home.sensibo.com/api"; private final Logger logger = LoggerFactory.getLogger(SensiboAccountHandler.class); private final HttpClient httpClient; - private final JsonParser jsonParser = new JsonParser(); private final RequestLogger requestLogger; private final Gson gson; private SensiboModel model = new SensiboModel(0); @@ -207,7 +206,7 @@ private T sendRequest(final Request request, final AbstractRequest req, fina final ContentResponse contentResponse = request.send(); final String responseJson = contentResponse.getContentAsString(); if (contentResponse.getStatus() == HttpStatus.OK_200) { - final JsonObject o = jsonParser.parse(responseJson).getAsJsonObject(); + final JsonObject o = JsonParser.parseString(responseJson).getAsJsonObject(); final String overallStatus = o.get("status").getAsString(); if ("success".equals(overallStatus)) { return gson.fromJson(o.get("result"), responseType); diff --git a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboSkyHandler.java b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboSkyHandler.java index 910eae9ddd758..9fb8b60a99c2d 100644 --- a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboSkyHandler.java +++ b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/handler/SensiboSkyHandler.java @@ -31,10 +31,10 @@ import javax.measure.UnitConverter; import javax.measure.quantity.Temperature; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.commons.lang3.text.WordUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.sensibo.internal.CallbackChannelsTypeProvider; diff --git a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboModel.java b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboModel.java index d8bc7bb1b75ff..0311311bbd797 100644 --- a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboModel.java +++ b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboModel.java @@ -16,7 +16,7 @@ import java.util.List; import java.util.Optional; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; /** diff --git a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboSky.java b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboSky.java index 8c346a6fff221..f94bec4c71bc2 100644 --- a/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboSky.java +++ b/bundles/org.openhab.binding.sensibo/src/main/java/org/openhab/binding/sensibo/internal/model/SensiboSky.java @@ -22,7 +22,7 @@ import javax.measure.Unit; import javax.measure.quantity.Temperature; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.sensibo.internal.SensiboTemperatureUnitConverter; import org.openhab.binding.sensibo.internal.dto.poddetails.ModeCapabilityDTO; diff --git a/bundles/org.openhab.binding.sensibo/src/test/java/org/openhab/binding/sensibo/internal/WireHelper.java b/bundles/org.openhab.binding.sensibo/src/test/java/org/openhab/binding/sensibo/internal/WireHelper.java index a25d12c00a775..d354b1ac25729 100644 --- a/bundles/org.openhab.binding.sensibo/src/test/java/org/openhab/binding/sensibo/internal/WireHelper.java +++ b/bundles/org.openhab.binding.sensibo/src/test/java/org/openhab/binding/sensibo/internal/WireHelper.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.sensibo.internal; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.lang.reflect.Type; @@ -54,8 +54,7 @@ public T deSerializeResponse(final String jsonClasspathName, final Type type final String json = new String(WireHelper.class.getResourceAsStream(jsonClasspathName).readAllBytes(), StandardCharsets.UTF_8); - final JsonParser parser = new JsonParser(); - final JsonObject o = parser.parse(json).getAsJsonObject(); + final JsonObject o = JsonParser.parseString(json).getAsJsonObject(); assertEquals("success", o.get("status").getAsString()); return gson.fromJson(o.get("result"), type); diff --git a/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml index 5060e10eb0839..a7249ea48dcee 100644 --- a/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml @@ -88,10 +88,9 @@ Represents a device - + pattern-match - true Regular expression used to identify device from received data (must match the whole line) diff --git a/bundles/org.openhab.binding.serialbutton/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.serialbutton/src/main/resources/OH-INF/thing/thing-types.xml index 3157a92c790b0..e753f6c398250 100644 --- a/bundles/org.openhab.binding.serialbutton/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.serialbutton/src/main/resources/OH-INF/thing/thing-types.xml @@ -13,11 +13,10 @@ - + serial-port false - true The serial port that the button is connected to diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index 9006b94dd3759..459036cb55aa1 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -1,6 +1,7 @@ # Shelly Binding This Binding integrates [Shelly devices](https://shelly.cloud) devloped by Allterco. + ![](https://shop.shelly.cloud/image/cache/catalog/shelly_1/s1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_dimmer2/shelly_dimmer2_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_vintage/shelly_vintage_A60-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_plug_s/s_plug_s_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_button1/shelly_button1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_gas/shelly_gas_eu-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_ht/s_ht_x1-80x80.jpg) Allterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API. @@ -15,6 +16,14 @@ The binding gets in sync with the next status refresh. Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB Shelly integration, e.g. firmware update, network communication or log filtering. +Also check out the [Shelly Manager](doc/ShellyManager.md), which +- provides detailed information on your Shellys +- helps to diagnose WiFi issues or device instabilities +- includes some common actions and +- simplifies firmware updates. + +[Shelly Manager](doc/ShellyManager.md) could also act as a firmware upgrade proxy - the device doesn't need to connect directly to the Internet, instead openHAB services as a download proxy, which improves device security. + ## Supported Devices | thing-type | Model | Vendor ID | @@ -30,6 +39,7 @@ Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB | shellydimmer | Shelly Dimmer | SHDM-1 | | shellydimmer2 | Shelly Dimmer2 | SHDM-2 | | shellyix3 | Shelly ix3 | SHIX3-1 | +| shellyuni | Shelly UNI | SHUNI-1 | | shellyplug | Shelly Plug | SHPLG2-1 | | shellyplugs | Shelly Plug-S | SHPLG-S | | shellyem | Shelly EM with integrated Power Meters | SHEM | @@ -255,16 +265,20 @@ The following trigger types are sent: |Event Type |Description | |-------------------|---------------------------------------------------------------------------------------------------------------| -|SHORT_PRESSED |The button was pressed once for a short time | -|DOUBLE_PRESSED |The button was pressed twice with short delay | -|TRIPLE_PRESSED |The button was pressed three times with short delay | -|LONG_PRESSED |The button was pressed for a longer time | -|SHORT_LONG_PRESSED |A short followed by a long button push | -|LONG_SHORT_PRESSED |A long followed by a short button push | +|SHORT_PRESSED |The button was pressed once for a short time (lastEvent=S) | +|DOUBLE_PRESSED |The button was pressed twice with short delay (lastEvent=SS) | +|TRIPLE_PRESSED |The button was pressed three times with short delay (lastEvent=SSS) | +|LONG_PRESSED |The button was pressed for a longer time (lastEvent=L) | +|SHORT_LONG_PRESSED |A short followed by a long button push (lastEvent=SL) | +|LONG_SHORT_PRESSED |A long followed by a short button push (lastEvent=LS) | Check the channel definitions for the various devices to see if the device supports those events. You could use the Shelly App to set the timing for those events. +If you want to use those events triggering a rule: +- If a physical switch is connected to the Shelly use the input channel(`input` or `input1`/`input2`) to trigger a rule +- For a momentary button use the `button` trigger channel as trigger, channels `lastEvent` and `eventCount` will provide details on the event + ### Alarms The binding provides health monitoring functions for the device. @@ -578,6 +592,23 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the | |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush | | |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. | +### Shelly UNI - Low voltage sensor/actor: shellyuni) + +|Group |Channel |Type |read-only|Description | +|----------|-------------|---------|---------|----------------------------------------------------------------------------| +|relay1 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels | +|relay2 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels | +|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) | +| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) | +| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) | +| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) | +| |voltage |Number |yes |ADCS voltage | +|status |input1 |Switch |yes |State of Input 1 | +| |input2 |Switch |yes |State of Input 2 | +| |button |Trigger |yes |Event trigger, see section Button Events | +| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush | +| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. | + ### Shelly Bulb (thing-type: shellybulb) |Group |Channel |Type |read-only|Description | @@ -775,6 +806,11 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa ### Shelly Motion (thing-type: shellymotion) +Important: The Shelly Motion does only support CoIoT Unicast, which means you need to set the CoIoT peer address. + +- Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or +- Use [Shelly Manager](doc/ShellyManager.md, select Action 'Set CoIoT peer' and the Manager will sets the openHAB IP address as peer address + |Group |Channel |Type |read-only|Description | |----------|---------------|---------|---------|---------------------------------------------------------------------| |sensors |motion |Switch |yes |ON: Motion was detected | @@ -783,10 +819,17 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa | |illumination |String |yes |Current illumination: dark/twilight/bright | | |vibration |Switch |yes |ON: Vibration detected | | |charger |Switch |yes |ON: USB charging cable is connected external power supply activated. | +| |motionActive |Switch |yes |ON: Motion detection is currently active | +| |sensorSleepTime|Number |no |Specifies the number of sec the sensor should not report events ] | |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | |battery |batteryLevel |Number |yes |Battery Level in % | | |lowBattery |Switch |yes |Low battery alert (< 20%) | +Use case for the 'sensorSleepTime': +You have a Motion controlling your light. +You switch off the light and want to leave the room, but the motion sensor immediately switches light back on. +Using 'sensorSleepTime' you could suppress motion events while leaving the room, e.g. for 5sec and the light doesn's switch on. + ### Shelly Button 1 (thing-type: shellybutton1) |Group |Channel |Type |read-only|Description | diff --git a/bundles/org.openhab.binding.shelly/doc/ShellyManager.md b/bundles/org.openhab.binding.shelly/doc/ShellyManager.md new file mode 100644 index 0000000000000..909d373528700 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/doc/ShellyManager.md @@ -0,0 +1,186 @@ +# Shelly Manager + +The Shelly Manager is a small extension to the binding, which provides some low level information on the Shelly Devices, but also provides some functions to manage the devices. + +To open the Shelly Manage launch the following URL in your browser +- http://<openHAB IP address>:8080/shelly/manager or +- http://<openHAB IP address>:8443/shelly/manager + +Maybe you need to change the port matching your setup. + +Shelly Manager makes you various device insights available to get an overview of your Shellys +- Get a quick overview that all Shellys operate like expected, statistical data will help to identify issues +- Have some basic setting actions integrated, which help to do an easy setup of new Shellys added to openHAB +- Make firmware updates way easier - filter 'Update available' + integrated 2-click update +- Provide a firmware download proxy, which allows to separate your Shellys from the Internet (improved device security) + +## Overview + +Once the Shelly Manager is opened an overview of all Shelly devices added as a Thing are displayed. +Things which are not discovered or still site in the Inbox will not be displayed. + +![](images/manager/overview.png) + +You'll see a bunch of technical details, which are not available as channels or in the Thing properties. +This includes information on the device communication stability. +The statistic gives you a good overview if device communication is stable or a relevant number of timeouts need to be recovered. +In this case you should verify the WiFi coverage or other options to improve stability. + +The following information is available +|Column |Description | +|--------------------|---------------------------------------------------------------------------------| +|S |Thing Status - hover over the icon to see more details | +|Name |Device name - hover over the name to get more details | +|Cloud Status Icon |Indicates the status of the Shelly Cloud feature: disabled/enabled/connected | +|MQQT Status Icon |Indicates the staus of the MQTT featured disabled/enabled/connected | +|Refresh button |Trigger a status refresh in background, maybe you need to click more than once | +|Device IP |Assigned IP address, click to open the device’s Web UI in a separate browser tab | +|WiFi Network |SSID of the connected WiFi network | +|WiFi Signal |WiFi signal strength, 0=none, 4=very good | +|Battery Level |Remaining capacity of the battery | +|Heartbeat |Last time a response or an event was received from the device | +|Actions |Drop down with some actions, see below | +|Firmware |Current firmware release | +|Update avail |yes indicates that a firmware update is available | +|Versions |List available firmware versions: prod, beta or archived | +|Uptime |Number of seconds since last device restart | +|Internal Temp |Device internal temperature. Max is depending on device type. | +|Update Period |Timeout for device refresh | +|Remaining Watchdog |Shows number of seconds until device will go offline if no update is received | +|Events |Increases on every event triggered by the device or the binding | +|Last Event |Type of last event or alarm (refer README.md for details) | +|Event Time |When was last event received | +|Device Restarts |Number of detected restarts. This is ok on firmware updates, otherwise indicates a crash | +|Timeout Errors |Number of API timeouts, could be an indication for an unstable connection | +|Timeouts recovered |The binding does retries and timeouts and counts successful recoveries | +|CoIoT Messages |Number of received CoIoT messages, must be >= 2 to indicate CoIoT working | +|CoIoT Errors |Number of CoIoT messages, which can't be processed. >0 indicates firmware issues | + +The column S and Name display more information when hovering with the mouse over the entries. + +![](images/manager/overview_devstatus.png) +![](images/manager/overview_devsettings.png) + +### Device Filters +|Filter |Description | +|--------------------|---------------------------------------------------------------------------------| +|All |Clear filter / display all devices | +|Online only |Filter on devices with Thing Status = ONLINE | +|Inactive only |Filter on devices, which are not initialized for in Thing Status = OFFLINE | +|Needs Attention |Filter on devices, which need attention (setup/connectivity issues), see below | +|Update available |Filter on devices having a new firmware version available | +|Unprotected |Filter on devices, which are currently not password protected | + +Beside the Device Filter box you see a refresh button. +At the bottom right you see number of displayed devices vs. number of total devices. +A click triggers a background status update for all devices rather only the selected one when clicking of the refresh button in the device lines. + +Filter 'Needs Attention': +This is a dynamic filter, which helps to identify devices having some kind of setup / connectivity or operation issues. +The binding checks the following conditions +- Thing status != ONLINE: Use the 'Inactive Only' filter to find those devices, check openhab.log +- WIFISIGNAL: WiFi signal strength < 2 - this usually leads into connectivity problems, check positioning of portable devices or antenna direction. +- LOWBATTERY: The remaining battery is < 20% (configuration in Thing Configuration), consider to replace the battery +Watch out for bigger number of timeout errors. +- Device RESTARTED: Indicates a firmware problem / crash if this happens without a device reboot or firmware update (timestamp is included) +- OVERTEMP / OVERLOAD / LOADERROR: There are problems with the physical installation of the device, check specifications, wiring, housing! +- SENSORERROR: A sensor error / malfunction was detected, check product documentation +- NO_COIOT_DISCOVERY: The CoIoT discovery has not been completed, check IP network configuration, re-discover the device +- NO_COIOT_MULTICAST: The CoIoT discovery could be completed, but the device is not receiving CoIoT status updates. +You might try to switch to CoIoT Peer mode, in this case the device doesn't use IP Multicast and sends updates directly to the openHAB host. + +The result is shown in the Device Status tooltip. + +### Device settings & status + +When hovering with the mouse over the status icon or the device name you'll get additional information settings and status. + +### Device Status + +|Status |Description | +|--------------------|---------------------------------------------------------------------------------| +|Status |Thing status, sub-status and description as you know it from openHAB | +|CoIoT Status |CoIoT status: enabled or disabled | +|CoIoT Destination |CoIoT Peer address (ip address:port) or Multicast | +|Cloud Status |Status of the Shelly Cloud connection: disabled, enabled, connected | +|MQTT Status |MQTT Status: disabled, enabled, connected | +|Actions skipped |Number of actions skipped by the device, usually 0 | +|Max Internal Temp |Maximum internal temperature, check device specification for valid range | + +### Device Settings + +|Setting |Description | +|--------------------|---------------------------------------------------------------------------------| +|Shelly Device Name |Device name according to device settings | +|Device Hardware Rev |Hardware revision of the device | +|Device Type |Device Type ID | +|Device Mode |Selected mode for dual mode devices (relay/roller or white/color) | +|Firmware Version |Current firmware version | +|Network Name |Network name of the device used for mDNS | +|MAC Address |Unique hardware/network address of the device | +|Discoverable |true: the device can be discovered using mDNS, false: device is hidden | +|WiFi Auto Recovery |enabled: the device will automatically reboot when WiFi connect fails | +|Timezone |Configured device zone (see device settings) | +|Time server |Configured time server (use device UI to change) | + +### Actions + +The Shelly Manager provides the following actions when the Thing is ONLINE. +They are available in the dropdown list in column Actions. + +|Action |Description | +|---------------------|---------------------------------------------------------------------------------| +|Reset Statistics |Resets device statistic and clear the last alarm | +|Restart |Restart the device and reconnect to WiFi | +|Protect |Use binding's default credentials to protect device access with user and password| +|Set CoIoT Peer |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates | +|Set CoIoT Multicast |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates | +|Enable Cloud |Enable the Shelly Cloud connectivity | +|Disable Cloud |Disable the Shelly Cloud connectivity (takes about 15sec to become active) | +|Reconnect WiFi |Sensor devices only: Clears the STA/AP list and reconnects to strongest AP | +|Enable WiFi Roaming |The device will connect to the strongest AP when roadming is enabled | +|Disable WiFi Roaming |Disable Access Point Roaming, device will periodically search for better APs | +|Enable WiFi Recovery |Enables auto-restart if device detects persistent WiFi connectivity issues | +|Disable WiFi Recovery|Disables device auto-restart ion persistent WiFi connectivity issues | +|Factory Reset |Performs a **factory reset**; Attention: The device will lose its configuration | +|Enable Device Debug |Enables on-device debug log - activate only when requested by Allterco support | +|Get Debug Log |Retrieve and display device debug output | +|Get Debug Log1 |Retrieve and display 2nd device debug output | +|Factory Reset |Performs **firmware reset**; Attention: The device will lose its configuration | + +Note: Various actions available only for certain devices or when using a minimum firmware version. + +![](images/manager/overview_actions.png) + +## Firmware Update + +The Shelly Manager simplifies the firmware update. +You could select between different versions using the drop down list on the overview page. + +Shelly Manager integrates different sources +- Allterco official releases: production and beta release (like in the device UI) +- Older firmware release from the firmware archive - this is a community service +- You could specify any custom URL providing the firmware image (e.g. a local web server), which is accessible for the device using http + +| | | +|-|-| +|![](images/manager/overview_versions.png)|All firmware releases are combined to the selection list.
Click on the version you want to install and Shelly Manager will generate the requested URL to trigger the firmware upgrade.| + +The upgrade starts if you click "Perform Update". + +![](images/manager/fwupgrade.png) + +The device will download the firmware file, installs the update and restarts the device. +Depending on the device type this takes between 10 and 60 seconds. +The binding will automatically recover the device with the next status check (as usual). + +### Connection types + +You could choose between 3 different update types +* Internet: This triggers the regular update; the device needs to be connected to the Internet +* Use openHAB as a proxy: In this case the binding directs the device to request the firmware from the openHAB system. +The binding will then download the firmware from the selected sources and passes this transparently to the device. +This provides a security benefit: The device doesn't require Internet access, only the openHAB host, which could be filtered centrally. +* Custom URL: In this case you could specify + +The binding manages the download request with the proper download URL. diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png b/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png new file mode 100644 index 0000000000000..0b50b67050fbd Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png new file mode 100644 index 0000000000000..5cf5f4b0f682f Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png new file mode 100644 index 0000000000000..70fdfd6e200fd Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png new file mode 100644 index 0000000000000..0be8e1b6b55a0 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png new file mode 100644 index 0000000000000..f265710f0a1e0 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png new file mode 100644 index 0000000000000..c01a79fbe9999 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png index 8a2395c7767a9..0d09bdc7e65a3 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png index d80b49e7220cd..af4ef7a5e597f 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png index 112b640965a84..2dd54901f8f61 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png index 5cb5a56cbb892..c0b01c523f220 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png index 3c970c05f01cf..fc9296d91620e 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png index 628bfdb76eef0..799d64c50db10 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png index c6432cdb973a9..07888025e2f8a 100644 Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png differ diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java index c3bf59eaa0a13..09ca61a5301ec 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java @@ -164,7 +164,7 @@ public class ShellyBindingConstants { THING_TYPE_SHELLYVINTAGE, THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS, THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN, - THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, /* THING_TYPE_SHELLMOTION, */ + THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLMOTION, THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet())); // Thing Configuration Properties @@ -194,7 +194,6 @@ public class ShellyBindingConstants { public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors"; public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered"; public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable"; - public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh"; // Relay public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay"; @@ -249,8 +248,10 @@ public class ShellyBindingConstants { public static final String CHANNEL_SENSOR_VALVE = "valve"; public static final String CHANNEL_SENSOR_SSTATE = "status"; // Shelly Gas public static final String CHANNEL_SENSOR_ALARM_STATE = "alarmState"; + public static final String CHANNEL_SENSOR_MOTION_ACT = "motionActive"; public static final String CHANNEL_SENSOR_MOTION = "motion"; public static final String CHANNEL_SENSOR_MOTION_TS = "motionTimestamp"; + public static final String CHANNEL_SENSOR_SLEEPTIME = "sensorSleepTime"; public static final String CHANNEL_SENSOR_ERROR = "lastError"; // External sensors for Shelly1/1PM @@ -325,6 +326,7 @@ public class ShellyBindingConstants { public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+ public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+ public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+ + public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature // Alarm types/messages public static final String ALARM_TYPE_NONE = "NONE"; @@ -333,6 +335,7 @@ public class ShellyBindingConstants { public static final String ALARM_TYPE_OVERPOWER = "OVERPOWER"; public static final String ALARM_TYPE_OVERLOAD = "OVERLOAD"; public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR"; + public static final String ALARM_TYPE_SENSOR_ERROR = "SENSOR_ERROR"; public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY"; // Event types diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java index 9e0d3c02b0e6c..737094e72c7b6 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java @@ -14,6 +14,7 @@ import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -25,6 +26,7 @@ import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler; import org.openhab.binding.shelly.internal.handler.ShellyLightHandler; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler; import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler; import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; @@ -54,12 +56,14 @@ @NonNullByDefault @Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly") public class ShellyHandlerFactory extends BaseThingHandlerFactory { + private static final Set SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS; + private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class); private final HttpClient httpClient; private final ShellyTranslationProvider messages; private final ShellyCoapServer coapServer; - private final Set deviceListeners = ConcurrentHashMap.newKeySet(); - private static final Set SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS; + + private final Map deviceListeners = new ConcurrentHashMap<>(); private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration(); private String localIP = ""; private int httpPort = -1; @@ -129,7 +133,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } if (handler != null) { - deviceListeners.add(handler); + String uid = thing.getUID().getAsString(); + deviceListeners.put(uid, handler); + logger.debug("Thing handler for uid {} added, total things = {}", uid, deviceListeners.size()); return handler; } @@ -137,13 +143,18 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return null; } + public Map getThingHandlers() { + return new HashMap<>(deviceListeners); + } + /** * Remove handler of things. */ @Override protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) { if (thingHandler instanceof ShellyBaseHandler) { - deviceListeners.remove(thingHandler); + String uid = thingHandler.getThing().getUID().getAsString(); + deviceListeners.remove(uid); } } @@ -158,8 +169,9 @@ protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) { public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType, Map parameters) { logger.trace("{}: Dispatch event to thing handler", deviceName); - for (ShellyBaseHandler listener : deviceListeners) { - if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) { + for (Map.Entry listener : deviceListeners.entrySet()) { + ShellyBaseHandler thingHandler = listener.getValue(); + if (thingHandler.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) { // event processed return; } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java index 739861d62eaa7..ca1d1379a93a4 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings; import org.openhab.core.thing.CommonTriggerEvents; import com.google.gson.annotations.SerializedName; @@ -35,6 +36,7 @@ public class ShellyApiJsonDTO { public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud"; public static final String SHELLY_URL_LIST_IR = "/ir/list"; public static final String SHELLY_URL_SEND_IR = "/ir/emit"; + public static final String SHELLY_URL_RESTART = "/reboot"; public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay"; public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay"; @@ -225,12 +227,23 @@ public class ShellyApiJsonDTO { public static final String SHELLY_TEMP_CELSIUS = "C"; public static final String SHELLY_TEMP_FAHRENHEIT = "F"; + // Motion + public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset + + // CoIoT Multicast setting + public static final String SHELLY_COIOT_MCAST = "mcast"; + public static class ShellySettingsDevice { public String type; public String mac; public String hostname; public String fw; public Boolean auth; + + @SerializedName("coiot") // Shelly Motion Multicast Endpoint + public String coiot; + public Integer longid; + @SerializedName("num_outputs") public Integer numOutputs; @SerializedName("num_meters") @@ -261,7 +274,7 @@ public static class ShellySettingsWiFiNetwork { } public static class ShellySettingsMqtt { - public Boolean enabled; + public Boolean enable; public String server; public String user; @SerializedName("reconnect_timeout_max") @@ -286,10 +299,17 @@ public static class ShellySettingsMqtt { public static class ShellySettingsCoiot { // FW 1.6+ @SerializedName("update_period") public Integer updatePeriod; + public Boolean enabled; // Motion 1.0.7: Coap can be disabled + public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast + } + + public static class ShellyStatusMqtt { + public Boolean connected; } public static class ShellySettingsSntp { public String server; + public Boolean enabled; } public static class ShellySettingsLogin { @@ -313,10 +333,6 @@ public static class ShellyStatusCloud { public Boolean connected; } - public static class ShellyStatusMqtt { - public Boolean connected; - } - public static class ShellySettingsHwInfo { @SerializedName("hw_revision") public String hwRevision; @@ -513,6 +529,8 @@ public static class ShellySettingsUpdate { public String newVersion; @SerializedName("old_version") public String oldVersion; + @SerializedName("beta_version") + public String betaVersion; } public static class ShellySettingsGlobal { @@ -524,8 +542,13 @@ public static class ShellySettingsGlobal { public ShellySettingsWiFiNetwork wifiSta; @SerializedName("wifi_sta1") public ShellySettingsWiFiNetwork wifiSta1; - // public ShellySettingsMqtt mqtt; // not used for now - // public ShellySettingsSntp sntp; // not used for now + @SerializedName("wifirecovery_reboot_enabled") + public Boolean wifiRecoveryReboot; // FW 1.10+ + @SerializedName("ap_roaming") + public ShellyApRoaming apRoaming; // FW 1.10+ + + public ShellySettingsMqtt mqtt; // not used for now + public ShellySettingsSntp sntp; // not used for now public ShellySettingsCoiot coiot; // Firmware 1.6+ public ShellySettingsLogin login; @SerializedName("pin_code") @@ -536,10 +559,13 @@ public static class ShellySettingsGlobal { public Boolean discoverable; // FW 1.6+ public String fw; @SerializedName("build_info") - ShellySettingsBuildInfo buildInfo; - ShellyStatusCloud cloud; + public ShellySettingsBuildInfo buildInfo; + public ShellyStatusCloud cloud; @SerializedName("sleep_mode") public ShellySensorSleepMode sleepMode; // FW 1.6 + @SerializedName("external_power") + public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense + public Boolean debug_enable; // FW 1.10+ public String timezone; public Double lat; @@ -616,6 +642,18 @@ public static class ShellySettingsGlobal { @SerializedName("favorites_enabled") public Boolean favoritesEnabled; public ArrayList favorites; + + // Motion + public ShellyMotionSettings motion; + @SerializedName("tamper_sensitivity") + public Integer tamperSensitivity; + @SerializedName("dark_threshold") + public Integer darkThreshold; + @SerializedName("twilight_threshold") + public Integer twilightThreshold; + + @SerializedName("sleep_time") // Shelly Motion + public Integer sleepTime; } public static class ShellySettingsAttributes { @@ -634,11 +672,17 @@ public static class ShellySettingsAttributes { public String fw; // current FW version } + public static class ShellyActionsStats { + public Integer skipped; + } + public static class ShellySettingsStatus { public String name; // FW 1.8: Symbolic Device name is configurable @SerializedName("wifi_sta") public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details + public ShellyStatusCloud cloud; + public ShellyStatusMqtt mqtt; public String time; public Integer serial; @@ -648,6 +692,8 @@ public static class ShellySettingsStatus { public Boolean discoverable; // FW 1.6+ @SerializedName("cfg_changed_cnt") public Integer cfgChangedCount; // FW 1.8 + @SerializedName("actions_stats") + public ShellyActionsStats astats; public ArrayList relays; public ArrayList rollers; @@ -680,6 +726,9 @@ public static class ShellySettingsStatus { public Long fsFree; public Long uptime; + @SerializedName("sleep_time") // Shelly Motion + public Integer sleepTime; + public String json; } @@ -845,6 +894,15 @@ public static class ShellyControlRoller { public Integer currentPos; // current position 0..100, 100=open } + public class ShellyOtaCheckResult { + public String status; + } + + public class ShellyApRoaming { + public Boolean enabled; + public Integer threshold; + } + public class ShellySensorSleepMode { public Integer period; public String unit; @@ -898,6 +956,17 @@ public static class ShellySensorAccel { public Integer vibration; // Whether vibration is detected } + public static class ShellyMotionSettings { + public Integer sensitivity; + @SerializedName("blind_time_minutes") + public Integer blindTimeMinutes; + @SerializedName("pulse_count") + public Integer pulseCount; + @SerializedName("operating_mode") + public Integer operatingMode; + public Boolean enabled; + } + public static class ShellyExtTemperature { public static class ShellyShortTemp { public Double tC; // temperature in deg C @@ -943,8 +1012,6 @@ public static class ShellyADC { public Boolean motion; // Shelly Sense: true=motion detected public Boolean charger; // Shelly Sense: true=charger connected - @SerializedName("external_power") - public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense @SerializedName("act_reasons") public List actReasons; // HT/Smoke/Flood: list of reasons which woke up the device diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java index b1a2bc143098d..4cadd3f96af20 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java @@ -63,6 +63,10 @@ public boolean isHttpOk() { return httpCode == OK_200; } + public boolean isNotFound() { + return httpCode == NOT_FOUND_404; + } + public boolean isHttpAccessUnauthorized() { return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED)); } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java index 00cdb38fa12f9..45cc461cf527c 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java @@ -18,14 +18,18 @@ import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus; +import org.openhab.binding.shelly.internal.util.ShellyVersionDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,11 +45,13 @@ @NonNullByDefault public class ShellyDeviceProfile { private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class); + private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?"); public boolean initialized = false; // true when initialized public String thingName = ""; public String deviceType = ""; + public boolean extFeatures = false; public String settingsJson = ""; public ShellySettingsGlobal settings = new ShellySettingsGlobal(); @@ -54,11 +60,12 @@ public class ShellyDeviceProfile { public String hostname = ""; public String mode = ""; public boolean discoverable = true; + public boolean auth = false; + public boolean alwaysOn = true; public String hwRev = ""; public String hwBatchId = ""; public String mac = ""; - public String fwId = ""; public String fwVersion = ""; public String fwDate = ""; @@ -81,6 +88,7 @@ public class ShellyDeviceProfile { public boolean isSensor = false; // true for HT & Smoke public boolean hasBattery = false; // true if battery device public boolean isSense = false; // true if thing is a Shelly Sense + public boolean isMotion = false; // true if thing is a Shelly Sense public boolean isHT = false; // true for H&T public boolean isDW = false; // true for Door Window sensor public boolean isButton = false; // true for a Shelly Button 1 @@ -91,6 +99,8 @@ public class ShellyDeviceProfile { public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10; + public String coiotEndpoint = ""; + public Map irCodes = new HashMap<>(); // Sense: list of stored IR codes public ShellyDeviceProfile() { @@ -112,12 +122,13 @@ public ShellyDeviceProfile initialize(String thingType, String json) throws Shel hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty() ? settings.device.hostname.toLowerCase() : "shelly-" + mac.toUpperCase().substring(6, 11); - mode = !getString(settings.mode).isEmpty() ? getString(settings.mode).toLowerCase() : ""; + mode = getString(settings.mode).toLowerCase(); hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : ""; hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : ""; fwDate = substringBefore(settings.fw, "/"); - fwVersion = substringBetween(settings.fw, "/", "@"); - fwId = substringAfter(settings.fw, "@"); + fwVersion = extractFwVersion(settings.fw); + ShellyVersionDTO version = new ShellyVersionDTO(); + extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0; discoverable = (settings.discoverable == null) || settings.discoverable; inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR); @@ -126,8 +137,6 @@ public ShellyDeviceProfile initialize(String thingType, String json) throws Shel if ((numRelays > 0) && (settings.relays == null)) { numRelays = 0; } - isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2); - isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); hasRelays = (numRelays > 0) || isDimmer; numRollers = getInteger(settings.device.numRollers); numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0; @@ -174,6 +183,9 @@ public void initFromThingType(String name) { return; } + isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2); + isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); + isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR); isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR); @@ -188,21 +200,21 @@ public void initFromThingType(String name) { boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR); boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR); boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR); - boolean isMotion = thingType.equals(THING_TYPE_SHELLYMOTION_STR); isHT = thingType.equals(THING_TYPE_SHELLYHT_STR); isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR); + isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR); isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR); isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR); isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR); - isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isSense; - hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion; // we assume that Sense is connected to - // the charger + isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense; + hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion; + + alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode) } public void updateFromStatus(ShellySettingsStatus status) { if (hasRelays) { - // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after - // initialization + // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init if (status.inputs != null) { numInputs = status.inputs.size(); } @@ -304,7 +316,8 @@ public boolean inButtonMode(int idx) { logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType); return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE) - || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON); + || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON) + || btnType.equalsIgnoreCase(SHELLY_BTNT_DETACHED); } public int getRollerFav(int id) { @@ -314,4 +327,27 @@ public int getRollerFav(int id) { } return -1; } + + public static String extractFwVersion(@Nullable String version) { + if (version != null) { + // fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.) + String vers = version.replace("/v.1.10-", "/v1.10.0-"); + + // Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master + Matcher matcher = VERSION_PATTERN.matcher(vers); + if (matcher.find()) { + return matcher.group(0); + } + } + return ""; + } + + public boolean coiotEnabled() { + if ((settings.coiot != null) && (settings.coiot.enabled != null)) { + return settings.coiot.enabled; + } + + // If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled + return true; + } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java index 7a383f2a027cd..df7bd8a9437f9 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java @@ -32,11 +32,14 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay; @@ -88,6 +91,14 @@ public ShellySettingsDevice getDevInfo() throws ShellyApiException { return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class); } + public String setDebug(boolean enabled) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class); + } + + public String getDebugLog(String id) throws ShellyApiException { + return callApi("/debug/" + id, String.class); + } + /** * Initialize the device profile * @@ -192,26 +203,28 @@ public ShellyStatusSensor getSensorStatus() throws ShellyApiException { .convert(getDouble(status.tmp.value)); status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f; } - if ((status.charger == null) && (status.externalPower != null)) { + if ((status.charger == null) && (profile.settings.externalPower != null)) { // SHelly H&T uses external_power, Sense uses charger - status.charger = status.externalPower != 0; + status.charger = profile.settings.externalPower != 0; } - return status; } - public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException { + public void setTimer(int index, String timerName, int value) throws ShellyApiException { String type = SHELLY_CLASS_RELAY; if (profile.isRoller) { type = SHELLY_CLASS_ROLLER; } else if (profile.isLight) { type = SHELLY_CLASS_LIGHT; } - String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" - + ((Integer) value.intValue()).toString(); + String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value; request(uri); } + public void setSleepTime(int value) throws ShellyApiException { + request(SHELLY_URL_SETTINGS + "?sleep_time=" + value); + } + public void setLedStatus(String ledName, Boolean value) throws ShellyApiException { request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE)); } @@ -228,6 +241,63 @@ public void setLightSetting(String parm, String value) throws ShellyApiException request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value); } + public ShellySettingsLogin getLoginSettings() throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class); + } + + public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + user + "&password=" + password, + ShellySettingsLogin.class); + } + + public String getCoIoTDescription() throws ShellyApiException { + try { + return callApi("/cit/d", String.class); + } catch (ShellyApiException e) { + if (e.getApiResult().isNotFound()) { + return ""; // only supported by FW 1.10+ + } + throw e; + } + } + + public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class); + } + + public String deviceReboot() throws ShellyApiException { + return callApi(SHELLY_URL_RESTART, String.class); + } + + public String factoryReset() throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class); + } + + public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException { + return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check + } + + public String setWiFiRecovery(boolean enable) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"), + String.class); // FW 1.10+: Enable auto-restart on WiFi problems + } + + public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming + return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class); + } + + public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan + return callApi("/sta_cache_reset", String.class); + } + + public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException { + return callApi("/ota?" + uri, ShellySettingsUpdate.class); + } + + public String setCloud(boolean enabled) throws ShellyApiException { + return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class); + } + /** * Change between White and Color Mode * @@ -527,7 +597,8 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri) throws Shell if (contentResponse.getStatus() != HttpStatus.OK_200) { throw new ShellyApiException(apiResult); } - if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) { + if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/") + && !url.contains("/sta_cache_reset")) { throw new ShellyApiException("Unexpected response: " + response); } } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) { diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java index db9d7f614af1e..723f7607985bd 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor; @@ -50,6 +51,7 @@ public class ShellyCoIoTProtocol { protected final String thingName; protected final ShellyBaseHandler thingHandler; protected final ShellyDeviceProfile profile; + protected final ShellyHttpApi api; protected final Map blkMap; protected final Map sensorMap; private final Gson gson = new GsonBuilder().create(); @@ -68,6 +70,7 @@ public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map this.blkMap = blkMap; this.sensorMap = sensorMap; this.profile = thingHandler.getProfile(); + this.api = thingHandler.getApi(); } protected boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen sen, CoIotSensor s, @@ -83,7 +86,7 @@ protected boolean handleStatusUpdate(List sensorUpdates, CoIotDescr switch (sen.type.toLowerCase()) { case "b": // BatteryLevel + updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL, - toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT)); + toQuantityType(s.value, 0, Units.PERCENT)); break; case "h" /* Humidity */: updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java index 78e2cc7f01e3b..a239f41e0f2fa 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java @@ -111,8 +111,12 @@ public boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen break; case "1103": // roller_0: S, rollerPos, 0-100, unknown -1 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS)); + logger.debug("{}: CoAP update roller position: control={}, position={}", thingName, + SHELLY_MAX_ROLLER_POS - pos, pos); updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL, toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); + updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS, + toQuantityType((double) pos, Units.PERCENT)); break; case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr)); @@ -297,11 +301,15 @@ public boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen break; case "3119": // Motion timestamp // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1}, - updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS, - getTimestamp(getString(profile.settings.timezone), (long) s.value)); + if (s.value != 0) { + updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS, + getTimestamp(getString(profile.settings.timezone), (long) s.value)); + } break; case "3120": // motionActive // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1}, + updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT, + getTimestamp(getString(profile.settings.timezone), (long) s.value)); break; case "6108": // A, gas, none/mild/heavy/test or unknown diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java index 8b2db84103249..a35dc157fa1a9 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java @@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyApiException; import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter; @@ -80,18 +81,23 @@ public class ShellyCoapHandler implements ShellyCoapListener { private @Nullable CoapClient statusClient; private Request reqDescription = new Request(Code.GET, Type.CON); private Request reqStatus = new Request(Code.GET, Type.CON); - private boolean discovering = false; + private boolean updatesRequested = false; + private int coiotPort = COIOT_PORT; + private long coiotMessages = 0; + private long coiotErrors = 0; private int lastSerial = -1; private String lastPayload = ""; private Map blkMap = new LinkedHashMap<>(); private Map sensorMap = new LinkedHashMap<>(); private ShellyDeviceProfile profile; + private ShellyHttpApi api; public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) { this.thingHandler = thingHandler; this.thingName = thingHandler.thingName; this.profile = thingHandler.getProfile(); + this.api = thingHandler.getApi(); this.coapServer = coapServer; this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2 @@ -118,8 +124,12 @@ public synchronized void start(String thingName, ShellyThingConfiguration config } logger.debug("{}: Starting CoAP Listener", thingName); - coapServer.start(config.localIp, this); - statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS)) + if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) { + String ps = substringAfter(profile.coiotEndpoint, ":"); + coiotPort = Integer.parseInt(ps); + } + coapServer.start(config.localIp, coiotPort, this); + statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS)) .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint()); @Nullable Endpoint endpoint = null; @@ -130,6 +140,7 @@ public synchronized void start(String thingName, ShellyThingConfiguration config logger.warn("{}: Unable to initialize CoAP access (network error)", thingName); throw new ShellyApiException("Network initialization failed"); } + discover(); } catch (SocketException e) { logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage()); @@ -152,10 +163,39 @@ public boolean isStarted() { @Override public void processResponse(@Nullable Response response) { if (response == null) { + coiotErrors++; return; // other device instance } + ResponseCode code = response.getCode(); + if (code != ResponseCode.CONTENT) { + // error handling + logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code, + response.getPayloadString()); + coiotErrors++; + return; + } + + List