mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
parent
bbf1a7fd29
commit
6df6783b60
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,3 +21,4 @@ features/**/src/main/feature
|
||||
|
||||
.vscode
|
||||
.factorypath
|
||||
pom.xml.versionsBackup
|
||||
|
83
CODEOWNERS
83
CODEOWNERS
@ -5,8 +5,10 @@
|
||||
* @openhab/add-ons-maintainers
|
||||
|
||||
# Add-on maintainers:
|
||||
/bundles/org.openhab.binding.adorne/ @theiding
|
||||
/bundles/org.openhab.binding.airquality/ @kubawolanin
|
||||
/bundles/org.openhab.binding.airvisualnode/ @3cky
|
||||
/bundles/org.openhab.binding.alarmdecoder/ @bobadair @billfor
|
||||
/bundles/org.openhab.binding.allplay/ @dominicdesu
|
||||
/bundles/org.openhab.binding.amazondashbutton/ @OLibutzki
|
||||
/bundles/org.openhab.binding.amazonechocontrol/ @mgeramb
|
||||
@ -14,20 +16,31 @@
|
||||
/bundles/org.openhab.binding.astro/ @gerrieg
|
||||
/bundles/org.openhab.binding.atlona/ @tmrobert8
|
||||
/bundles/org.openhab.binding.autelis/ @digitaldan
|
||||
/bundles/org.openhab.binding.automower/ @maxpg
|
||||
/bundles/org.openhab.binding.avmfritz/ @cweitkamp
|
||||
/bundles/org.openhab.binding.bigassfan/ @mhilbush
|
||||
/bundles/org.openhab.binding.bluetooth/ @cdjackson @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.airthings/ @paulianttila
|
||||
/bundles/org.openhab.binding.bluetooth.am43/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.bluegiga/ @cdjackson @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.bluez/ @cdjackson @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.blukii/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.daikinmadoka/ @blafois
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
/bundles/org.openhab.binding.boschindego/ @jofleck
|
||||
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
|
||||
/bundles/org.openhab.binding.bsblan/ @hypetsch
|
||||
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
|
||||
/bundles/org.openhab.binding.buienradar/ @gedejong
|
||||
/bundles/org.openhab.binding.caddx/ @jossuar
|
||||
/bundles/org.openhab.binding.chromecast/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.cm11a/ @BobRak
|
||||
/bundles/org.openhab.binding.comfoair/ @boehan
|
||||
/bundles/org.openhab.binding.coolmasternet/ @projectgus
|
||||
/bundles/org.openhab.binding.daikin/ @caffineehacker @psmedley
|
||||
/bundles/org.openhab.binding.coronastats/ @DerOetzi
|
||||
/bundles/org.openhab.binding.daikin/ @caffineehacker
|
||||
/bundles/org.openhab.binding.danfossairunit/ @pravussum
|
||||
/bundles/org.openhab.binding.darksky/ @cweitkamp
|
||||
/bundles/org.openhab.binding.deconz/ @davidgraeff
|
||||
/bundles/org.openhab.binding.denonmarantz/ @jwveldhuis
|
||||
@ -36,16 +49,23 @@
|
||||
/bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor
|
||||
/bundles/org.openhab.binding.dmx/ @J-N-K
|
||||
/bundles/org.openhab.binding.doorbird/ @mhilbush
|
||||
/bundles/org.openhab.binding.draytonwiser/ @andrew-schofield
|
||||
/bundles/org.openhab.binding.dscalarm/ @RSStephens
|
||||
/bundles/org.openhab.binding.dsmr/ @Hilbrand
|
||||
/bundles/org.openhab.binding.ecobee/ @mhilbush
|
||||
/bundles/org.openhab.binding.dwdpollenflug/ @DerOetzi
|
||||
/bundles/org.openhab.binding.dwdunwetter/ @limdul79
|
||||
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
|
||||
/bundles/org.openhab.binding.energenie/ @hmerk
|
||||
/bundles/org.openhab.binding.enigma2/ @gdolfen
|
||||
/bundles/org.openhab.binding.enocean/ @fruggy83
|
||||
/bundles/org.openhab.binding.enturno/ @klocsson
|
||||
/bundles/org.openhab.binding.etherrain/ @dfad1469
|
||||
/bundles/org.openhab.binding.evohome/ @Nebula83
|
||||
/bundles/org.openhab.binding.exec/ @kgoderis
|
||||
/bundles/org.openhab.binding.feed/ @svilenvul
|
||||
/bundles/org.openhab.binding.feican/ @Hilbrand
|
||||
/bundles/org.openhab.binding.fmiweather/ @ssalonen
|
||||
/bundles/org.openhab.binding.folding/ @fa2k
|
||||
/bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand
|
||||
/bundles/org.openhab.binding.freebox/ @lolodomo
|
||||
@ -53,13 +73,16 @@
|
||||
/bundles/org.openhab.binding.fsinternetradio/ @paphko
|
||||
/bundles/org.openhab.binding.ftpupload/ @paulianttila
|
||||
/bundles/org.openhab.binding.gardena/ @gerrieg
|
||||
/bundles/org.openhab.binding.gce/ @clinique
|
||||
/bundles/org.openhab.binding.globalcache/ @mhilbush
|
||||
/bundles/org.openhab.binding.gpstracker/ @gbicskei
|
||||
/bundles/org.openhab.binding.gree/ @markus7017
|
||||
/bundles/org.openhab.binding.groheondus/ @FlorianSW
|
||||
/bundles/org.openhab.binding.harmonyhub/ @digitaldan
|
||||
/bundles/org.openhab.binding.hdanywhere/ @kgoderis
|
||||
/bundles/org.openhab.binding.hdpowerview/ @beowulfe
|
||||
/bundles/org.openhab.binding.helios/ @kgoderis
|
||||
/bundles/org.openhab.binding.heliosventilation/ @ramack
|
||||
/bundles/org.openhab.binding.heos/ @Wire82
|
||||
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
|
||||
/bundles/org.openhab.binding.hpprinter/ @cossey
|
||||
@ -67,12 +90,19 @@
|
||||
/bundles/org.openhab.binding.hydrawise/ @digitaldan
|
||||
/bundles/org.openhab.binding.hyperion/ @tavalin
|
||||
/bundles/org.openhab.binding.iaqualink/ @digitaldan
|
||||
/bundles/org.openhab.binding.icalendar/ @daMihe
|
||||
/bundles/org.openhab.binding.icloud/ @pgfeller
|
||||
/bundles/org.openhab.binding.ihc/ @paulianttila
|
||||
/bundles/org.openhab.binding.innogysmarthome/ @ollie-dev
|
||||
/bundles/org.openhab.binding.insteon/ @robnielsen
|
||||
/bundles/org.openhab.binding.ipcamera/ @Skinah
|
||||
/bundles/org.openhab.binding.intesis/ @hmerk
|
||||
/bundles/org.openhab.binding.ipp/ @peuter
|
||||
/bundles/org.openhab.binding.irtrans/ @kgoderis
|
||||
/bundles/org.openhab.binding.ism8/ @hans-reiner
|
||||
/bundles/org.openhab.binding.jablotron/ @octa22
|
||||
/bundles/org.openhab.binding.jeelink/ @vbier
|
||||
/bundles/org.openhab.binding.kaleidescape/ @mlobstein
|
||||
/bundles/org.openhab.binding.keba/ @kgoderis
|
||||
/bundles/org.openhab.binding.km200/ @Markinus
|
||||
/bundles/org.openhab.binding.knx/ @sjka
|
||||
@ -80,20 +110,25 @@
|
||||
/bundles/org.openhab.binding.konnected/ @volfan6415
|
||||
/bundles/org.openhab.binding.kostalinverter/ @cschneider
|
||||
/bundles/org.openhab.binding.lametrictime/ @syphr42
|
||||
/bundles/org.openhab.binding.lcn/ @fwolter
|
||||
/bundles/org.openhab.binding.leapmotion/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.lghombot/ @FluBBaOfWard
|
||||
/bundles/org.openhab.binding.lgtvserial/ @fa2k
|
||||
/bundles/org.openhab.binding.lgwebos/ @sprehn
|
||||
/bundles/org.openhab.binding.lifx/ @wborn
|
||||
/bundles/org.openhab.binding.linky/ @clinique @lolodomo
|
||||
/bundles/org.openhab.binding.linuxinput/ @t-8ch
|
||||
/bundles/org.openhab.binding.lirc/ @kabili207
|
||||
/bundles/org.openhab.binding.logreader/ @paulianttila
|
||||
/bundles/org.openhab.binding.loxone/ @ppieczul
|
||||
/bundles/org.openhab.binding.luftdateninfo/ @weymann
|
||||
/bundles/org.openhab.binding.lutron/ @actong @bobadair
|
||||
/bundles/org.openhab.binding.magentatv/ @markus7017
|
||||
/bundles/org.openhab.binding.mail/ @J-N-K
|
||||
/bundles/org.openhab.binding.max/ @marcelrv
|
||||
/bundles/org.openhab.binding.mcp23017/ @aogorek
|
||||
/bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep
|
||||
/bundles/org.openhab.binding.meteoalerte/ @clinique
|
||||
/bundles/org.openhab.binding.meteoblue/ @9037568
|
||||
/bundles/org.openhab.binding.meteostick/ @cdjackson
|
||||
/bundles/org.openhab.binding.miele/ @kgoderis
|
||||
@ -103,10 +138,17 @@
|
||||
/bundles/org.openhab.binding.milight/ @davidgraeff
|
||||
/bundles/org.openhab.binding.minecraft/ @ibaton
|
||||
/bundles/org.openhab.binding.modbus/ @ssalonen
|
||||
/bundles/org.openhab.binding.modbus.e3dc/ @weymann
|
||||
/bundles/org.openhab.binding.modbus.studer/ @giovannimirulla
|
||||
/bundles/org.openhab.binding.modbus.sunspec/ @mrbig
|
||||
/bundles/org.openhab.binding.modbus.stiebeleltron/ @pail23
|
||||
/bundles/org.openhab.binding.modbus.helioseasycontrols/ @bern77
|
||||
/bundles/org.openhab.binding.monopriceaudio/ @mlobstein
|
||||
/bundles/org.openhab.binding.mqtt/ @davidgraeff
|
||||
/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.mystrom/ @pail23
|
||||
/bundles/org.openhab.binding.nanoleaf/ @raepple
|
||||
/bundles/org.openhab.binding.neato/ @jjlauterbach
|
||||
/bundles/org.openhab.binding.neeo/ @tmrobert8
|
||||
@ -119,9 +161,13 @@
|
||||
/bundles/org.openhab.binding.nibeuplink/ @alexf2015
|
||||
/bundles/org.openhab.binding.nikobus/ @crnjan
|
||||
/bundles/org.openhab.binding.nikohomecontrol/ @mherwege
|
||||
/bundles/org.openhab.binding.novafinedust/ @t2000
|
||||
/bundles/org.openhab.binding.ntp/ @marcelrv
|
||||
/bundles/org.openhab.binding.nuki/ @mkatter
|
||||
/bundles/org.openhab.binding.nuvo/ @mlobstein
|
||||
/bundles/org.openhab.binding.nzwateralerts/ @cossey
|
||||
/bundles/org.openhab.binding.oceanic/ @kgoderis
|
||||
/bundles/org.openhab.binding.ojelectronics/ @EvilPingu
|
||||
/bundles/org.openhab.binding.omnikinverter/ @hansbogert
|
||||
/bundles/org.openhab.binding.onebusaway/ @sdwilsh
|
||||
/bundles/org.openhab.binding.onewiregpio/ @aogorek
|
||||
@ -129,8 +175,11 @@
|
||||
/bundles/org.openhab.binding.onkyo/ @pail23 @paulianttila
|
||||
/bundles/org.openhab.binding.opengarage/ @psmedley
|
||||
/bundles/org.openhab.binding.opensprinkler/ @CrackerStealth @FlorianSW
|
||||
/bundles/org.openhab.binding.openthermgateway/ @ArjenKorevaar
|
||||
/bundles/org.openhab.binding.openuv/ @clinique
|
||||
/bundles/org.openhab.binding.openweathermap/ @cweitkamp
|
||||
/bundles/org.openhab.binding.openwebnet/ @mvalla
|
||||
/bundles/org.openhab.binding.oppo/ @mlobstein
|
||||
/bundles/org.openhab.binding.orvibo/ @tavalin
|
||||
/bundles/org.openhab.binding.paradoxalarm/ @theater
|
||||
/bundles/org.openhab.binding.pentair/ @jsjames
|
||||
@ -143,6 +192,7 @@
|
||||
/bundles/org.openhab.binding.powermax/ @lolodomo
|
||||
/bundles/org.openhab.binding.pulseaudio/ @peuter
|
||||
/bundles/org.openhab.binding.pushbullet/ @hakan42
|
||||
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
|
||||
/bundles/org.openhab.binding.regoheatpump/ @crnjan
|
||||
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
|
||||
/bundles/org.openhab.binding.rme/ @kgoderis
|
||||
@ -150,10 +200,13 @@
|
||||
/bundles/org.openhab.binding.rotel/ @lolodomo
|
||||
/bundles/org.openhab.binding.rotelra1x/ @fa2k
|
||||
/bundles/org.openhab.binding.russound/ @tmrobert8
|
||||
/bundles/org.openhab.binding.sagercaster/ @clinique
|
||||
/bundles/org.openhab.binding.samsungtv/ @paulianttila
|
||||
/bundles/org.openhab.binding.satel/ @druciak
|
||||
/bundles/org.openhab.binding.senechome/ @vctender
|
||||
/bundles/org.openhab.binding.seneye/ @nikotanghe
|
||||
/bundles/org.openhab.binding.sensebox/ @hakan42
|
||||
/bundles/org.openhab.binding.sensibo/ @seime
|
||||
/bundles/org.openhab.binding.serialbutton/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.shelly/ @markus7017
|
||||
/bundles/org.openhab.binding.siemensrds/ @andrewfg
|
||||
@ -162,30 +215,42 @@
|
||||
/bundles/org.openhab.binding.sleepiq/ @syphr42
|
||||
/bundles/org.openhab.binding.smaenergymeter/ @monnimeter
|
||||
/bundles/org.openhab.binding.smartmeter/ @msteigenberger
|
||||
/bundles/org.openhab.binding.smhi/ @pacive
|
||||
/bundles/org.openhab.binding.smartthings/ @BobRak
|
||||
/bundles/org.openhab.binding.snmp/ @J-N-K
|
||||
/bundles/org.openhab.binding.solaredge/ @alexf2015
|
||||
/bundles/org.openhab.binding.solarlog/ @johannrichard
|
||||
/bundles/org.openhab.binding.somfymylink/ @loungeflyz
|
||||
/bundles/org.openhab.binding.somfytahoma/ @octa22
|
||||
/bundles/org.openhab.binding.sonos/ @kgoderis @lolodomo
|
||||
/bundles/org.openhab.binding.sonyaudio/ @freke
|
||||
/bundles/org.openhab.binding.sonyprojector/ @lolodomo
|
||||
/bundles/org.openhab.binding.spotify/ @Hilbrand
|
||||
/bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush
|
||||
/bundles/org.openhab.binding.mpd/ @stefanroellin
|
||||
/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.tellstick/ @jarlebh
|
||||
/bundles/org.openhab.binding.tesla/ @kgoderis
|
||||
/bundles/org.openhab.binding.toon/ @jongj
|
||||
/bundles/org.openhab.binding.tibber/ @kjoglum
|
||||
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
|
||||
/bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
|
||||
/bundles/org.openhab.binding.unifi/ @mgbowman
|
||||
/bundles/org.openhab.binding.upnpcontrol/ @mherwege
|
||||
/bundles/org.openhab.binding.upb/ @marcusb
|
||||
/bundles/org.openhab.binding.urtsi/ @OLibutzki
|
||||
/bundles/org.openhab.binding.valloxmv/ @bjoernbrings
|
||||
/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.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
|
||||
@ -193,29 +258,24 @@
|
||||
/bundles/org.openhab.binding.wemo/ @hmerk
|
||||
/bundles/org.openhab.binding.wifiled/ @rvt @xylo
|
||||
/bundles/org.openhab.binding.windcentrale/ @marcelrv
|
||||
/bundles/org.openhab.binding.wlanthermo/ @CSchlipp
|
||||
/bundles/org.openhab.binding.xmltv/ @clinique
|
||||
/bundles/org.openhab.binding.xmppclient/ @pavel-gololobov
|
||||
/bundles/org.openhab.binding.yamahareceiver/ @davidgraeff @zarusz
|
||||
/bundles/org.openhab.binding.yeelight/ @claell
|
||||
/bundles/org.openhab.binding.zoneminder/ @Mr-Eskildsen
|
||||
/bundles/org.openhab.binding.zway/ @pathec
|
||||
/bundles/org.openhab.extensionservice.marketplace/ @kaikreuzer
|
||||
/bundles/org.openhab.extensionservice.marketplace.automation/ @kaikreuzer
|
||||
/bundles/org.openhab.io.azureiothub/ @nikotanghe
|
||||
/bundles/org.openhab.io.homekit/ @beowulfe
|
||||
/bundles/org.openhab.io.homekit/ @beowulfe @yfre
|
||||
/bundles/org.openhab.io.hueemulation/ @davidgraeff @digitaldan
|
||||
/bundles/org.openhab.io.imperihome/ @pdegeus
|
||||
/bundles/org.openhab.io.javasound/ @kaikreuzer
|
||||
/bundles/org.openhab.io.mqttembeddedbroker/ @davidgraeff
|
||||
/bundles/org.openhab.io.neeo/ @tmrobert8
|
||||
/bundles/org.openhab.io.openhabcloud/ @kaikreuzer
|
||||
/bundles/org.openhab.io.transport.modbus/ @ssalonen
|
||||
/bundles/org.openhab.io.webaudio/ @kaikreuzer
|
||||
/bundles/org.openhab.persistence.mapdb/ @mkhl
|
||||
/bundles/org.openhab.persistence.influxdb/ @lujop
|
||||
/bundles/org.openhab.transform.exec/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.transform.javascript/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.transform.jinja/ @jochen314
|
||||
/bundles/org.openhab.transform.jinja/ @jochen314
|
||||
/bundles/org.openhab.transform.jsonpath/ @clinique
|
||||
/bundles/org.openhab.transform.map/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.transform.regex/ @openhab/add-ons-maintainers
|
||||
@ -233,6 +293,7 @@
|
||||
/itests/org.openhab.binding.feed.tests/ @svilenvul
|
||||
/itests/org.openhab.binding.hue.tests/ @cweitkamp
|
||||
/itests/org.openhab.binding.max.tests/ @marcelrv
|
||||
/itests/org.openhab.binding.modbus.tests/ @ssalonen
|
||||
/itests/org.openhab.binding.mqtt.homeassistant.tests/ @davidgraeff
|
||||
/itests/org.openhab.binding.mqtt.homie.tests/ @davidgraeff
|
||||
/itests/org.openhab.binding.nest.tests/ @wborn
|
||||
@ -241,7 +302,7 @@
|
||||
/itests/org.openhab.binding.tradfri.tests/ @cweitkamp @kaikreuzer
|
||||
/itests/org.openhab.binding.wemo.tests/ @hmerk
|
||||
/itests/org.openhab.io.hueemulation.tests/ @davidgraeff @digitaldan
|
||||
/itests/org.openhab.io.mqttembeddedbroker.tests/ @J-N-K
|
||||
/itests/org.openhab.io.mqttembeddedbroker.tests/ @J-N-K
|
||||
/itests/org.openhab.persistence.mapdb.tests/ @mkhl
|
||||
|
||||
# PLEASE HELP ADDING FURTHER LINES HERE!
|
||||
|
File diff suppressed because it is too large
Load Diff
38
bundles/org.openhab.binding.adorne/.classpath
Normal file
38
bundles/org.openhab.binding.adorne/.classpath
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
23
bundles/org.openhab.binding.adorne/.project
Normal file
23
bundles/org.openhab.binding.adorne/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.adorne</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
13
bundles/org.openhab.binding.adorne/NOTICE
Normal file
13
bundles/org.openhab.binding.adorne/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab2-addons
|
113
bundles/org.openhab.binding.adorne/README.md
Normal file
113
bundles/org.openhab.binding.adorne/README.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Adorne Binding
|
||||
|
||||
The Adorne Binding integrates [Adorne Wi-Fi ready devices](https://www.legrand.us/adorne/products/wireless-whole-house-lighting-controls.aspx) (switches, dimmers, outlets) from [Legrand](https://legrand.com/).
|
||||
|
||||
Legrand attempted to provide a public API based on Samsung's ARTIK Cloud and the initial version of this binding was based on that API.
|
||||
However, Samsung shut down ARTIK Cloud shortly after the release and Legrand has not offered a public API replacement since.
|
||||
That leaves direct interaction with the Adorne Hub as the only control option.
|
||||
Consequently the openHAB server and the Adorne Hub must be located on the same network.
|
||||
|
||||
The Adorne Hub supports a REST API, but unfortunately there is no documentation or official support from Legrand.
|
||||
This binding's implementation of the REST API is motivated by the great work of [sbozarth](https://github.com/sbozarth/homebridge-lc7001) who figured out the API details.
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Type | Description |
|
||||
|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| hub | The Adorne [Hub LC7001](https://www.legrand.us/adorne/products/wireless-whole-house-lighting-controls/lc7001.aspx) serves as the bridge to control all Adorne devices |
|
||||
| switch | All Adorne switches and outlets |
|
||||
| dimmer | All Adorne dimmers |
|
||||
|
||||
## Discovery
|
||||
|
||||
Auto-discovery is supported as long as the hub can be discovered using the default host and port.
|
||||
If the hub requires custom host and/or port configuration manual setup is required.
|
||||
|
||||
Background discovery is not supported.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### Hub
|
||||
|
||||
The hub offers two optional configuration parameters:
|
||||
| Parameter | Description |
|
||||
|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| host | The URL to reach the hub. The hub makes itself known through mDNS as `LCM1.local` and the host parameter defaults to this value. As long as the openHAB server and the hub are on the same broadcast domain for mDNS the host parameter doesn't need to be specified. |
|
||||
| port | The port the hub communicates on. By default the hub answers on port 2112 and the port parameter defaults to this value. As long as the hub configuration hasn't been changed the port parameter doesn't need to be specified. |
|
||||
|
||||
### Devices
|
||||
|
||||
All devices share one required paramenter:
|
||||
| Parameter | Description |
|
||||
|-----------|--------------------------------------------------------------------------------|
|
||||
| zoneId | The zone ID that is assigned by the hub to each device as a unique identifier. |
|
||||
|
||||
Legrand does not provide an easy way to look up a zone ID for a device.
|
||||
However, zone IDs are simply assigned sequentially starting with 0 in the order devices are added to the hub.
|
||||
So the first device will have zone ID 0, the next 1 and so on.
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel Type ID | Item Type | Commands | Description | Thing Types Supporting This Channel |
|
||||
|-----------------|-----------|----------|-------------------------|-------------------------------------|
|
||||
| power | Switch | ON, OFF | Turn device on and off | switch, dimmer |
|
||||
| brightness | Dimmer | 1-100 | Set device's brightness | dimmer |
|
||||
|
||||
Note that the brightness channel is limited to values from 1 to 100.
|
||||
All other commands are ignored.
|
||||
That means in particular that a dimmer can't be turned off by sending 0 to the brightness channel.
|
||||
Also, if a dimmer is turned off (via the power channel) and the brightness is updated the dimmer will remain off.
|
||||
Once the dimmer is turned on it will turn on with the updated brightness setting.
|
||||
Consequently when a dimmer is turned on it always returns to the most recent brightness setting.
|
||||
In other words power and brightness states are controlled independently.
|
||||
This matches how power and brightness are managed on the physical dimmer itself.
|
||||
|
||||
To avoid confusion for the user any UI must ensure that only values from 1 to 100 are passed to the brightness channel.
|
||||
A default slider allows a 0 value and should not be used since there will be no response when the user selects 0.
|
||||
Common UI choices are Sliders or Setpoints with a minimum value of 1 and a maximum value of 100 (min/max values in Sliders are only supported as of openHAB 2.5).
|
||||
|
||||
## Example
|
||||
|
||||
This is a simple example that uses an Adorne switch and two dimmers.
|
||||
Remember that the host and port parameter are not needed in most cases.
|
||||
As discussed above care is taken that the brightness channel only allows values from 1 to 100 by specifying a min and max value in the sitemap for the dimmers.
|
||||
For this example to run on an openHAB version older than 2.5 Bedroom 1's Slider must be removed in the sitemap since older versions don't support the min/max setting.
|
||||
|
||||
## demo.things
|
||||
|
||||
```
|
||||
Bridge adorne:hub:home "Adorne Hub" [host="192.160.1.111", port=2113] {
|
||||
switch bathroom "Bathroom" [zoneId=0]
|
||||
dimmer bedroom1 "Bedroom1" [zoneId=1]
|
||||
dimmer bedroom2 "Bedroom2" [zoneId=2]
|
||||
}
|
||||
```
|
||||
|
||||
## demo.items
|
||||
|
||||
```
|
||||
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
|
||||
Switch LightBedroomSwitch1 {channel="adorne:dimmer:home:bedroom1:power"}
|
||||
Dimmer LightBedroomDimmer1 {channel="adorne:dimmer:home:bedroom1:brightness"}
|
||||
Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
|
||||
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}
|
||||
```
|
||||
|
||||
## demo.sitemap
|
||||
|
||||
```
|
||||
sitemap demo label="Adorne Binding Demo"
|
||||
{
|
||||
Frame label="Adorne Switch" {
|
||||
Switch item=LightBathroom label="Bathroom" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
}
|
||||
Frame label="Adorne Dimmer using Slider" {
|
||||
Switch item=LightBedroomSwitch1 label="Bedroom 1" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Slider item=LightBedroomDimmer1 label="Bedroom 1" icon="light-on" minValue=1 maxValue=100 step=1
|
||||
}
|
||||
Frame label="Adorne Dimmer using Setpoint" {
|
||||
Switch item=LightBedroomSwitch2 label="Bedroom 2" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Setpoint item=LightBedroomDimmer2 label="Bedroom 2" icon="light-on" minValue=1 maxValue=100 step=5
|
||||
}
|
||||
}
|
||||
```
|
17
bundles/org.openhab.binding.adorne/pom.xml
Normal file
17
bundles/org.openhab.binding.adorne/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.adorne</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Adorne Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.adorne-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-adorne" description="Adorne Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.adorne/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AdorneBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "adorne";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_HUB = new ThingTypeUID(BINDING_ID, "hub");
|
||||
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
|
||||
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_POWER = "power";
|
||||
public static final String CHANNEL_BRIGHTNESS = "brightness";
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDeviceState} class defines a simple POJO representing the Adorne device state.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneDeviceState {
|
||||
public final int zoneId;
|
||||
public final String name;
|
||||
public final ThingTypeUID deviceType;
|
||||
public final boolean onOff;
|
||||
public final int brightness;
|
||||
|
||||
public AdorneDeviceState(int zoneId, String name, ThingTypeUID deviceType, boolean onOff, int brightness) {
|
||||
this.zoneId = zoneId;
|
||||
this.name = name;
|
||||
this.deviceType = deviceType;
|
||||
this.onOff = onOff;
|
||||
this.brightness = brightness;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubConfiguration} class represents the hub configuration options.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubConfiguration {
|
||||
public String host = "LCM1.local";
|
||||
public Integer port = 2112;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AdorneSwitchConfiguration} class represents the switch configuration options.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneSwitchConfiguration {
|
||||
public @Nullable Integer zoneId;
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
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.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.util.UIDUtils;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDiscoveryService} discovers things for the Adorne hub and Adorne devices.
|
||||
* Discovery is only supported if the hub is accessible via default host and port.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.adorne")
|
||||
public class AdorneDiscoveryService extends AbstractDiscoveryService implements AdorneHubChangeNotify {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneDiscoveryService.class);
|
||||
private static final int DISCOVERY_TIMEOUT_SECONDS = 10;
|
||||
private static final String DISCOVERY_HUB_LABEL = "Adorne Hub";
|
||||
private static final String DISCOVERY_ZONE_ID = "zoneId";
|
||||
private @Nullable AdorneHubController adorneHubController;
|
||||
|
||||
/**
|
||||
* Creates a AdorneDiscoveryService with disabled auto-discovery.
|
||||
*/
|
||||
public AdorneDiscoveryService() {
|
||||
// Passing false as last argument to super constructor turns off background discovery
|
||||
super(Collections.singleton(new ThingTypeUID(BINDING_ID, "-")), DISCOVERY_TIMEOUT_SECONDS, false);
|
||||
|
||||
// We create the hub controller with default host and port. In the future we could let users create hubs
|
||||
// manually with custom host and port settings and then perform discovery here for those hubs.
|
||||
adorneHubController = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off discovery of all devices on the hub
|
||||
*/
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Discovery scan started");
|
||||
|
||||
AdorneHubController adorneHubController = new AdorneHubController(new AdorneHubConfiguration(), scheduler,
|
||||
this);
|
||||
this.adorneHubController = adorneHubController;
|
||||
|
||||
// Hack - we wrap the ThingUID in an array to make it appear effectively final to the compiler throughout the
|
||||
// chain of futures. Passing it through the chain as context would bloat the code.
|
||||
ThingUID[] bridgeUID = new ThingUID[1];
|
||||
|
||||
// Future enhancement: Need a timeout for each future execution to recover from bugs in the hub controller, but
|
||||
// Java8 doesn't yet offer that
|
||||
adorneHubController.start().thenCompose(Void -> {
|
||||
// We use the hub's MAC address as its unique identifier
|
||||
return adorneHubController.getMACAddress();
|
||||
}).thenCompose(macAddress -> {
|
||||
String macAddressNoColon = macAddress.replace(':', '-'); // Colons are not allowed in ThingUIDs
|
||||
bridgeUID[0] = new ThingUID(THING_TYPE_HUB, macAddressNoColon);
|
||||
// We have fully discovered the hub
|
||||
thingDiscovered(DiscoveryResultBuilder.create(bridgeUID[0]).withLabel(DISCOVERY_HUB_LABEL).build());
|
||||
return adorneHubController.getZones();
|
||||
}).thenAccept(zoneIds -> {
|
||||
zoneIds.forEach(zoneId -> {
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
String id = UIDUtils.encode(state.name); // Strip zone ID's name to become a valid ThingUID
|
||||
// We have fully discovered a new zone ID
|
||||
thingDiscovered(DiscoveryResultBuilder
|
||||
.create(new ThingUID(state.deviceType, bridgeUID[0], id.toLowerCase()))
|
||||
.withLabel(state.name).withBridge(bridgeUID[0])
|
||||
.withProperty(DISCOVERY_ZONE_ID, state.zoneId).build());
|
||||
}).exceptionally(e -> {
|
||||
logger.warn("Discovery of zone ID {} failed ({})", zoneId, e.getMessage());
|
||||
return null;
|
||||
});
|
||||
});
|
||||
adorneHubController.stopWhenCommandsServed(); // Shut down hub once all discovery requests have been served
|
||||
}).exceptionally(e -> {
|
||||
logger.warn("Discovery failed ({})", e.getMessage());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification to stop scanning
|
||||
*/
|
||||
@Override
|
||||
protected void stopScan() {
|
||||
super.stopScan();
|
||||
|
||||
AdorneHubController adorneHubController = this.adorneHubController;
|
||||
if (adorneHubController != null) {
|
||||
adorneHubController.stop();
|
||||
this.adorneHubController = null;
|
||||
logger.debug("Discovery timed out. Scan stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do on change notifications
|
||||
@Override
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionChangeNotify(boolean connected) {
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_BRIGHTNESS;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
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.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDimmerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels. It supports the brightness channel in addition to the inherited switch channel.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneDimmerHandler extends AdorneSwitchHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneDimmerHandler.class);
|
||||
|
||||
public AdorneDimmerHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles refresh and percent commands for channel
|
||||
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_BRIGHTNESS}
|
||||
* It delegates all other commands to its parent class.
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
|
||||
try {
|
||||
if (channelUID.getId().equals(CHANNEL_BRIGHTNESS)) {
|
||||
if (command instanceof RefreshType) {
|
||||
refreshBrightness();
|
||||
} else if (command instanceof PercentType) {
|
||||
// Change the brightness through the hub controller
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
int level = ((PercentType) command).intValue();
|
||||
if (level >= 1 && level <= 100) { // Ignore commands outside of the supported 1-100 range
|
||||
adorneHubController.setBrightness(zoneId, level);
|
||||
} else {
|
||||
logger.debug("Ignored command to set brightness to level {}", level);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.handleCommand(channelUID, command); // Parent can handle everything else
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
|
||||
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
|
||||
// want the framework to handle it gracefully. No point to update the thing status, since the
|
||||
// AdorneHubController already does that. So we are forced to swallow the exception here.
|
||||
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
|
||||
getThing().getLabel(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the brightness of our thing to the actual state of the device.
|
||||
*
|
||||
*/
|
||||
public void refreshBrightness() {
|
||||
// Asynchronously get our brightness from the hub controller and update our state accordingly
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
updateState(CHANNEL_BRIGHTNESS, new PercentType(state.brightness));
|
||||
logger.debug("Refreshed dimmer {} with brightness {}", getThing().getLabel(), state.brightness);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all supported channels.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void refresh() {
|
||||
super.refresh();
|
||||
refreshBrightness();
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHandlerFactory} is responsible for creating thing handlers.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.adorne", service = ThingHandlerFactory.class)
|
||||
public class AdorneHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHandlerFactory.class);
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(THING_TYPE_HUB, THING_TYPE_SWITCH, THING_TYPE_DIMMER).collect(Collectors.toSet()));
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates handlers for switches, dimmers and hubs.
|
||||
*/
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
|
||||
logger.debug("Creating an AdorneSwitchHandler for thing '{}'", thing.getUID());
|
||||
|
||||
return new AdorneSwitchHandler(thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
|
||||
logger.debug("Creating an AdorneDimmerHandler for thing '{}'", thing.getUID());
|
||||
|
||||
return new AdorneDimmerHandler(thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_HUB)) {
|
||||
logger.debug("Creating an AdorneHubHandler for bridge '{}'", thing.getUID());
|
||||
|
||||
return new AdorneHubHandler((Bridge) thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
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.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} manages the state and status of the Adorne Hub's devices.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubHandler extends BaseBridgeHandler implements AdorneHubChangeNotify {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubHandler.class);
|
||||
private @Nullable AdorneHubController adorneHubController = null;
|
||||
|
||||
public AdorneHubHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} does not support any commands itself. This method is a NOOP and only provided since
|
||||
* its implementation is required.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Unfortunately BaseBridgeHandler doesn't provide a default implementation of handleCommand. However, hub
|
||||
// commands could be added as a future enhancement e.g. to support hub firmware upgrades.
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the hub controller for communication with the hub.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing hub {}", getThing().getLabel());
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
AdorneHubConfiguration config = getConfigAs(AdorneHubConfiguration.class);
|
||||
logger.debug("Configuration host:{} port:{}", config.host, config.port);
|
||||
|
||||
AdorneHubController adorneHubController = new AdorneHubController(config, scheduler, this);
|
||||
this.adorneHubController = adorneHubController;
|
||||
// Kick off the hub controller that handles all interactions with the hub for us
|
||||
adorneHubController.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes resources by stopping the hub controller.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
AdorneHubController adorneHubController = this.adorneHubController;
|
||||
if (adorneHubController != null) {
|
||||
adorneHubController.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hub controller. Returns <code>null</code> if hub controller has not been created yet.
|
||||
*
|
||||
* @return hub controller
|
||||
*/
|
||||
public @Nullable AdorneHubController getAdorneHubController() {
|
||||
return adorneHubController;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} is notified that the state of one of its physical devices has changed. The
|
||||
* {@link AdorneHubHandler} then asks the appropriate thing handler to update the thing to match the new state.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
|
||||
logger.debug("State changed (zoneId:{} onOff:{} brightness:{})", zoneId, onOff, brightness);
|
||||
getThing().getThings().forEach(thing -> {
|
||||
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
|
||||
if (thingHandler != null && thingHandler.getZoneId() == zoneId) {
|
||||
thingHandler.updateState(CHANNEL_POWER, OnOffType.from(onOff));
|
||||
if (thing.getThingTypeUID().equals(THING_TYPE_DIMMER)) {
|
||||
thingHandler.updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} is notified that its connectivity has changed.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void connectionChangeNotify(boolean connected) {
|
||||
logger.debug("Status changed (connected:{})", connected);
|
||||
|
||||
if (connected) {
|
||||
// Refresh all of our things in case thing states changed while we were disconnected
|
||||
getThing().getThings().forEach(thing -> {
|
||||
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
|
||||
if (thingHandler != null) {
|
||||
thingHandler.refresh();
|
||||
}
|
||||
});
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_POWER;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneSwitchConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
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.ThingStatusInfo;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link AdorneSwitchHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneSwitchHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneSwitchHandler.class);
|
||||
|
||||
/**
|
||||
* The zone ID that represents this {@link AdorneSwitchHandler}'s thing
|
||||
*/
|
||||
protected int zoneId;
|
||||
|
||||
public AdorneSwitchHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles refresh and on/off commands for channel
|
||||
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_POWER}
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
|
||||
try {
|
||||
if (channelUID.getId().equals(CHANNEL_POWER)) {
|
||||
if (command instanceof OnOffType) {
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.setOnOff(zoneId, command.equals(OnOffType.ON));
|
||||
} else if (command instanceof RefreshType) {
|
||||
refreshOnOff();
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
|
||||
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
|
||||
// want the framework to handle it gracefully. No point to update the thing status, since the
|
||||
// AdorneHubController already does that. So we are forced to swallow the exception here.
|
||||
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
|
||||
getThing().getLabel(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the handled thing to online.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing switch {}", getThing().getLabel());
|
||||
|
||||
AdorneSwitchConfiguration config = getConfigAs(AdorneSwitchConfiguration.class);
|
||||
Integer configZoneId = config.zoneId;
|
||||
if (configZoneId != null) {
|
||||
zoneId = configZoneId;
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates thing status in response to bridge status changes.
|
||||
*/
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
logger.trace("bridgeStatusChanged bridgeStatusInfo:{}", bridgeStatusInfo.getStatus());
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else {
|
||||
updateStatus(bridgeStatusInfo.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hub controller.
|
||||
*
|
||||
* @throws IllegalStateException if hub controller is not available yet.
|
||||
*/
|
||||
protected AdorneHubController getAdorneHubController() {
|
||||
Bridge bridge;
|
||||
AdorneHubHandler hubHandler;
|
||||
AdorneHubController adorneHubController = null;
|
||||
|
||||
bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
hubHandler = (AdorneHubHandler) bridge.getHandler();
|
||||
if (hubHandler != null) {
|
||||
adorneHubController = hubHandler.getAdorneHubController();
|
||||
}
|
||||
}
|
||||
if (adorneHubController == null) {
|
||||
throw new IllegalStateException("Hub Controller not available yet.");
|
||||
}
|
||||
return adorneHubController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zone ID that represents this {@link AdorneSwitchHandler}'s thing
|
||||
*
|
||||
* @return zone ID
|
||||
*/
|
||||
public int getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the on/off state of our thing to the actual state of the device.
|
||||
*
|
||||
*/
|
||||
public void refreshOnOff() {
|
||||
// Asynchronously get our onOff state from the hub controller and update our state accordingly
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
OnOffType onOffState = OnOffType.from(state.onOff);
|
||||
updateState(CHANNEL_POWER, onOffState);
|
||||
logger.debug("Refreshed switch {} with switch state {}", getThing().getLabel(), onOffState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all supported channels.
|
||||
*
|
||||
*/
|
||||
public void refresh() {
|
||||
refreshOnOff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a public version of updateState.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void updateState(String channelID, State state) {
|
||||
super.updateState(channelID, state);// Leverage our base class' protected method
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubChangeNotify} interface is used by the {@link AdorneHubController} to notify listeners about
|
||||
* Adorne device status and hub connection changes.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AdorneHubChangeNotify {
|
||||
/**
|
||||
* Notify listener about state change of on/off and brightness state
|
||||
*
|
||||
* @param zoneID zone ID for which change occurred
|
||||
* @param onOff new on/off state
|
||||
* @param brightness new brightness
|
||||
*/
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness);
|
||||
|
||||
/**
|
||||
* Notify listener about hub connection change
|
||||
*
|
||||
* @param connected new connection state
|
||||
*/
|
||||
public void connectionChangeNotify(boolean connected);
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonStreamParser;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubConnection} manages basic connectivity with the Adorne hub.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubConnection {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubConnection.class);
|
||||
|
||||
private final Socket hubSocket;
|
||||
private final PrintStream hubOut;
|
||||
private final InputStreamReader hubInReader;
|
||||
private final JsonStreamParser hubIn;
|
||||
|
||||
public AdorneHubConnection(String hubHost, int hubPort, int timeout) throws IOException {
|
||||
hubSocket = new Socket(hubHost, hubPort);
|
||||
hubSocket.setSoTimeout(timeout);
|
||||
hubOut = new PrintStream(hubSocket.getOutputStream());
|
||||
hubInReader = new InputStreamReader(hubSocket.getInputStream());
|
||||
hubIn = new JsonStreamParser(hubInReader);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
hubInReader.close(); // Closes underlying input stream as well
|
||||
} catch (IOException e) {
|
||||
logger.warn("Closing hub input reader failed ({})", e.getMessage());
|
||||
}
|
||||
hubOut.close(); // Closes underlying output stream as well
|
||||
try {
|
||||
hubSocket.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Closing hub controller socket failed ({})", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
hubSocket.shutdownInput();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Couldn't shutdown hub socket");
|
||||
}
|
||||
}
|
||||
|
||||
public void putMsg(String cmd) {
|
||||
hubOut.print(cmd);
|
||||
}
|
||||
|
||||
public @Nullable JsonObject getMsg() throws JsonParseException {
|
||||
JsonElement msg = null;
|
||||
JsonObject msgJsonObject = null;
|
||||
|
||||
msg = hubIn.next();
|
||||
|
||||
if (msg == null || (msg instanceof JsonPrimitive && msg.getAsCharacter() == 0)) {
|
||||
return null; // Eat empty messages
|
||||
}
|
||||
logger.debug("Received message {}", msg);
|
||||
if (msg instanceof JsonObject) {
|
||||
msgJsonObject = (JsonObject) msg;
|
||||
}
|
||||
return msgJsonObject;
|
||||
}
|
||||
}
|
@ -0,0 +1,511 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.AdorneDeviceState;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubController} manages the interaction with the Adorne hub. The controller maintains a connection
|
||||
* with the Adorne Hub and listens to device changes and issues device commands. Interaction with the hub is performed
|
||||
* asynchronously through REST messages.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubController {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubController.class);
|
||||
|
||||
private static final int HUB_CONNECT_TIMEOUT = 10000;
|
||||
private static final int HUB_RECONNECT_SLEEP_MINIMUM = 1;
|
||||
private static final int HUB_RECONNECT_SLEEP_MAXIMUM = 15 * 60;
|
||||
|
||||
// Hub rest commands
|
||||
private static final String HUB_REST_SET_ONOFF = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"Power\":%b}}\0";
|
||||
private static final String HUB_REST_SET_BRIGHTNESS = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"PowerLevel\":%d}}\0";
|
||||
private static final String HUB_REST_REQUEST_STATE = "{\"ID\":%d,\"Service\":\"ReportZoneProperties\",\"ZID\":%d}\0";
|
||||
private static final String HUB_REST_REQUEST_ZONES = "{\"ID\":%d,\"Service\":\"ListZones\"}\0";
|
||||
private static final String HUB_REST_REQUEST_MACADDRESS = "{\"ID\":%d,\"Service\":\"SystemInfo\"}\0";
|
||||
private static final String HUB_TOKEN_SERVICE = "Service";
|
||||
private static final String HUB_TOKEN_ZID = "ZID";
|
||||
private static final String HUB_TOKEN_PROPERTY_LIST = "PropertyList";
|
||||
private static final String HUB_TOKEN_DEVICE_TYPE = "DeviceType";
|
||||
private static final String HUB_TOKEN_SWITCH = "Switch";
|
||||
private static final String HUB_TOKEN_DIMMER = "Dimmer";
|
||||
private static final String HUB_TOKEN_NAME = "Name";
|
||||
private static final String HUB_TOKEN_POWER = "Power";
|
||||
private static final String HUB_TOKEN_POWER_LEVEL = "PowerLevel";
|
||||
private static final String HUB_TOKEN_MAC_ADDRESS = "MACAddress";
|
||||
private static final String HUB_TOKEN_ZONE_LIST = "ZoneList";
|
||||
private static final String HUB_SERVICE_REPORT_ZONE_PROPERTIES = "ReportZoneProperties";
|
||||
private static final String HUB_SERVICE_ZONE_PROPERTIES_CHANGED = "ZonePropertiesChanged";
|
||||
private static final String HUB_SERVICE_LIST_ZONE = "ListZones";
|
||||
private static final String HUB_SERVICE_SYSTEM_INFO = "SystemInfo";
|
||||
|
||||
private @Nullable Future<?> hubController;
|
||||
private final String hubHost;
|
||||
private int hubPort;
|
||||
private @Nullable AdorneHubConnection hubConnection;
|
||||
private final CompletableFuture<@Nullable Void> hubControllerConnected;
|
||||
private int hubReconnectSleep; // Sleep time before we attempt re-connect
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private volatile boolean stopWhenCommandsServed; // Stop the controller once all pending commands have been served
|
||||
|
||||
// When we submit commmands to the hub we don't correlate commands and responses. We simply use the first available
|
||||
// response that answers our question. For that we store all pending commands.
|
||||
// Note that for optimal resiliency we send a new request for each command even if a request is already pending
|
||||
private final Map<Integer, CompletableFuture<AdorneDeviceState>> stateCommands;
|
||||
private @Nullable CompletableFuture<List<Integer>> zoneCommand;
|
||||
private @Nullable CompletableFuture<String> macAddressCommand;
|
||||
private final AtomicInteger commandId; // We assign increasing command ids to all REST commands to the hub for
|
||||
// easier troubleshooting
|
||||
|
||||
private final AdorneHubChangeNotify changeListener;
|
||||
|
||||
private final Object stopLock;
|
||||
private final Object hubConnectionLock;
|
||||
private final Object macAddressCommandLock;
|
||||
private final Object zoneCommandLock;
|
||||
|
||||
public AdorneHubController(AdorneHubConfiguration config, ScheduledExecutorService scheduler,
|
||||
AdorneHubChangeNotify changeListener) {
|
||||
hubHost = config.host;
|
||||
hubPort = config.port;
|
||||
this.scheduler = scheduler;
|
||||
this.changeListener = changeListener;
|
||||
hubController = null;
|
||||
hubConnection = null;
|
||||
hubControllerConnected = new CompletableFuture<>();
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM;
|
||||
|
||||
stopWhenCommandsServed = false;
|
||||
|
||||
stopLock = new Object();
|
||||
hubConnectionLock = new Object();
|
||||
macAddressCommandLock = new Object();
|
||||
zoneCommandLock = new Object();
|
||||
|
||||
stateCommands = new HashMap<>();
|
||||
zoneCommand = null;
|
||||
macAddressCommand = null;
|
||||
commandId = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the hub controller. Call only once.
|
||||
*
|
||||
* @return Future to inform the caller that the hub controller is ready for receiving commands
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> start() {
|
||||
logger.info("Starting hub controller");
|
||||
hubController = scheduler.submit(this::msgLoop);
|
||||
return hubControllerConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the hub controller. Can't restart afterwards. If called before start nothing happens.
|
||||
*/
|
||||
public void stop() {
|
||||
logger.info("Stopping hub controller");
|
||||
synchronized (stopLock) {
|
||||
// Canceling the controller tells the message loop to stop and also cancels recreation of the message loop
|
||||
// if that is pending after a disconnect.
|
||||
Future<?> hubController = this.hubController;
|
||||
if (hubController != null) {
|
||||
hubController.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the input stream in case controller is waiting on input
|
||||
// Note this is best effort. If we are unlucky the hub can still enter waiting on input just after our stop
|
||||
// here. Because waiting on input is long-running we can't just synchronize it with the stop check as case 2
|
||||
// above. But that is ok as waiting on input has a timeout and will honor stop after that.
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
cancelCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the hub controller once all in-flight commands have been executed.
|
||||
*/
|
||||
public void stopWhenCommandsServed() {
|
||||
stopWhenCommandsServed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns device on or off.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @param on true to turn on the device
|
||||
*/
|
||||
public void setOnOff(int zoneId, boolean on) {
|
||||
sendRestCmd(String.format(HUB_REST_SET_ONOFF, getNextCommandId(), zoneId, on));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the brightness for a device. Applies only to dimmer devices.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @param level A value from 1-100. Note that in particular value 0 is not supported, which means this method can't
|
||||
* be used to turn off a dimmer.
|
||||
*/
|
||||
public void setBrightness(int zoneId, int level) {
|
||||
if (level < 1 || level > 100) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
sendRestCmd(String.format(HUB_REST_SET_BRIGHTNESS, getNextCommandId(), zoneId, level));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously the state for a device.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @return a future for the {@link AdorneDeviceState}
|
||||
*/
|
||||
public CompletableFuture<AdorneDeviceState> getState(int zoneId) {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_STATE, getNextCommandId(), zoneId));
|
||||
|
||||
CompletableFuture<AdorneDeviceState> stateCommand;
|
||||
synchronized (stateCommands) {
|
||||
stateCommand = stateCommands.get(zoneId);
|
||||
if (stateCommand == null) {
|
||||
stateCommand = new CompletableFuture<>();
|
||||
stateCommands.put(zoneId, stateCommand);
|
||||
}
|
||||
}
|
||||
return stateCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously all zone IDs that are in use on the hub.
|
||||
*
|
||||
* @return a future for the list of zone IDs
|
||||
*/
|
||||
public CompletableFuture<List<Integer>> getZones() {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_ZONES, getNextCommandId()));
|
||||
|
||||
CompletableFuture<List<Integer>> zoneCommand;
|
||||
synchronized (zoneCommandLock) {
|
||||
zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand == null) {
|
||||
this.zoneCommand = zoneCommand = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
return zoneCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously the MAC address of the hub.
|
||||
*
|
||||
* @return a future for the MAC address
|
||||
*/
|
||||
public CompletableFuture<String> getMACAddress() {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_MACADDRESS, getNextCommandId()));
|
||||
|
||||
CompletableFuture<String> macAddressCommand;
|
||||
synchronized (macAddressCommandLock) {
|
||||
macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand == null) {
|
||||
this.macAddressCommand = macAddressCommand = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
return macAddressCommand;
|
||||
}
|
||||
|
||||
private void sendRestCmd(String cmd) {
|
||||
logger.debug("Sending command {}", cmd);
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.putMsg(cmd);
|
||||
} else {
|
||||
throw new IllegalStateException("Can't send command. Adorne Hub connection is not available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the controller message loop that is interacting with the Adorne Hub by sending commands and listening for
|
||||
* updates
|
||||
*/
|
||||
private void msgLoop() {
|
||||
try {
|
||||
JsonObject hubMsg;
|
||||
JsonPrimitive jsonService;
|
||||
String service;
|
||||
|
||||
// Main message loop listening for updates from the hub
|
||||
logger.debug("Starting message loop");
|
||||
while (!shouldStop()) {
|
||||
if (!connect()) {
|
||||
int sleep = hubReconnectSleep;
|
||||
logger.debug("Waiting {} seconds before re-attempting to connect.", sleep);
|
||||
if (hubReconnectSleep < HUB_RECONNECT_SLEEP_MAXIMUM) {
|
||||
hubReconnectSleep = hubReconnectSleep * 2; // Increase sleep time exponentially
|
||||
}
|
||||
restartMsgLoop(sleep);
|
||||
return;
|
||||
} else {
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset
|
||||
}
|
||||
|
||||
hubMsg = null;
|
||||
try {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubMsg = hubConnection.getMsg();
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Failed to read valid message {}", e.getMessage());
|
||||
disconnect(); // Disconnect so we can recover
|
||||
}
|
||||
if (hubMsg == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process message based on service type
|
||||
if ((jsonService = hubMsg.getAsJsonPrimitive(HUB_TOKEN_SERVICE)) != null) {
|
||||
service = jsonService.getAsString();
|
||||
} else {
|
||||
continue; // Ignore messages that don't have a service specified
|
||||
}
|
||||
|
||||
if (service.equals(HUB_SERVICE_REPORT_ZONE_PROPERTIES)) {
|
||||
processMsgReportZoneProperties(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_ZONE_PROPERTIES_CHANGED)) {
|
||||
processMsgZonePropertiesChanged(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_LIST_ZONE)) {
|
||||
processMsgListZone(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_SYSTEM_INFO)) {
|
||||
processMsgSystemInfo(hubMsg);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Hub controller failed", e);
|
||||
}
|
||||
|
||||
// Shut down
|
||||
disconnect();
|
||||
|
||||
cancelCommands();
|
||||
hubControllerConnected.cancel(false);
|
||||
logger.info("Exiting hub controller");
|
||||
}
|
||||
|
||||
private boolean shouldStop() {
|
||||
boolean stateCommandsIsEmpty;
|
||||
synchronized (stateCommands) {
|
||||
stateCommandsIsEmpty = stateCommands.isEmpty();
|
||||
}
|
||||
boolean commandsServed = stopWhenCommandsServed && stateCommandsIsEmpty && (zoneCommand == null)
|
||||
&& (macAddressCommand == null);
|
||||
|
||||
return isCancelled() || commandsServed;
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
Future<?> hubController = this.hubController;
|
||||
return hubController == null || hubController.isCancelled();
|
||||
}
|
||||
|
||||
private boolean connect() {
|
||||
try {
|
||||
if (hubConnection == null) {
|
||||
hubConnection = new AdorneHubConnection(hubHost, hubPort, HUB_CONNECT_TIMEOUT);
|
||||
logger.debug("Hub connection established");
|
||||
|
||||
// Working around an Adorne Hub bug: the first command sent from a new connection intermittently
|
||||
// gets lost in the hub. We are requesting the MAC address here simply to get this fragile first
|
||||
// command out of the way. Requesting the MAC address and ignoring the result doesn't do any harm.
|
||||
getMACAddress();
|
||||
|
||||
hubControllerConnected.complete(null);
|
||||
|
||||
changeListener.connectionChangeNotify(true);
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Couldn't establish hub connection ({}).", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset our reconnect sleep time
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.close();
|
||||
this.hubConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
changeListener.connectionChangeNotify(false);
|
||||
}
|
||||
|
||||
private void cancelCommands() {
|
||||
// If there are still pending commands we need to cancel them
|
||||
synchronized (stateCommands) {
|
||||
stateCommands.forEach((zoneId, stateCommand) -> stateCommand.cancel(false));
|
||||
stateCommands.clear();
|
||||
}
|
||||
synchronized (zoneCommandLock) {
|
||||
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand != null) {
|
||||
zoneCommand.cancel(false);
|
||||
this.zoneCommand = null;
|
||||
}
|
||||
}
|
||||
synchronized (macAddressCommandLock) {
|
||||
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand != null) {
|
||||
macAddressCommand.cancel(false);
|
||||
this.macAddressCommand = null;
|
||||
}
|
||||
}
|
||||
logger.debug("Cancelled commands");
|
||||
}
|
||||
|
||||
private void restartMsgLoop(int sleep) {
|
||||
synchronized (stopLock) {
|
||||
if (!isCancelled()) {
|
||||
this.hubController = scheduler.schedule(this::msgLoop, sleep, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent zone properties in response to a command.
|
||||
*/
|
||||
private void processMsgReportZoneProperties(JsonObject hubMsg) {
|
||||
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
|
||||
logger.debug("Reporting zone properties for zone ID {} ", zoneId);
|
||||
|
||||
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
|
||||
String deviceTypeStr = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_DEVICE_TYPE).getAsString();
|
||||
ThingTypeUID deviceType;
|
||||
if (deviceTypeStr.equals(HUB_TOKEN_SWITCH)) {
|
||||
deviceType = THING_TYPE_SWITCH;
|
||||
} else if (deviceTypeStr.equals(HUB_TOKEN_DIMMER)) {
|
||||
deviceType = THING_TYPE_DIMMER;
|
||||
} else {
|
||||
logger.debug("Unsupported device type {}", deviceTypeStr);
|
||||
return;
|
||||
}
|
||||
AdorneDeviceState state = new AdorneDeviceState(zoneId,
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_NAME).getAsString(), deviceType,
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean(),
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt());
|
||||
|
||||
synchronized (stateCommands) {
|
||||
CompletableFuture<AdorneDeviceState> stateCommand = stateCommands.get(zoneId);
|
||||
if (stateCommand != null) {
|
||||
stateCommand.complete(state);
|
||||
stateCommands.remove(zoneId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub informs us about a zone's change in properties.
|
||||
*/
|
||||
private void processMsgZonePropertiesChanged(JsonObject hubMsg) {
|
||||
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
|
||||
logger.debug("Zone properties changed for zone ID {} ", zoneId);
|
||||
|
||||
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
|
||||
boolean onOff = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean();
|
||||
int brightness = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt();
|
||||
changeListener.stateChangeNotify(zoneId, onOff, brightness);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent a list of zones in response to a command.
|
||||
*/
|
||||
private void processMsgListZone(JsonObject hubMsg) {
|
||||
List<Integer> zones = new ArrayList<>();
|
||||
JsonArray jsonZoneList;
|
||||
|
||||
jsonZoneList = hubMsg.getAsJsonArray(HUB_TOKEN_ZONE_LIST);
|
||||
jsonZoneList.forEach(jsonZoneId -> {
|
||||
JsonPrimitive jsonZoneIdValue = ((JsonObject) jsonZoneId).getAsJsonPrimitive(HUB_TOKEN_ZID);
|
||||
zones.add(jsonZoneIdValue.getAsInt());
|
||||
});
|
||||
|
||||
synchronized (zoneCommandLock) {
|
||||
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand != null) {
|
||||
zoneCommand.complete(zones);
|
||||
this.zoneCommand = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent system info in response to a command.
|
||||
*/
|
||||
private void processMsgSystemInfo(JsonObject hubMsg) {
|
||||
synchronized (macAddressCommandLock) {
|
||||
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand != null) {
|
||||
macAddressCommand.complete(hubMsg.getAsJsonPrimitive(HUB_TOKEN_MAC_ADDRESS).getAsString());
|
||||
this.macAddressCommand = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getNextCommandId() {
|
||||
IntUnaryOperator op = commandId -> {
|
||||
int newCommandId = commandId;
|
||||
if (commandId == Integer.MAX_VALUE) {
|
||||
newCommandId = 0;
|
||||
}
|
||||
return ++newCommandId;
|
||||
};
|
||||
|
||||
return commandId.updateAndGet(op);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="adorne" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Adorne Binding</name>
|
||||
<description>The Adorne Binding controls Legrand's Adorne Wi-Fi ready switches and outlets.</description>
|
||||
<author>Mark Theiding</author>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="adorne"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="http://openhab.org/schemas/thing-description/v1.0.0 http://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="hub">
|
||||
<label>Adorne Hub</label>
|
||||
<description>The Adorne Hub serves as the bridge to control Adorne switches, dimmer switches and outlets.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text">
|
||||
<default>LCM1.local</default>
|
||||
<label>Host</label>
|
||||
<description>
|
||||
Host name or IP address.
|
||||
</description>
|
||||
<context>network_address</context>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer">
|
||||
<default>2112</default>
|
||||
<label>Port</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="adorne"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="switch">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Adorne Switch</label>
|
||||
<description>Controls an Adorne switch or outlet.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zoneId" type="integer" required="true">
|
||||
<label>Zone ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="dimmer">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Adorne Dimmer Switch</label>
|
||||
<description>Controls an Adorne dimmer switch.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="brightness" typeId="system.brightness"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zoneId" type="integer" required="true">
|
||||
<label>Zone ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,5 @@
|
||||
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
|
||||
Switch LightBedroomSwitch1 {channel="adorne:dimmer:home:bedroom1:power"}
|
||||
Dimmer LightBedroomDimmer1 {channel="adorne:dimmer:home:bedroom1:brightness"}
|
||||
Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
|
||||
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}
|
@ -0,0 +1,14 @@
|
||||
sitemap demo label="Adorne Binding Demo"
|
||||
{
|
||||
Frame label="Adorne Switch" {
|
||||
Switch item=LightBathroom label="Bathroom" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
}
|
||||
Frame label="Adorne Dimmer using Slider" {
|
||||
Switch item=LightBedroomSwitch1 label="Bedroom 1" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Slider item=LightBedroomDimmer1 label="Bedroom 1" icon="light-on" minValue=1 maxValue=100 step=1 // Requires OpenHAB 2.5
|
||||
}
|
||||
Frame label="Adorne Dimmer using Setpoint" {
|
||||
Switch item=LightBedroomSwitch2 label="Bedroom 2" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Setpoint item=LightBedroomDimmer2 label="Bedroom 2" icon="light-on" minValue=1 maxValue=100 step=5
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
Bridge adorne:hub:home "Adorne Hub" {
|
||||
switch bathroom "Bathroom" [zoneId=0]
|
||||
dimmer bedroom1 "Bedroom1" [zoneId=1]
|
||||
dimmer bedroom2 "Bedroom2" [zoneId=2]
|
||||
}
|
32
bundles/org.openhab.binding.airquality/.classpath
Normal file
32
bundles/org.openhab.binding.airquality/.classpath
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
23
bundles/org.openhab.binding.airquality/.project
Normal file
23
bundles/org.openhab.binding.airquality/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.airquality</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
13
bundles/org.openhab.binding.airquality/NOTICE
Normal file
13
bundles/org.openhab.binding.airquality/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
202
bundles/org.openhab.binding.airquality/README.md
Normal file
202
bundles/org.openhab.binding.airquality/README.md
Normal file
@ -0,0 +1,202 @@
|
||||
# Air Quality Binding
|
||||
|
||||
This binding uses the [AQIcn.org service](https://aqicn.org) for providing air quality information for any location worldwide.
|
||||
|
||||
The World Air Quality Index project is a social enterprise project started in 2007.
|
||||
Its mission is to promote Air Pollution awareness and provide a unified Air Quality information for the whole world.
|
||||
|
||||
The project is proving a transparent Air Quality information for more than 70 countries, covering more than 9000 stations in 600 major cities, via those two websites: [aqicn.org](https://aqicn.org) and [waqi.info](https://waqi.info).
|
||||
|
||||
To use this binding, you first need to [register and get your API token](https://aqicn.org/data-platform/token/).
|
||||
|
||||
## Supported Things
|
||||
|
||||
There is exactly one supported thing type, which represents the air quality information for an observation location.
|
||||
It has the `aqi` id.
|
||||
Of course, you can add multiple Things, e.g. for measuring AQI for different locations.
|
||||
|
||||
## Discovery
|
||||
|
||||
Local Air Quality can be autodiscovered based on system location.
|
||||
You will have complete default configuration with your apiKey.
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
The binding has no configuration options, all configuration is done at Thing level.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The thing has a few configuration parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------------------------------------------------------------------|
|
||||
| apikey | Data-platform token to access the AQIcn.org service. Mandatory. |
|
||||
| location | Geo coordinates to be considered by the service. |
|
||||
| stationId | Unique ID of the measuring station. |
|
||||
| refresh | Refresh interval in minutes. Optional, the default value is 60 minutes. |
|
||||
|
||||
For the location parameter, the following syntax is allowed (comma separated latitude and longitude):
|
||||
|
||||
```java
|
||||
37.8,-122.4
|
||||
37.8255,-122.456
|
||||
```
|
||||
|
||||
If you always want to receive data from specific station and you know its unique ID, you can enter it instead of the coordinates.
|
||||
|
||||
This `stationId` can be found by using the following link:
|
||||
https://api.waqi.info/search/?token=TOKEN&keyword=NAME, replacing TOKEN by your apikey and NAME by the station you are looking for.
|
||||
|
||||
## Channels
|
||||
|
||||
The AirQuality information that is retrieved is available as these channels:
|
||||
|
||||
| Channel ID | Item Type | Description |
|
||||
|-----------------|----------------------|----------------------------------------------|
|
||||
| aqiLevel | Number | Air Quality Index |
|
||||
| aqiColor | Color | Color associated to given AQI Index. |
|
||||
| aqiDescription | String | AQI Description |
|
||||
| locationName | String | Nearest measuring station location |
|
||||
| stationId | Number | Measuring station ID |
|
||||
| stationLocation | Location | Latitude/longitude of measuring station |
|
||||
| pm25 | Number | Fine particles pollution level (PM2.5) |
|
||||
| pm10 | Number | Coarse dust particles pollution level (PM10) |
|
||||
| o3 | Number | Ozone level (O3) |
|
||||
| no2 | Number | Nitrogen Dioxide level (NO2) |
|
||||
| co | Number | Carbon monoxide level (CO) |
|
||||
| so2 | Number | Sulfur dioxide level (SO2) |
|
||||
| observationTime | DateTime | Observation date and time |
|
||||
| temperature | Number:Temperature | Temperature in Celsius degrees |
|
||||
| pressure | Number:Pressure | Pressure level |
|
||||
| humidity | Number:Dimensionless | Humidity level |
|
||||
| dominentpol | String | Dominent Polutor |
|
||||
|
||||
`AQI Description` item provides a human-readable output that can be interpreted e.g. by MAP transformation.
|
||||
|
||||
*Note that channels like* `pm25`, `pm10`, `o3`, `no2`, `co`, `so2` *can sometimes return* `UNDEF` *value due to the fact that some stations don't provide measurements for them.*
|
||||
|
||||
## Full Example
|
||||
|
||||
airquality.map:
|
||||
|
||||
```text
|
||||
-=-
|
||||
UNDEF=No data
|
||||
NULL=No data
|
||||
NO_DATA=No data
|
||||
GOOD=Good
|
||||
MODERATE=Moderate
|
||||
UNHEALTHY_FOR_SENSITIVE=Unhealthy for sensitive groups
|
||||
UNHEALTHY=Unhealthy
|
||||
VERY_UNHEALTHY=Very unhealthy
|
||||
HAZARDOUS=Hazardous
|
||||
```
|
||||
|
||||
airquality.things:
|
||||
|
||||
```java
|
||||
airquality:aqi:home "AirQuality" @ "Krakow" [ apikey="XXXXXXXXXXXX", location="50.06465,19.94498", refresh=60 ]
|
||||
airquality:aqi:warsaw "AirQuality in Warsaw" [ apikey="XXXXXXXXXXXX", location="52.22,21.01", refresh=60 ]
|
||||
airquality:aqi:brisbane "AirQuality in Brisbane" [ apikey="XXXXXXXXXXXX", stationId=5115 ]
|
||||
```
|
||||
|
||||
airquality.items:
|
||||
|
||||
```java
|
||||
Group AirQuality <flow>
|
||||
|
||||
Number Aqi_Level "Air Quality Index" <flow> (AirQuality) { channel="airquality:aqi:home:aqiLevel" }
|
||||
String Aqi_Description "AQI Level [MAP(airquality.map):%s]" <flow> (AirQuality) { channel="airquality:aqi:home:aqiDescription" }
|
||||
|
||||
Number Aqi_Pm25 "PM\u2082\u2085 Level" <line> (AirQuality) { channel="airquality:aqi:home:pm25" }
|
||||
Number Aqi_Pm10 "PM\u2081\u2080 Level" <line> (AirQuality) { channel="airquality:aqi:home:pm10" }
|
||||
Number Aqi_O3 "O\u2083 Level" <line> (AirQuality) { channel="airquality:aqi:home:o3" }
|
||||
Number Aqi_No2 "NO\u2082 Level" <line> (AirQuality) { channel="airquality:aqi:home:no2" }
|
||||
Number Aqi_Co "CO Level" <line> (AirQuality) { channel="airquality:aqi:home:co" }
|
||||
Number Aqi_So2 "SO\u2082 Level" <line> (AirQuality) { channel="airquality:aqi:home:so2" }
|
||||
|
||||
String Aqi_LocationName "Measuring Location" <settings> (AirQuality) { channel="airquality:aqi:home:locationName" }
|
||||
Location Aqi_StationGeo "Station Location" <office> (AirQuality) { channel="airquality:aqi:home:stationLocation" }
|
||||
Number Aqi_StationId "Station ID" <pie> (AirQuality) { channel="airquality:aqi:home:stationId" }
|
||||
DateTime Aqi_ObservationTime "Time of observation [%1$tH:%1$tM]" <clock> (AirQuality) { channel="airquality:aqi:home:observationTime" }
|
||||
|
||||
Number:Temperature Aqi_Temperature "Temperature" <temperature> (AirQuality) { channel="airquality:aqi:home:temperature" }
|
||||
Number:Pressure Aqi_Pressure "Pressure" <pressure> (AirQuality) { channel="airquality:aqi:home:pressure" }
|
||||
Number:DimensionLess Aqi_Humidity "Humidity" <humidity> (AirQuality) { channel="airquality:aqi:home:humidity" }
|
||||
```
|
||||
|
||||
airquality.sitemap:
|
||||
|
||||
```perl
|
||||
sitemap airquality label="Air Quality" {
|
||||
Frame {
|
||||
Text item=Aqi_Level valuecolor=[
|
||||
Aqi_Level=="-"="lightgray",
|
||||
Aqi_Level>=300="#7e0023",
|
||||
>=201="#660099",
|
||||
>=151="#cc0033",
|
||||
>=101="#ff9933",
|
||||
>=51="#ffde33",
|
||||
>=0="#009966"
|
||||
]
|
||||
Text item=Aqi_Description valuecolor=[
|
||||
Aqi_Description=="HAZARDOUS"="#7e0023",
|
||||
=="VERY_UNHEALTHY"="#660099",
|
||||
=="UNHEALTHY"="#cc0033",
|
||||
=="UNHEALTHY_FOR_SENSITIVE"="#ff9933",
|
||||
=="MODERATE"="#ffde33",
|
||||
=="GOOD"="#009966"
|
||||
]
|
||||
}
|
||||
|
||||
Frame {
|
||||
Text item=Aqi_Pm25
|
||||
Text item=Aqi_Pm10
|
||||
Text item=Aqi_O3
|
||||
Text item=Aqi_No2
|
||||
Text item=Aqi_Co
|
||||
Text item=Aqi_So2
|
||||
}
|
||||
|
||||
Frame {
|
||||
Text item=Aqi_LocationName
|
||||
Text item=Aqi_ObservationTime
|
||||
Text item=Aqi_Temperature
|
||||
Text item=Aqi_Pressure
|
||||
Text item=Aqi_Humidity
|
||||
}
|
||||
|
||||
Frame label="Station Location" {
|
||||
Mapview item=Aqi_StationGeo height=10
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
airquality.rules:
|
||||
|
||||
```java
|
||||
rule "Change lamp color to reflect Air Quality"
|
||||
when
|
||||
Item Aqi_Description changed
|
||||
then
|
||||
var String hsb
|
||||
|
||||
switch Aqi_Description.state {
|
||||
case "HAZARDOUS":
|
||||
hsb = "343,100,49"
|
||||
case "VERY_UNHEALTHY":
|
||||
hsb = "280,100,60"
|
||||
case "UNHEALTHY":
|
||||
hsb = "345,100,80"
|
||||
case "UNHEALTHY_FOR_SENSITIVE":
|
||||
hsb = "30,80,100"
|
||||
case "MODERATE":
|
||||
hsb = "50,80,100"
|
||||
case "GOOD":
|
||||
hsb = "160,100,60"
|
||||
}
|
||||
|
||||
Lamp_Color.sendCommand(hsb)
|
||||
end
|
||||
```
|
17
bundles/org.openhab.binding.airquality/pom.xml
Normal file
17
bundles/org.openhab.binding.airquality/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.airquality</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Airquality Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.airquality-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-airquality" description="Air Quality Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airquality/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal;
|
||||
|
||||
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Pressure;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "airquality";
|
||||
public static final String LOCAL = "local";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_AQI = new ThingTypeUID(BINDING_ID, "aqi");
|
||||
|
||||
// List of all Channel id's
|
||||
public static final String AQI = "aqiLevel";
|
||||
public static final String AQI_COLOR = "aqiColor";
|
||||
public static final String AQIDESCRIPTION = "aqiDescription";
|
||||
public static final String PM25 = "pm25";
|
||||
public static final String PM10 = "pm10";
|
||||
public static final String O3 = "o3";
|
||||
public static final String NO2 = "no2";
|
||||
public static final String CO = "co";
|
||||
public static final String SO2 = "so2";
|
||||
public static final String LOCATIONNAME = "locationName";
|
||||
public static final String STATIONLOCATION = "stationLocation";
|
||||
public static final String STATIONID = "stationId";
|
||||
public static final String OBSERVATIONTIME = "observationTime";
|
||||
public static final String TEMPERATURE = "temperature";
|
||||
public static final String PRESSURE = "pressure";
|
||||
public static final String HUMIDITY = "humidity";
|
||||
public static final String DOMINENTPOL = "dominentpol";
|
||||
|
||||
public static final State GOOD = new StringType("GOOD");
|
||||
public static final State MODERATE = new StringType("MODERATE");
|
||||
public static final State UNHEALTHY_FOR_SENSITIVE = new StringType("UNHEALTHY_FOR_SENSITIVE");
|
||||
public static final State UNHEALTHY = new StringType("UNHEALTHY");
|
||||
public static final State VERY_UNHEALTHY = new StringType("VERY_UNHEALTHY");
|
||||
public static final State HAZARDOUS = new StringType("HAZARDOUS");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_AQI);
|
||||
public static final Set<String> SUPPORTED_CHANNEL_IDS = Stream.of(AQI, AQIDESCRIPTION, PM25, PM10, O3, NO2, CO, SO2,
|
||||
LOCATIONNAME, STATIONLOCATION, STATIONID, OBSERVATIONTIME, TEMPERATURE, PRESSURE, HUMIDITY)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Units of measurement of the data delivered by the API
|
||||
public static final Unit<Temperature> API_TEMPERATURE_UNIT = SIUnits.CELSIUS;
|
||||
public static final Unit<Dimensionless> API_HUMIDITY_UNIT = SmartHomeUnits.PERCENT;
|
||||
public static final Unit<Pressure> API_PRESSURE_UNIT = HECTO(SIUnits.PASCAL);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityConfiguration {
|
||||
|
||||
public static final String LOCATION = "location";
|
||||
|
||||
public String apikey = "";
|
||||
public String location = "";
|
||||
public @Nullable Integer stationId;
|
||||
public int refresh = 60;
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal;
|
||||
|
||||
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airquality.internal.handler.AirQualityHandler;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
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;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.airquality")
|
||||
@NonNullByDefault
|
||||
public class AirQualityHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Gson gson = new Gson();
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
@Activate
|
||||
public AirQualityHandlerFactory(final @Reference TimeZoneProvider timeZoneProvider) {
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_AQI.equals(thingTypeUID)) {
|
||||
return new AirQualityHandler(thing, gson, timeZoneProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
|
||||
import static org.openhab.binding.airquality.internal.AirQualityConfiguration.LOCATION;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.i18n.LocationProvider;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityDiscoveryService} creates things based on the configured location.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial Contribution
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, configurationPid = "discovery.airquality")
|
||||
@NonNullByDefault
|
||||
public class AirQualityDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(AirQualityDiscoveryService.class);
|
||||
|
||||
private static final int DISCOVER_TIMEOUT_SECONDS = 10;
|
||||
private static final int LOCATION_CHANGED_CHECK_INTERVAL = 60;
|
||||
|
||||
private final LocationProvider locationProvider;
|
||||
private @Nullable ScheduledFuture<?> discoveryJob;
|
||||
private @Nullable PointType previousLocation;
|
||||
|
||||
/**
|
||||
* Creates a AirQualityDiscoveryService with enabled autostart.
|
||||
*/
|
||||
@Activate
|
||||
public AirQualityDiscoveryService(@Reference LocationProvider locationProvider) {
|
||||
super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS, true);
|
||||
this.locationProvider = locationProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
|
||||
super.activate(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Modified
|
||||
protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
|
||||
super.modified(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting Air Quality discovery scan");
|
||||
PointType location = locationProvider.getLocation();
|
||||
if (location == null) {
|
||||
logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results");
|
||||
return;
|
||||
}
|
||||
createResults(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (discoveryJob == null) {
|
||||
discoveryJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
PointType currentLocation = locationProvider.getLocation();
|
||||
if (currentLocation != null && !Objects.equals(currentLocation, previousLocation)) {
|
||||
logger.debug("Location has been changed from {} to {}: Creating new discovery results",
|
||||
previousLocation, currentLocation);
|
||||
createResults(currentLocation);
|
||||
previousLocation = currentLocation;
|
||||
}
|
||||
}, 0, LOCATION_CHANGED_CHECK_INTERVAL, TimeUnit.SECONDS);
|
||||
logger.debug("Scheduled Air Qualitylocation-changed job every {} seconds", LOCATION_CHANGED_CHECK_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
public void createResults(PointType location) {
|
||||
ThingUID localAirQualityThing = new ThingUID(THING_TYPE_AQI, LOCAL);
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(LOCATION, String.format("%s,%s", location.getLatitude(), location.getLongitude()));
|
||||
thingDiscovered(DiscoveryResultBuilder.create(localAirQualityThing).withLabel("Local Air Quality")
|
||||
.withProperties(properties).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping Air Quality background discovery");
|
||||
ScheduledFuture<?> job = this.discoveryJob;
|
||||
if (job != null && !job.isCancelled()) {
|
||||
if (job.cancel(true)) {
|
||||
discoveryJob = null;
|
||||
logger.debug("Stopped Air Quality background discovery");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
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.airquality.internal.AirQualityConfiguration;
|
||||
import org.openhab.binding.airquality.internal.json.AirQualityJsonData;
|
||||
import org.openhab.binding.airquality.internal.json.AirQualityJsonResponse;
|
||||
import org.openhab.binding.airquality.internal.json.AirQualityJsonResponse.ResponseStatus;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
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.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.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityHandler extends BaseThingHandler {
|
||||
private static final String URL = "http://api.waqi.info/feed/%QUERY%/?token=%apikey%";
|
||||
private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
|
||||
private final Logger logger = LoggerFactory.getLogger(AirQualityHandler.class);
|
||||
private @Nullable ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Gson gson;
|
||||
|
||||
private int retryCounter = 0;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
public AirQualityHandler(Thing thing, Gson gson, TimeZoneProvider timeZoneProvider) {
|
||||
super(thing);
|
||||
this.gson = gson;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Air Quality handler.");
|
||||
|
||||
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
|
||||
logger.debug("config apikey = (omitted from logging)");
|
||||
logger.debug("config location = {}", config.location);
|
||||
logger.debug("config stationId = {}", config.stationId);
|
||||
logger.debug("config refresh = {}", config.refresh);
|
||||
|
||||
List<String> errorMsg = new ArrayList<>();
|
||||
|
||||
if (config.apikey.trim().isEmpty()) {
|
||||
errorMsg.add("Parameter 'apikey' is mandatory and must be configured");
|
||||
}
|
||||
if (config.location.trim().isEmpty() && config.stationId == null) {
|
||||
errorMsg.add("Parameter 'location' or 'stationId' is mandatory and must be configured");
|
||||
}
|
||||
if (config.refresh < 30) {
|
||||
errorMsg.add("Parameter 'refresh' must be at least 30 minutes");
|
||||
}
|
||||
|
||||
if (errorMsg.isEmpty()) {
|
||||
ScheduledFuture<?> job = this.refreshJob;
|
||||
if (job == null || job.isCancelled()) {
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublishData, 0, config.refresh,
|
||||
TimeUnit.MINUTES);
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.join(", ", errorMsg));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAndPublishData() {
|
||||
retryCounter = 0;
|
||||
AirQualityJsonData aqiResponse = getAirQualityData();
|
||||
if (aqiResponse != null) {
|
||||
// Update all channels from the updated AQI data
|
||||
getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID().getId())).forEach(channel -> {
|
||||
String channelId = channel.getUID().getId();
|
||||
State state = getValue(channelId, aqiResponse);
|
||||
updateState(channelId, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Disposing the Air Quality handler.");
|
||||
ScheduledFuture<?> job = this.refreshJob;
|
||||
if (job != null && !job.isCancelled()) {
|
||||
job.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateAndPublishData();
|
||||
} else {
|
||||
logger.debug("The Air Quality binding is read-only and can not handle command {}", command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request URL from configuration data
|
||||
*
|
||||
* @return a valid URL for the aqicn.org service
|
||||
*/
|
||||
private String buildRequestURL() {
|
||||
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
|
||||
|
||||
String location = config.location.trim();
|
||||
Integer stationId = config.stationId;
|
||||
|
||||
String geoStr = "geo:" + location.replace(" ", "").replace(",", ";").replace("\"", "").replace("'", "").trim();
|
||||
|
||||
String urlStr = URL.replace("%apikey%", config.apikey.trim());
|
||||
|
||||
return urlStr.replace("%QUERY%", stationId == null ? geoStr : "@" + stationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request new air quality data to the aqicn.org service
|
||||
*
|
||||
* @param location geo-coordinates from config
|
||||
* @param stationId station ID from config
|
||||
* @return the air quality data object mapping the JSON response or null in case of error
|
||||
*/
|
||||
private @Nullable AirQualityJsonData getAirQualityData() {
|
||||
String errorMsg;
|
||||
|
||||
String urlStr = buildRequestURL();
|
||||
logger.debug("URL = {}", urlStr);
|
||||
|
||||
try {
|
||||
String response = HttpUtil.executeUrl("GET", urlStr, null, null, null, REQUEST_TIMEOUT_MS);
|
||||
logger.debug("aqiResponse = {}", response);
|
||||
AirQualityJsonResponse result = gson.fromJson(response, AirQualityJsonResponse.class);
|
||||
if (result.getStatus() == ResponseStatus.OK) {
|
||||
AirQualityJsonData data = result.getData();
|
||||
String attributions = data.getAttributions();
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, attributions);
|
||||
return data;
|
||||
} else {
|
||||
retryCounter++;
|
||||
if (retryCounter == 1) {
|
||||
logger.warn("Error in aqicn.org, retrying once");
|
||||
return getAirQualityData();
|
||||
}
|
||||
errorMsg = "Missing data sub-object";
|
||||
logger.warn("Error in aqicn.org response: {}", errorMsg);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
errorMsg = e.getMessage();
|
||||
} catch (JsonSyntaxException e) {
|
||||
errorMsg = "Configuration is incorrect";
|
||||
logger.warn("Error running aqicn.org request: {}", errorMsg);
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
public State getValue(String channelId, AirQualityJsonData aqiResponse) {
|
||||
String[] fields = channelId.split("#");
|
||||
|
||||
switch (fields[0]) {
|
||||
case AQI:
|
||||
return new DecimalType(aqiResponse.getAqi());
|
||||
case AQIDESCRIPTION:
|
||||
return getAqiDescription(aqiResponse.getAqi());
|
||||
case PM25:
|
||||
case PM10:
|
||||
case O3:
|
||||
case NO2:
|
||||
case CO:
|
||||
case SO2:
|
||||
double value = aqiResponse.getIaqiValue(fields[0]);
|
||||
return value != -1 ? new DecimalType(value) : UnDefType.UNDEF;
|
||||
case TEMPERATURE:
|
||||
double temp = aqiResponse.getIaqiValue("t");
|
||||
return temp != -1 ? new QuantityType<>(temp, API_TEMPERATURE_UNIT) : UnDefType.UNDEF;
|
||||
case PRESSURE:
|
||||
double press = aqiResponse.getIaqiValue("p");
|
||||
return press != -1 ? new QuantityType<>(press, API_PRESSURE_UNIT) : UnDefType.UNDEF;
|
||||
case HUMIDITY:
|
||||
double hum = aqiResponse.getIaqiValue("h");
|
||||
return hum != -1 ? new QuantityType<>(hum, API_HUMIDITY_UNIT) : UnDefType.UNDEF;
|
||||
case LOCATIONNAME:
|
||||
return new StringType(aqiResponse.getCity().getName());
|
||||
case STATIONID:
|
||||
return new DecimalType(aqiResponse.getStationId());
|
||||
case STATIONLOCATION:
|
||||
return new PointType(aqiResponse.getCity().getGeo());
|
||||
case OBSERVATIONTIME:
|
||||
return new DateTimeType(
|
||||
aqiResponse.getTime().getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone()));
|
||||
case DOMINENTPOL:
|
||||
return new StringType(aqiResponse.getDominentPol());
|
||||
case AQI_COLOR:
|
||||
return getAsHSB(aqiResponse.getAqi());
|
||||
default:
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interprets the current aqi value within the ranges;
|
||||
* Returns AQI in a human readable format
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public State getAqiDescription(int index) {
|
||||
if (index >= 300) {
|
||||
return HAZARDOUS;
|
||||
} else if (index >= 201) {
|
||||
return VERY_UNHEALTHY;
|
||||
} else if (index >= 151) {
|
||||
return UNHEALTHY;
|
||||
} else if (index >= 101) {
|
||||
return UNHEALTHY_FOR_SENSITIVE;
|
||||
} else if (index >= 51) {
|
||||
return MODERATE;
|
||||
} else if (index > 0) {
|
||||
return GOOD;
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
private State getAsHSB(int index) {
|
||||
State state = getAqiDescription(index);
|
||||
if (state == HAZARDOUS) {
|
||||
return HSBType.fromRGB(343, 100, 49);
|
||||
} else if (state == VERY_UNHEALTHY) {
|
||||
return HSBType.fromRGB(280, 100, 60);
|
||||
} else if (state == UNHEALTHY) {
|
||||
return HSBType.fromRGB(345, 100, 80);
|
||||
} else if (state == UNHEALTHY_FOR_SENSITIVE) {
|
||||
return HSBType.fromRGB(30, 80, 100);
|
||||
} else if (state == MODERATE) {
|
||||
return HSBType.fromRGB(50, 80, 100);
|
||||
} else if (state == GOOD) {
|
||||
return HSBType.fromRGB(160, 100, 60);
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityJsonCity} is responsible for storing
|
||||
* the "city" node from the waqi.org JSON response
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityJsonCity {
|
||||
|
||||
private String name = "";
|
||||
private @Nullable String url;
|
||||
private List<Double> geo = new ArrayList<>();
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public @Nullable String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getGeo() {
|
||||
List<String> list = new ArrayList<>();
|
||||
geo.forEach(item -> list.add(item.toString()));
|
||||
return String.join(",", list);
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityJsonData} is responsible for storing
|
||||
* the "data" node from the waqi.org JSON response
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityJsonData {
|
||||
|
||||
private int aqi;
|
||||
private int idx;
|
||||
|
||||
private @NonNullByDefault({}) AirQualityJsonTime time;
|
||||
private @NonNullByDefault({}) AirQualityJsonCity city;
|
||||
private List<Attribute> attributions = new ArrayList<>();
|
||||
private Map<String, @Nullable AirQualityValue> iaqi = new HashMap<>();
|
||||
private String dominentpol = "";
|
||||
|
||||
/**
|
||||
* Air Quality Index
|
||||
*
|
||||
* @return {Integer}
|
||||
*/
|
||||
public int getAqi() {
|
||||
return aqi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measuring Station ID
|
||||
*
|
||||
* @return {Integer}
|
||||
*/
|
||||
public int getStationId() {
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives "time" node from the "data" object in JSON response
|
||||
*
|
||||
* @return {AirQualityJsonTime}
|
||||
*/
|
||||
public AirQualityJsonTime getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives "city" node from the "data" object in JSON response
|
||||
*
|
||||
* @return {AirQualityJsonCity}
|
||||
*/
|
||||
public AirQualityJsonCity getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects a list of attributions (vendors making data available)
|
||||
* and transforms it into readable string.
|
||||
* Currently displayed in Thing Status description when ONLINE
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
public String getAttributions() {
|
||||
List<String> list = new ArrayList<>();
|
||||
attributions.forEach(item -> list.add(item.getName()));
|
||||
return "Attributions : " + String.join(", ", list);
|
||||
}
|
||||
|
||||
public String getDominentPol() {
|
||||
return dominentpol;
|
||||
}
|
||||
|
||||
public double getIaqiValue(String key) {
|
||||
AirQualityValue result = iaqi.get(key);
|
||||
if (result != null) {
|
||||
return result.getValue();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityJsonResponse} is the Java class used to map the JSON
|
||||
* response to the aqicn.org request.
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityJsonResponse {
|
||||
|
||||
public static enum ResponseStatus {
|
||||
NONE,
|
||||
@SerializedName("error")
|
||||
ERROR,
|
||||
@SerializedName("ok")
|
||||
OK;
|
||||
}
|
||||
|
||||
private ResponseStatus status = ResponseStatus.NONE;
|
||||
|
||||
@SerializedName("data")
|
||||
private @NonNullByDefault({}) AirQualityJsonData data;
|
||||
|
||||
public ResponseStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public AirQualityJsonData getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link AirQualityJsonTime} is responsible for storing
|
||||
* the "time" node from the waqi.org JSON response
|
||||
*
|
||||
* @author Kuba Wolanin - Initial contribution
|
||||
* @author Gaël L'hopital - Use ZonedDateTime instead of Calendar
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityJsonTime {
|
||||
|
||||
@SerializedName("s")
|
||||
private String dateString = "";
|
||||
|
||||
@SerializedName("tz")
|
||||
private String timeZone = "";
|
||||
|
||||
private String iso = "";
|
||||
|
||||
/**
|
||||
* Get observation time
|
||||
*
|
||||
* @return {ZonedDateTime}
|
||||
*/
|
||||
public ZonedDateTime getObservationTime() throws DateTimeParseException {
|
||||
return ZonedDateTime.parse(iso);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Wrapper type around values reported by aqicn index values.
|
||||
*
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirQualityValue {
|
||||
|
||||
@SerializedName("v")
|
||||
private double value;
|
||||
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airquality.internal.json;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Attribute representation.
|
||||
*
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Attribute {
|
||||
|
||||
private @NonNullByDefault({}) String name;
|
||||
private @Nullable String url;
|
||||
private @Nullable String logo;
|
||||
|
||||
public @Nullable String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public @Nullable String getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="airquality" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Air Quality Binding</name>
|
||||
<description>Measure Air Quality Index and details about pollution particles for a given location</description>
|
||||
<author>Kuba Wolanin</author>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,30 @@
|
||||
# binding
|
||||
binding.airquality.name = Extension Air Quality
|
||||
binding.airquality.description = Indice de qualité de l'air et informations sur la pollution aux particules pour un emplacement donné.
|
||||
|
||||
# thing types
|
||||
thing-type.airquality.aqi.label = Qualité de l'air
|
||||
thing-type.airquality.aqi.description = Fournit diverses données sur la qualité de l'air du World Air Quality Project. Pour recevoir les données, vous devez créer un compte sur http://aqicn.org/data-platform/token/ pour obtenir votre token API.
|
||||
|
||||
channel-type.airquality.aqiLevel.label = Indice
|
||||
channel-type.airquality.aqiDescription.label = Appréciation
|
||||
channel-type.airquality.observationTime.label = Heure d'observation
|
||||
channel-type.airquality.temperature.label = Température
|
||||
channel-type.airquality.pressure.label = Pression
|
||||
channel-type.airquality.humidity.label = Humidité
|
||||
channel-type.airquality.dominentpol.label = Polluant principal
|
||||
|
||||
|
||||
channel-type.airquality.aqiDescription.state.option.GOOD = Bonne
|
||||
channel-type.airquality.aqiDescription.state.option.MODERATE = Modérée
|
||||
channel-type.airquality.aqiDescription.state.option.UNHEALTHY_FOR_SENSITIVE = Mauvaise pour les groupes sensibles
|
||||
channel-type.airquality.aqiDescription.state.option.UNHEALTHY = Mauvaise
|
||||
channel-type.airquality.aqiDescription.state.option.VERY_UNHEALTHY = Très mauvaise
|
||||
channel-type.airquality.aqiDescription.state.option.HAZARDOUS = Dangereuse
|
||||
|
||||
channel-type.airquality.dominentPol.state.option.pm25 = Particules fines
|
||||
channel-type.airquality.dominentPol.state.option.pm10 = Particules de poussière
|
||||
channel-type.airquality.dominentPol.state.option.o3 = Ozone
|
||||
channel-type.airquality.dominentPol.state.option.no2 = Dioxyde d'azote
|
||||
channel-type.airquality.dominentPol.state.option.co = Monoxyde de carbone
|
||||
channel-type.airquality.dominentPol.state.option.so2 = Dioxyde de soufre
|
@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airquality"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Air Quality Thing -->
|
||||
<thing-type id="aqi">
|
||||
<label>Air Quality</label>
|
||||
<description>
|
||||
Provides various air quality data from the World Air Quality Project.
|
||||
In order to receive the data, you
|
||||
must register an account on http://aqicn.org/data-platform/token/ and get your API
|
||||
token.
|
||||
</description>
|
||||
|
||||
<channels>
|
||||
<channel id="aqiLevel" typeId="aqiLevel"/>
|
||||
<channel id="aqiColor" typeId="aqiColor"/>
|
||||
<channel id="aqiDescription" typeId="aqiDescription"/>
|
||||
<channel id="pm25" typeId="pm25"/>
|
||||
<channel id="pm10" typeId="pm10"/>
|
||||
<channel id="o3" typeId="o3"/>
|
||||
<channel id="no2" typeId="no2"/>
|
||||
<channel id="co" typeId="co"/>
|
||||
<channel id="so2" typeId="so2"/>
|
||||
<channel id="locationName" typeId="locationName"/>
|
||||
<channel id="stationLocation" typeId="stationLocation"/>
|
||||
<channel id="stationId" typeId="stationId"/>
|
||||
<channel id="observationTime" typeId="observationTime"/>
|
||||
<channel id="temperature" typeId="temperature"/>
|
||||
<channel id="pressure" typeId="pressure"/>
|
||||
<channel id="humidity" typeId="humidity"/>
|
||||
<channel id="dominentpol" typeId="dominentPol"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="apikey" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>API Key</label>
|
||||
<description>Data-platform token to access the AQIcn.org service</description>
|
||||
</parameter>
|
||||
<parameter name="location" type="text" required="false"
|
||||
pattern="^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)[,]\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$">
|
||||
<label>Location</label>
|
||||
<description>Your geo coordinates separated with comma (e.g. "37.8,-122.4").</description>
|
||||
</parameter>
|
||||
<parameter name="stationId" type="integer" required="false">
|
||||
<label>Station ID</label>
|
||||
<description>Fill only in case you want to receive data from the specific station</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" min="30" required="false" unit="min">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in minutes.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>60</default>
|
||||
<unitLabel>Minutes</unitLabel>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="aqiLevel">
|
||||
<item-type>Number</item-type>
|
||||
<label>Air Quality Index</label>
|
||||
<description></description>
|
||||
<category>Air Quality Index</category>
|
||||
<state readOnly="true" pattern="%d" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="aqiDescription">
|
||||
<item-type>String</item-type>
|
||||
<label>AQI Description</label>
|
||||
<description></description>
|
||||
<category>AQI Description</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="GOOD">Good</option>
|
||||
<option value="MODERATE">Moderate</option>
|
||||
<option value="UNHEALTHY_FOR_SENSITIVE">Unhealthy for Sensitive Groups</option>
|
||||
<option value="UNHEALTHY">Unhealthy</option>
|
||||
<option value="VERY_UNHEALTHY">Very Unhealthy</option>
|
||||
<option value="HAZARDOUS">Hazardous</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pm25">
|
||||
<item-type>Number</item-type>
|
||||
<label>PM2.5</label>
|
||||
<description>Fine particles pollution level</description>
|
||||
<category>PM2.5</category>
|
||||
<state readOnly="true" pattern="%d" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pm10">
|
||||
<item-type>Number</item-type>
|
||||
<label>PM10</label>
|
||||
<description>Coarse dust particles pollution level</description>
|
||||
<category>PM10</category>
|
||||
<state readOnly="true" pattern="%d" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="o3">
|
||||
<item-type>Number</item-type>
|
||||
<label>O3</label>
|
||||
<description>Ozone level</description>
|
||||
<category>O3</category>
|
||||
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="no2">
|
||||
<item-type>Number</item-type>
|
||||
<label>NO2</label>
|
||||
<description>Nitrogen dioxide level</description>
|
||||
<category>NO2</category>
|
||||
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="co">
|
||||
<item-type>Number</item-type>
|
||||
<label>CO</label>
|
||||
<description>Carbon monoxide level</description>
|
||||
<category>CO</category>
|
||||
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="so2">
|
||||
<item-type>Number</item-type>
|
||||
<label>SO2</label>
|
||||
<description>Sulfur dioxide level</description>
|
||||
<category>SO2</category>
|
||||
<state readOnly="true" pattern="%.1f"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="locationName" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Location</label>
|
||||
<description>Nearest measuring station location</description>
|
||||
<category>Location</category>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="stationLocation" advanced="true">
|
||||
<item-type>Location</item-type>
|
||||
<label>Station Location</label>
|
||||
<description>Location of the measuring station</description>
|
||||
<category>Station Location</category>
|
||||
<state readOnly="true" pattern="%2$s°N,%3$s°W"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="stationId" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Station ID</label>
|
||||
<description>Unique measuring station ID</description>
|
||||
<category>Station ID</category>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="observationTime" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Observation Time</label>
|
||||
<description>Observation date and time</description>
|
||||
<category>Observation time</category>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pressure" advanced="true">
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Pressure</label>
|
||||
<description>Current Pressure</description>
|
||||
<category>Pressure</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="humidity" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Current humidity</description>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" min="0" max="100" pattern="%.2f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dominentPol">
|
||||
<item-type>String</item-type>
|
||||
<label>Dominent Polutor</label>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="pm25">Fine particles</option>
|
||||
<option value="pm10">Coarse dust particles</option>
|
||||
<option value="o3">Ozone</option>
|
||||
<option value="no2">Nitrogen Dioxide</option>
|
||||
<option value="co">Carbon Monoxide</option>
|
||||
<option value="so2">Sulfur Dioxide</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="aqiColor" advanced="true">
|
||||
<item-type>Color</item-type>
|
||||
<label>AQI Color</label>
|
||||
<description>Color associated to given AQI Index.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
32
bundles/org.openhab.binding.airvisualnode/.classpath
Normal file
32
bundles/org.openhab.binding.airvisualnode/.classpath
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
23
bundles/org.openhab.binding.airvisualnode/.project
Normal file
23
bundles/org.openhab.binding.airvisualnode/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.airvisualnode</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
21
bundles/org.openhab.binding.airvisualnode/NOTICE
Normal file
21
bundles/org.openhab.binding.airvisualnode/NOTICE
Normal file
@ -0,0 +1,21 @@
|
||||
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
|
||||
|
||||
jcifs
|
||||
* License: LGPL v2.1 License
|
||||
* License: BSD License
|
||||
* Project: https://www.jcifs.org
|
||||
* Source: https://www.jcifs.org/src/src/jcifs
|
132
bundles/org.openhab.binding.airvisualnode/README.md
Normal file
132
bundles/org.openhab.binding.airvisualnode/README.md
Normal file
@ -0,0 +1,132 @@
|
||||
# AirVisual Node Binding
|
||||
|
||||
This is an openHAB binding for the [AirVisual Node Air Quality Monitor](https://airvisual.com/node) (also known as IQAir AirVisual Pro).
|
||||
|
||||
## Supported Things
|
||||
|
||||
There is one supported Thing, the "avnode".
|
||||
|
||||
## Discovery
|
||||
|
||||
Binding will do autodiscovery for AirVisual Node by searching for a host advertised with the NetBIOS name 'AVISUAL-<SerialNumber>'.
|
||||
|
||||
All discovered devices will be added to the inbox. Please note you will need to set the Node username and password in the configuration
|
||||
of the newly discovered thing before a connection can be made.
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
The binding has no configuration options, all configuration is done at Thing level.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The thing has a few configuration parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------------------------------------------------------------------------------|
|
||||
| address | Hostname or IP address of the Node |
|
||||
| username | The Node Samba share username. Default is 'airvisual' |
|
||||
| password | The Node Samba share password |
|
||||
| share | (Optional) The Node SMB share name. Default is 'airvisual' |
|
||||
| refresh | (Optional) The time (in seconds) to refresh the Node data. Default is 60, min is 30 |
|
||||
|
||||
Required configuration parameters can be obtained by pressing the center button on the Node for "Settings Menu" > "Network" > "Access Node data" tab.
|
||||
|
||||
## Channels
|
||||
|
||||
The binding supports the following channels:
|
||||
|
||||
| Channel ID | Item Type | Description |
|
||||
|-----------------|-----------------------|-----------------------------|
|
||||
| co2 | Number:Dimensionless | CO2 level, ppm |
|
||||
| humidity | Number:Dimensionless | Relative humidity, % |
|
||||
| aqi | Number:Dimensionless | Air Quality Index (US) |
|
||||
| pm_25 | Number:Density | PM2.5 level, µg/m³ |
|
||||
| temperature | Number:Temperature | Temperature |
|
||||
| used_memory | Number | Used memory |
|
||||
| timestamp | DateTime | Timestamp |
|
||||
| battery-level | Number | Battery level, % |
|
||||
| signal-strength | Number | Wi-Fi signal strength, 0-4 |
|
||||
|
||||
The Node updates measurements data every 5 minutes in active mode and every 15 minutes in power saving mode (screen off).
|
||||
|
||||
## Example
|
||||
|
||||
### Thing
|
||||
|
||||
The preferred way to add AirVisual Node to the openHAB installation is autodiscovery,
|
||||
but the AirVisual Node also can be configured using `.things` file:
|
||||
|
||||
```
|
||||
airvisualnode:avnode:1a2b3c4 [ address="192.168.1.32", username="airvisual", password="12345", share="airvisual", refresh=60 ]
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
Here is an example of items for the AirVisual Node:
|
||||
|
||||
```
|
||||
Number:Temperature Livingroom_Temperature "Temperature [%.1f %unit%]" <temperature> {channel="airvisualnode:avnode:1a2b3c4:temperature"}
|
||||
Number:Dimensionless Livingroom_Humidity "Humidity [%d %unit%]" <humidity> {channel="airvisualnode:avnode:1a2b3c4:humidity"}
|
||||
Number:Dimensionless Livingroom_CO2_Level "CO₂" {channel="airvisualnode:avnode:1a2b3c4:co2"}
|
||||
Number:Dimensionless Livingroom_Aqi_Level "Air Quality Index" { channel="airvisualnode:avnode:1a2b3c4:aqi" }
|
||||
Number:Density Livingroom_Pm25_Level "PM2.5 Level" { channel="airvisualnode:avnode:1a2b3c4:pm_25" }
|
||||
DateTime Livingroom_Aqi_Timestamp "AQI Timestamp [%1$tH:%1$tM]" { channel="airvisualnode:avnode:1a2b3c4:timestamp" }
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
Example rules:
|
||||
|
||||
```
|
||||
rule "AirVisual Node Temperature Rule"
|
||||
when
|
||||
Item Livingroom_Temperature changed
|
||||
then
|
||||
if (Livingroom_Temperature.state > 25.0|°C) {
|
||||
logInfo("avnode.rules", "Temperature is above 25°C")
|
||||
}
|
||||
end
|
||||
|
||||
rule "AirVisual Node Humidity Rule"
|
||||
when
|
||||
Item Livingroom_Humidity changed
|
||||
then
|
||||
if (Livingroom_Humidity.state < 35.0|%) {
|
||||
logInfo("avnode.rules", "Humidity is below 35%")
|
||||
}
|
||||
end
|
||||
|
||||
rule "AirVisual Node CO₂ Level Rule"
|
||||
when
|
||||
Item Livingroom_CO2_Level changed
|
||||
then
|
||||
if (Livingroom_CO2_Level.state > 1000.0|"ppm") {
|
||||
logInfo("avnode.rules", "CO₂ level is above 1000 ppm")
|
||||
}
|
||||
end
|
||||
|
||||
rule "AirVisual Node PM2.5 Level Rule"
|
||||
when
|
||||
Item Livingroom_Pm25_Level changed
|
||||
then
|
||||
if (Livingroom_Pm25_Level.state > 25.0|"µg/m³") {
|
||||
logInfo("avnode.rules", "PM2.5 level is above 25 µg/m³")
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
### Sitemap
|
||||
|
||||
Example sitemap:
|
||||
|
||||
```
|
||||
sitemap home label="Home" {
|
||||
Frame label="Living Room" {
|
||||
Text item=Livingroom_Temperature
|
||||
Text item=Livingroom_Humidity
|
||||
Text item=Livingroom_CO2_Level
|
||||
Text item=Livingroom_Aqi_Level
|
||||
Text item=Livingroom_Pm25_Level
|
||||
}
|
||||
}
|
||||
```
|
26
bundles/org.openhab.binding.airvisualnode/pom.xml
Normal file
26
bundles/org.openhab.binding.airvisualnode/pom.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.airvisualnode</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: AirVisual Node Air Quality Monitor Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.samba.jcifs</groupId>
|
||||
<artifactId>jcifs</artifactId>
|
||||
<version>1.3.17</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.airvisualnode-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-airvisualnode" description="AirVisual Node Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle dependency="true" start-level="80">mvn:org.samba.jcifs/jcifs/1.3.17</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airvisualnode/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AirVisualNodeBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirVisualNodeBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "airvisualnode";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_AVNODE = new ThingTypeUID(BINDING_ID, "avnode");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_CO2 = "co2";
|
||||
public static final String CHANNEL_HUMIDITY = "humidity";
|
||||
public static final String CHANNEL_AQI_US = "aqi";
|
||||
public static final String CHANNEL_PM_25 = "pm_25";
|
||||
public static final String CHANNEL_TEMP_CELSIUS = "temperature";
|
||||
public static final String CHANNEL_TIMESTAMP = "timestamp";
|
||||
public static final String CHANNEL_USED_MEMORY = "used_memory";
|
||||
public static final String CHANNEL_BATTERY_LEVEL = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL
|
||||
.getUID().getId();
|
||||
public static final String CHANNEL_WIFI_STRENGTH = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_SIGNAL_STRENGTH
|
||||
.getUID().getId();
|
||||
|
||||
// List of all supported Thing UIDs
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(new HashSet<>(Arrays.asList(THING_TYPE_AVNODE)));
|
||||
|
||||
// List of all supported Channel ids
|
||||
public static final Set<String> SUPPORTED_CHANNEL_IDS = Collections.unmodifiableSet(new HashSet<>(
|
||||
Arrays.asList(CHANNEL_CO2, CHANNEL_HUMIDITY, CHANNEL_AQI_US, CHANNEL_PM_25, CHANNEL_TEMP_CELSIUS,
|
||||
CHANNEL_BATTERY_LEVEL, CHANNEL_WIFI_STRENGTH, CHANNEL_TIMESTAMP, CHANNEL_USED_MEMORY)));
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal;
|
||||
|
||||
import static org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airvisualnode.internal.handler.AirVisualNodeHandler;
|
||||
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 AirVisualNodeHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.airvisualnode")
|
||||
public class AirVisualNodeHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@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(THING_TYPE_AVNODE)) {
|
||||
return new AirVisualNodeHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.config;
|
||||
|
||||
/**
|
||||
* Configuration for AirVisual Node.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class AirVisualNodeConfig {
|
||||
|
||||
public static final String ADDRESS = "address";
|
||||
|
||||
public String address;
|
||||
|
||||
public String username;
|
||||
|
||||
public String password;
|
||||
|
||||
public String share;
|
||||
|
||||
public long refresh;
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.discovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants;
|
||||
import org.openhab.binding.airvisualnode.internal.config.AirVisualNodeConfig;
|
||||
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.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jcifs.netbios.NbtAddress;
|
||||
import jcifs.smb.SmbFile;
|
||||
|
||||
/**
|
||||
* Autodiscovery for AirVisual Node by searching for a host advertised with the NetBIOS name 'AVISUAL-<SerialNumber>'.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, immediate = true)
|
||||
public class AirVisualNodeDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirVisualNodeDiscoveryService.class);
|
||||
|
||||
public static final String AVISUAL_WORKGROUP_NAME = "MSHOME";
|
||||
|
||||
private static final Pattern AVISUAL_NAME_PATTERN = Pattern.compile("^AVISUAL-([^/]+)$");
|
||||
|
||||
private ScheduledFuture<?> backgroundDiscoveryFuture;
|
||||
|
||||
public AirVisualNodeDiscoveryService() {
|
||||
super(Collections.singleton(AirVisualNodeBindingConstants.THING_TYPE_AVNODE), 600, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting scan");
|
||||
scheduler.execute(this::scan);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Starting background discovery");
|
||||
backgroundDiscoveryFuture = scheduler.scheduleWithFixedDelay(this::scan, 0, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping background discovery");
|
||||
cancelBackgroundDiscoveryFuture();
|
||||
super.stopBackgroundDiscovery();
|
||||
}
|
||||
|
||||
private void cancelBackgroundDiscoveryFuture() {
|
||||
if (backgroundDiscoveryFuture != null && !backgroundDiscoveryFuture.isDone()) {
|
||||
backgroundDiscoveryFuture.cancel(true);
|
||||
backgroundDiscoveryFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
// Get all workgroup members
|
||||
SmbFile[] workgroupMembers;
|
||||
try {
|
||||
String workgroupUrl = "smb://" + AVISUAL_WORKGROUP_NAME + "/";
|
||||
workgroupMembers = new SmbFile(workgroupUrl).listFiles();
|
||||
} catch (IOException e) {
|
||||
// Can't get workgroup member list
|
||||
return;
|
||||
}
|
||||
|
||||
// Check found workgroup members for the Node devices
|
||||
for (SmbFile s : workgroupMembers) {
|
||||
String serverName = s.getServer();
|
||||
|
||||
// Check workgroup member for the Node device name match
|
||||
Matcher m = AVISUAL_NAME_PATTERN.matcher(serverName);
|
||||
if (!m.find()) {
|
||||
// Workgroup member server name doesn't match the Node device name pattern
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the Node serial number from device name
|
||||
String nodeSerialNumber = m.group(1);
|
||||
|
||||
// The Node Thing UID is serial number converted to lower case
|
||||
ThingUID thingUID = new ThingUID(AirVisualNodeBindingConstants.THING_TYPE_AVNODE,
|
||||
nodeSerialNumber.toLowerCase());
|
||||
|
||||
try {
|
||||
// Get the Node address by name
|
||||
NbtAddress nodeNbtAddress = NbtAddress.getByName(serverName);
|
||||
if (nodeNbtAddress == null) {
|
||||
// The Node address not found by some reason, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create discovery result
|
||||
String nodeAddress = nodeNbtAddress.getInetAddress().getHostAddress();
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(AirVisualNodeConfig.ADDRESS, nodeAddress)
|
||||
.withRepresentationProperty(AirVisualNodeConfig.ADDRESS)
|
||||
.withLabel("AirVisual Node (" + nodeSerialNumber + ")").build();
|
||||
thingDiscovered(result);
|
||||
} catch (UnknownHostException e) {
|
||||
logger.debug("The Node address resolving failed ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.MetricPrefix.MICRO;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
import static org.openhab.core.library.unit.SIUnits.CUBIC_METRE;
|
||||
import static org.openhab.core.library.unit.SIUnits.GRAM;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.ONE;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.PARTS_PER_MILLION;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.zone.ZoneRules;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.openhab.binding.airvisualnode.internal.config.AirVisualNodeConfig;
|
||||
import org.openhab.binding.airvisualnode.internal.json.NodeData;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import jcifs.smb.NtlmPasswordAuthentication;
|
||||
import jcifs.smb.SmbFile;
|
||||
import jcifs.smb.SmbFileInputStream;
|
||||
|
||||
/**
|
||||
* The {@link AirVisualNodeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class AirVisualNodeHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirVisualNodeHandler.class);
|
||||
|
||||
public static final String NODE_JSON_FILE = "latest_config_measurements.json";
|
||||
|
||||
private final Gson gson;
|
||||
|
||||
private ScheduledFuture<?> pollFuture;
|
||||
|
||||
private long refreshInterval;
|
||||
|
||||
private String nodeAddress;
|
||||
|
||||
private String nodeUsername;
|
||||
|
||||
private String nodePassword;
|
||||
|
||||
private String nodeShareName;
|
||||
|
||||
private NodeData nodeData;
|
||||
|
||||
public AirVisualNodeHandler(Thing thing) {
|
||||
super(thing);
|
||||
gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing AirVisual Node handler");
|
||||
|
||||
AirVisualNodeConfig config = getConfigAs(AirVisualNodeConfig.class);
|
||||
|
||||
if (config.address == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Node address must be set");
|
||||
return;
|
||||
}
|
||||
this.nodeAddress = config.address;
|
||||
|
||||
this.nodeUsername = config.username;
|
||||
|
||||
if (config.password == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Node password must be set");
|
||||
return;
|
||||
}
|
||||
this.nodePassword = config.password;
|
||||
|
||||
this.nodeShareName = config.share;
|
||||
|
||||
this.refreshInterval = config.refresh * 1000L;
|
||||
|
||||
schedulePoll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateChannel(channelUID.getId(), true);
|
||||
} else {
|
||||
logger.debug("Can not handle command '{}'", command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
super.handleRemoval();
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
private synchronized void stopPoll() {
|
||||
if (pollFuture != null && !pollFuture.isCancelled()) {
|
||||
pollFuture.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void schedulePoll() {
|
||||
logger.debug("Scheduling poll for 500ms out, then every {} ms", refreshInterval);
|
||||
pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 500, refreshInterval, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void poll() {
|
||||
try {
|
||||
logger.debug("Polling for state");
|
||||
pollNode();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Could not connect to Node", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void pollNode() throws IOException {
|
||||
String jsonData = getNodeJsonData();
|
||||
NodeData currentNodeData = gson.fromJson(jsonData, NodeData.class);
|
||||
if (nodeData == null || currentNodeData.getStatus().getDatetime() > nodeData.getStatus().getDatetime()) {
|
||||
nodeData = currentNodeData;
|
||||
// Update all channels from the updated Node data
|
||||
for (Channel channel : getThing().getChannels()) {
|
||||
updateChannel(channel.getUID().getId(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getNodeJsonData() throws IOException {
|
||||
String url = "smb://" + nodeAddress + "/" + nodeShareName + "/" + NODE_JSON_FILE;
|
||||
NtlmPasswordAuthentication auth = new NtlmPasswordAuthentication(null, nodeUsername, nodePassword);
|
||||
try (SmbFileInputStream in = new SmbFileInputStream(new SmbFile(url, auth))) {
|
||||
return IOUtils.toString(in, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannel(String channelId, boolean force) {
|
||||
if (nodeData != null && (force || isLinked(channelId))) {
|
||||
State state = getChannelState(channelId, nodeData);
|
||||
logger.debug("Update channel {} with state {}", channelId, state);
|
||||
updateState(channelId, state);
|
||||
}
|
||||
}
|
||||
|
||||
private State getChannelState(String channelId, NodeData nodeData) {
|
||||
State state = UnDefType.UNDEF;
|
||||
|
||||
// Handle system channel IDs separately, because 'switch/case' expressions must be constant expressions
|
||||
if (CHANNEL_BATTERY_LEVEL.equals(channelId)) {
|
||||
state = new DecimalType(BigDecimal.valueOf(nodeData.getStatus().getBattery()).longValue());
|
||||
} else if (CHANNEL_WIFI_STRENGTH.equals(channelId)) {
|
||||
state = new DecimalType(
|
||||
BigDecimal.valueOf(Math.max(0, nodeData.getStatus().getWifiStrength() - 1)).longValue());
|
||||
} else {
|
||||
// Handle binding-specific channel IDs
|
||||
switch (channelId) {
|
||||
case CHANNEL_CO2:
|
||||
state = new QuantityType<>(nodeData.getMeasurements().getCo2Ppm(), PARTS_PER_MILLION);
|
||||
break;
|
||||
case CHANNEL_HUMIDITY:
|
||||
state = new QuantityType<>(nodeData.getMeasurements().getHumidityRH(), PERCENT);
|
||||
break;
|
||||
case CHANNEL_AQI_US:
|
||||
state = new QuantityType<>(nodeData.getMeasurements().getPm25AQIUS(), ONE);
|
||||
break;
|
||||
case CHANNEL_PM_25:
|
||||
// PM2.5 is in ug/m3
|
||||
state = new QuantityType<>(nodeData.getMeasurements().getPm25Ugm3(),
|
||||
MICRO(GRAM).divide(CUBIC_METRE));
|
||||
break;
|
||||
case CHANNEL_TEMP_CELSIUS:
|
||||
state = new QuantityType<>(nodeData.getMeasurements().getTemperatureC(), CELSIUS);
|
||||
break;
|
||||
case CHANNEL_TIMESTAMP:
|
||||
// It seem the Node timestamp is Unix timestamp converted from UTC time plus timezone offset.
|
||||
// Not sure about DST though, but it's best guess at now
|
||||
Instant instant = Instant.ofEpochMilli(nodeData.getStatus().getDatetime() * 1000L);
|
||||
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
|
||||
ZoneId zoneId = ZoneId.of(nodeData.getSettings().getTimezone());
|
||||
ZoneRules zoneRules = zoneId.getRules();
|
||||
zonedDateTime.minus(Duration.ofSeconds(zoneRules.getOffset(instant).getTotalSeconds()));
|
||||
if (zoneRules.isDaylightSavings(instant)) {
|
||||
zonedDateTime.minus(Duration.ofSeconds(zoneRules.getDaylightSavings(instant).getSeconds()));
|
||||
}
|
||||
state = new DateTimeType(zonedDateTime);
|
||||
break;
|
||||
case CHANNEL_USED_MEMORY:
|
||||
state = new DecimalType(BigDecimal.valueOf(nodeData.getStatus().getUsedMemory()).longValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Date and time / timestamp data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class DateAndTime {
|
||||
|
||||
private String date;
|
||||
private String time;
|
||||
private String timestamp;
|
||||
|
||||
public DateAndTime(String date, String time, String timestamp) {
|
||||
this.date = date;
|
||||
this.time = time;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Measurements data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class Measurements {
|
||||
|
||||
private int co2Ppm;
|
||||
@SerializedName("humidity_RH")
|
||||
private int humidityRH;
|
||||
@SerializedName("pm25_AQICN")
|
||||
private int pm25AQICN;
|
||||
@SerializedName("pm25_AQIUS")
|
||||
private int pm25AQIUS;
|
||||
private float pm25Ugm3;
|
||||
@SerializedName("temperature_C")
|
||||
private float temperatureC;
|
||||
@SerializedName("temperature_F")
|
||||
private float temperatureF;
|
||||
private int vocPpb;
|
||||
|
||||
public Measurements(int co2Ppm, int humidityRH, int pm25AQICN, int pm25AQIUS, float pm25Ugm3, float temperatureC,
|
||||
float temperatureF, int vocPpb) {
|
||||
this.co2Ppm = co2Ppm;
|
||||
this.humidityRH = humidityRH;
|
||||
this.pm25AQICN = pm25AQICN;
|
||||
this.pm25AQIUS = pm25AQIUS;
|
||||
this.pm25Ugm3 = pm25Ugm3;
|
||||
this.temperatureC = temperatureC;
|
||||
this.temperatureF = temperatureF;
|
||||
this.vocPpb = vocPpb;
|
||||
}
|
||||
|
||||
public int getCo2Ppm() {
|
||||
return co2Ppm;
|
||||
}
|
||||
|
||||
public void setCo2Ppm(int co2Ppm) {
|
||||
this.co2Ppm = co2Ppm;
|
||||
}
|
||||
|
||||
public int getHumidityRH() {
|
||||
return humidityRH;
|
||||
}
|
||||
|
||||
public void setHumidityRH(int humidityRH) {
|
||||
this.humidityRH = humidityRH;
|
||||
}
|
||||
|
||||
public int getPm25AQICN() {
|
||||
return pm25AQICN;
|
||||
}
|
||||
|
||||
public void setPm25AQICN(int pm25AQICN) {
|
||||
this.pm25AQICN = pm25AQICN;
|
||||
}
|
||||
|
||||
public int getPm25AQIUS() {
|
||||
return pm25AQIUS;
|
||||
}
|
||||
|
||||
public void setPm25AQIUS(int pm25AQIUS) {
|
||||
this.pm25AQIUS = pm25AQIUS;
|
||||
}
|
||||
|
||||
public float getPm25Ugm3() {
|
||||
return pm25Ugm3;
|
||||
}
|
||||
|
||||
public void setPm25Ugm3(float pm25Ugm3) {
|
||||
this.pm25Ugm3 = pm25Ugm3;
|
||||
}
|
||||
|
||||
public float getTemperatureC() {
|
||||
return temperatureC;
|
||||
}
|
||||
|
||||
public void setTemperatureC(float temperatureC) {
|
||||
this.temperatureC = temperatureC;
|
||||
}
|
||||
|
||||
public float getTemperatureF() {
|
||||
return temperatureF;
|
||||
}
|
||||
|
||||
public void setTemperatureF(float temperatureF) {
|
||||
this.temperatureF = temperatureF;
|
||||
}
|
||||
|
||||
public int getVocPpb() {
|
||||
return vocPpb;
|
||||
}
|
||||
|
||||
public void setVocPpb(int vocPpb) {
|
||||
this.vocPpb = vocPpb;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Top level object for AirVisual Node JSON data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class NodeData {
|
||||
|
||||
private DateAndTime dateAndTime;
|
||||
private Measurements measurements;
|
||||
private String serialNumber;
|
||||
private Settings settings;
|
||||
private Status status;
|
||||
|
||||
public NodeData(DateAndTime dateAndTime, Measurements measurements, String serialNumber, Settings settings,
|
||||
Status status) {
|
||||
this.dateAndTime = dateAndTime;
|
||||
this.measurements = measurements;
|
||||
this.serialNumber = serialNumber;
|
||||
this.settings = settings;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public DateAndTime getDateAndTime() {
|
||||
return dateAndTime;
|
||||
}
|
||||
|
||||
public void setDateAndTime(DateAndTime dateAndTime) {
|
||||
this.dateAndTime = dateAndTime;
|
||||
}
|
||||
|
||||
public Measurements getMeasurements() {
|
||||
return measurements;
|
||||
}
|
||||
|
||||
public void setMeasurements(Measurements measurements) {
|
||||
this.measurements = measurements;
|
||||
}
|
||||
|
||||
public String getSerialNumber() {
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
public void setSerialNumber(String serialNumber) {
|
||||
this.serialNumber = serialNumber;
|
||||
}
|
||||
|
||||
public Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public void setSettings(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Power saving data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class PowerSaving {
|
||||
|
||||
@SerializedName("2slots")
|
||||
private List<PowerSavingTimeSlot> timeSlots = null;
|
||||
private String mode;
|
||||
@SerializedName("yes")
|
||||
private List<PowerSavingTime> times = null;
|
||||
|
||||
public PowerSaving(List<PowerSavingTimeSlot> timeSlots, String mode, List<PowerSavingTime> times) {
|
||||
this.mode = mode;
|
||||
this.times = times;
|
||||
this.timeSlots = timeSlots;
|
||||
}
|
||||
|
||||
public List<PowerSavingTimeSlot> getTimeSlots() {
|
||||
return timeSlots;
|
||||
}
|
||||
|
||||
public void setTimeSlots(List<PowerSavingTimeSlot> timeSlots) {
|
||||
this.timeSlots = timeSlots;
|
||||
}
|
||||
|
||||
public List<PowerSavingTime> getTimes() {
|
||||
return times;
|
||||
}
|
||||
|
||||
public void setTimes(List<PowerSavingTime> times) {
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Power saving time data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class PowerSavingTime {
|
||||
|
||||
private int hour;
|
||||
private int minute;
|
||||
|
||||
public PowerSavingTime(int hour, int minute) {
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
}
|
||||
|
||||
public int getHour() {
|
||||
return hour;
|
||||
}
|
||||
|
||||
public void setHour(int hour) {
|
||||
this.hour = hour;
|
||||
}
|
||||
|
||||
public int getMinute() {
|
||||
return minute;
|
||||
}
|
||||
|
||||
public void setMinute(int minute) {
|
||||
this.minute = minute;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Power saving time slot data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class PowerSavingTimeSlot {
|
||||
|
||||
private int hourOff;
|
||||
private int hourOn;
|
||||
|
||||
public PowerSavingTimeSlot(int hourOff, int hourOn) {
|
||||
this.hourOff = hourOff;
|
||||
this.hourOn = hourOn;
|
||||
}
|
||||
|
||||
public int getHourOff() {
|
||||
return hourOff;
|
||||
}
|
||||
|
||||
public void setHourOff(int hourOff) {
|
||||
this.hourOff = hourOff;
|
||||
}
|
||||
|
||||
public int getHourOn() {
|
||||
return hourOn;
|
||||
}
|
||||
|
||||
public void setHourOn(int hourOn) {
|
||||
this.hourOn = hourOn;
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Settings data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
private String followedStation;
|
||||
private boolean isAqiUsa;
|
||||
private boolean isConcentrationShowed;
|
||||
private boolean isIndoor;
|
||||
private boolean isLcdOn;
|
||||
private boolean isNetworkTime;
|
||||
private boolean isTemperatureCelsius;
|
||||
private String language;
|
||||
private int lcdBrightness;
|
||||
private String nodeName;
|
||||
private PowerSaving powerSaving;
|
||||
private String speedUnit;
|
||||
private String timezone;
|
||||
|
||||
public Settings(String followedStation, boolean isAqiUsa, boolean isConcentrationShowed, boolean isIndoor,
|
||||
boolean isLcdOn, boolean isNetworkTime, boolean isTemperatureCelsius, String language, int lcdBrightness,
|
||||
String nodeName, PowerSaving powerSaving, String speedUnit, String timezone) {
|
||||
this.followedStation = followedStation;
|
||||
this.isAqiUsa = isAqiUsa;
|
||||
this.isConcentrationShowed = isConcentrationShowed;
|
||||
this.isIndoor = isIndoor;
|
||||
this.isLcdOn = isLcdOn;
|
||||
this.isNetworkTime = isNetworkTime;
|
||||
this.isTemperatureCelsius = isTemperatureCelsius;
|
||||
this.language = language;
|
||||
this.lcdBrightness = lcdBrightness;
|
||||
this.nodeName = nodeName;
|
||||
this.powerSaving = powerSaving;
|
||||
this.speedUnit = speedUnit;
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
public String getFollowedStation() {
|
||||
return followedStation;
|
||||
}
|
||||
|
||||
public void setFollowedStation(String followedStation) {
|
||||
this.followedStation = followedStation;
|
||||
}
|
||||
|
||||
public boolean isIsAqiUsa() {
|
||||
return isAqiUsa;
|
||||
}
|
||||
|
||||
public void setIsAqiUsa(boolean isAqiUsa) {
|
||||
this.isAqiUsa = isAqiUsa;
|
||||
}
|
||||
|
||||
public boolean isIsConcentrationShowed() {
|
||||
return isConcentrationShowed;
|
||||
}
|
||||
|
||||
public void setIsConcentrationShowed(boolean isConcentrationShowed) {
|
||||
this.isConcentrationShowed = isConcentrationShowed;
|
||||
}
|
||||
|
||||
public boolean isIsIndoor() {
|
||||
return isIndoor;
|
||||
}
|
||||
|
||||
public void setIsIndoor(boolean isIndoor) {
|
||||
this.isIndoor = isIndoor;
|
||||
}
|
||||
|
||||
public boolean isIsLcdOn() {
|
||||
return isLcdOn;
|
||||
}
|
||||
|
||||
public void setIsLcdOn(boolean isLcdOn) {
|
||||
this.isLcdOn = isLcdOn;
|
||||
}
|
||||
|
||||
public boolean isIsNetworkTime() {
|
||||
return isNetworkTime;
|
||||
}
|
||||
|
||||
public void setIsNetworkTime(boolean isNetworkTime) {
|
||||
this.isNetworkTime = isNetworkTime;
|
||||
}
|
||||
|
||||
public boolean isIsTemperatureCelsius() {
|
||||
return isTemperatureCelsius;
|
||||
}
|
||||
|
||||
public void setIsTemperatureCelsius(boolean isTemperatureCelsius) {
|
||||
this.isTemperatureCelsius = isTemperatureCelsius;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public int getLcdBrightness() {
|
||||
return lcdBrightness;
|
||||
}
|
||||
|
||||
public void setLcdBrightness(int lcdBrightness) {
|
||||
this.lcdBrightness = lcdBrightness;
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
public void setNodeName(String nodeName) {
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
public PowerSaving getPowerSaving() {
|
||||
return powerSaving;
|
||||
}
|
||||
|
||||
public void setPowerSaving(PowerSaving powerSaving) {
|
||||
this.powerSaving = powerSaving;
|
||||
}
|
||||
|
||||
public String getSpeedUnit() {
|
||||
return speedUnit;
|
||||
}
|
||||
|
||||
public void setSpeedUnit(String speedUnit) {
|
||||
this.speedUnit = speedUnit;
|
||||
}
|
||||
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airvisualnode.internal.json;
|
||||
|
||||
/**
|
||||
* Status data.
|
||||
*
|
||||
* @author Victor Antonovich - Initial contribution
|
||||
*/
|
||||
public class Status {
|
||||
|
||||
private String appVersion;
|
||||
private int battery;
|
||||
private long datetime;
|
||||
private String model;
|
||||
private String sensorPm25Serial;
|
||||
private int syncTime;
|
||||
private String systemVersion;
|
||||
private int usedMemory;
|
||||
private int wifiStrength;
|
||||
|
||||
public Status(String appVersion, int battery, long datetime, String model, String sensorPm25Serial, int syncTime,
|
||||
String systemVersion, int usedMemory, int wifiStrength) {
|
||||
this.appVersion = appVersion;
|
||||
this.battery = battery;
|
||||
this.datetime = datetime;
|
||||
this.model = model;
|
||||
this.sensorPm25Serial = sensorPm25Serial;
|
||||
this.syncTime = syncTime;
|
||||
this.systemVersion = systemVersion;
|
||||
this.usedMemory = usedMemory;
|
||||
this.wifiStrength = wifiStrength;
|
||||
}
|
||||
|
||||
public String getAppVersion() {
|
||||
return appVersion;
|
||||
}
|
||||
|
||||
public void setAppVersion(String appVersion) {
|
||||
this.appVersion = appVersion;
|
||||
}
|
||||
|
||||
public int getBattery() {
|
||||
return battery;
|
||||
}
|
||||
|
||||
public void setBattery(int battery) {
|
||||
this.battery = battery;
|
||||
}
|
||||
|
||||
public long getDatetime() {
|
||||
return datetime;
|
||||
}
|
||||
|
||||
public void setDatetime(long datetime) {
|
||||
this.datetime = datetime;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getSensorPm25Serial() {
|
||||
return sensorPm25Serial;
|
||||
}
|
||||
|
||||
public void setSensorPm25Serial(String sensorPm25Serial) {
|
||||
this.sensorPm25Serial = sensorPm25Serial;
|
||||
}
|
||||
|
||||
public int getSyncTime() {
|
||||
return syncTime;
|
||||
}
|
||||
|
||||
public void setSyncTime(int syncTime) {
|
||||
this.syncTime = syncTime;
|
||||
}
|
||||
|
||||
public String getSystemVersion() {
|
||||
return systemVersion;
|
||||
}
|
||||
|
||||
public void setSystemVersion(String systemVersion) {
|
||||
this.systemVersion = systemVersion;
|
||||
}
|
||||
|
||||
public int getUsedMemory() {
|
||||
return usedMemory;
|
||||
}
|
||||
|
||||
public void setUsedMemory(int usedMemory) {
|
||||
this.usedMemory = usedMemory;
|
||||
}
|
||||
|
||||
public int getWifiStrength() {
|
||||
return wifiStrength;
|
||||
}
|
||||
|
||||
public void setWifiStrength(int wifiStrength) {
|
||||
this.wifiStrength = wifiStrength;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="airvisualnode" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>AirVisual Node Binding</name>
|
||||
<description>Binding for AirVisual Node air quality monitor</description>
|
||||
<author>Victor Antonovich</author>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airvisualnode"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="avnode">
|
||||
<label>AirVisual Node</label>
|
||||
<description>AirVisual Node air quality monitor</description>
|
||||
|
||||
<!-- Channels -->
|
||||
|
||||
<channels>
|
||||
<channel id="co2" typeId="Co2"/>
|
||||
<channel id="humidity" typeId="Humidity"/>
|
||||
<channel id="aqi" typeId="Aqi"/>
|
||||
<channel id="pm_25" typeId="Pm_25"/>
|
||||
<channel id="temperature" typeId="Temperature"/>
|
||||
<channel id="timestamp" typeId="Timestamp"/>
|
||||
<channel id="used_memory" typeId="Used_memory"/>
|
||||
<channel id="signal-strength" typeId="system.signal-strength"/>
|
||||
<channel id="battery-level" typeId="system.battery-level"/>
|
||||
</channels>
|
||||
|
||||
<!-- Configuration parameters -->
|
||||
|
||||
<config-description>
|
||||
<!-- Required parameters -->
|
||||
<parameter name="address" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Node Network Address</label>
|
||||
<description>Node network address</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text">
|
||||
<label>Node Username</label>
|
||||
<description>Node network username</description>
|
||||
<default>airvisual</default>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>Node Password</label>
|
||||
<description>Node network password</description>
|
||||
</parameter>
|
||||
<!-- Advanced parameters -->
|
||||
<parameter name="share" type="text">
|
||||
<label>Share Name</label>
|
||||
<description>Node network share name</description>
|
||||
<default>airvisual</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" min="30" unit="s">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Node data fetches interval (in seconds)</description>
|
||||
<default>60</default>
|
||||
<unitLabel>s</unitLabel>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Channel types -->
|
||||
|
||||
<channel-type id="Co2">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>CO₂ Level</label>
|
||||
<description>CO₂ level, ppm</description>
|
||||
<category>CarbonDioxide</category>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Humidity, %</description>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Aqi">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>AQI</label>
|
||||
<description>Air Quality Index (US)</description>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Pm_25">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>PM2.5</label>
|
||||
<description>PM2.5 level, µg/m³</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Current temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Timestamp">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Timestamp</label>
|
||||
<description>Status timestamp</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Used_memory" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Used Memory</label>
|
||||
<description>Used memory</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
32
bundles/org.openhab.binding.alarmdecoder/.classpath
Normal file
32
bundles/org.openhab.binding.alarmdecoder/.classpath
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
23
bundles/org.openhab.binding.alarmdecoder/.project
Normal file
23
bundles/org.openhab.binding.alarmdecoder/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.alarmdecoder</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
13
bundles/org.openhab.binding.alarmdecoder/NOTICE
Normal file
13
bundles/org.openhab.binding.alarmdecoder/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
313
bundles/org.openhab.binding.alarmdecoder/README.md
Normal file
313
bundles/org.openhab.binding.alarmdecoder/README.md
Normal file
@ -0,0 +1,313 @@
|
||||
# Alarm Decoder Binding
|
||||
|
||||
The [Alarm Decoder](http://www.alarmdecoder.com) from Nu Tech Software Solutions is a hardware adapter that interfaces with Ademco/Honeywell and DSC alarm panels.
|
||||
It acts essentially like a keypad, reading and writing messages on the serial bus that connects keypads with the main panel.
|
||||
|
||||
There are several versions of the adapter available:
|
||||
|
||||
* *AD2PI* or *AD2PHAT* - A board that plugs into a Raspberry Pi and offers network-based TCP connectivity
|
||||
* *AD2SERIAL* - Attaches to a host via a serial port
|
||||
* *AD2USB* - Attaches to a host via USB
|
||||
|
||||
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:
|
||||
|
||||
* `ipbridge` - Supports TCP connection to the AD.
|
||||
* `serialbridge` - Supports serial/USB connection to the AD.
|
||||
* `keypad` - Reports keypad status and optionally sends keypad messages.
|
||||
* `zone` - Reports status from zone expanders and relay expanders, and also from built-in zones via emulation.
|
||||
* `rfzone` - Reports status from RF zones.
|
||||
* `vzone` - Sends commands to virtual zones.
|
||||
* `lrr` - Reports messages sent from the panel to a Long Range Radio (LRR) or emulated LRR device.
|
||||
|
||||
## Discovery
|
||||
|
||||
Background discovery is currently supported for `zone` and `rfzone` things.
|
||||
If the bridge `discovery` parameter is set to *true*, the first time a status message is seen from each zone or RF zone a corresponding thing will appear in the inbox.
|
||||
Leaving the `discovery` parameter set to *false* during normal operation is recommended, as it will slightly reduce resource consumption by the binding.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The process for wiring the Alarm Decoder into the alarm panel and configuring it is described in the Alarm Decoder Quick Start guide for your model.
|
||||
Before working on the main panel, it is advisable to put the alarm system in test mode, and un-plug the phone connection to it for good measure.
|
||||
Don't forget to plug it back in when you are finished!
|
||||
|
||||
Understanding exactly what expansion boards are connected to the main panel is crucial for a successful setup of the AlarmDecoder and also helpful in interpreting the messages from the alarmdecoder.
|
||||
While many of the expansion devices don't have labels on the outside, inserting a flat screwdriver into the right slot and prying gently will usually uncover a circuit board with numbers on it that can be looked up via web search.
|
||||
|
||||
Although not mentioned in the Quick Start guide, configuring virtual relay boards is absolutely necessary on panels like the Honeywell Vista 20p and similar, or else all of the eight on-board zones will not be visible!
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
Alarm Decoder things can be configured through openHAB's management UI, or manually via configuration files.
|
||||
When first configuring the binding it is probably easiest to configure it via the management UI, even if you plan to use configuration files later.
|
||||
If you enable the *discovery* option on the bridge, as you fault zones (e.g. open doors and windows, trigger motion detectors, etc.) they should appear in the discovery inbox.
|
||||
|
||||
### ipbridge
|
||||
|
||||
The `ipbridge` thing supports a TCP/IP connection to an Alarm Decoder device such as *AD2PI* or *AD2PHAT*.
|
||||
|
||||
* `hostname` (required) The hostname or IP address of the Alarm Decoder device
|
||||
* `tcpPort` (default = 10000) TCP port number for the Alarm Decoder connection
|
||||
* `discovery` (default = false) Enable automatic discovery of zones and RF zones
|
||||
* `reconnect` (1-60, default = 2) The period in minutes that the handler will wait between connection checks and connection attempts
|
||||
* `timeout` (0-60, default = 5) The period in minutes after which the connection will be reset if no valid messages have been received. Set to 0 to disable.
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Bridge alarmdecoder:ipbridge:ad1 [ hostname="cerberus.home", tcpPort=10000, discovery=true ] {
|
||||
Thing ...
|
||||
Thing ...
|
||||
}
|
||||
```
|
||||
|
||||
### serialbridge
|
||||
|
||||
The `serialbridge` thing supports a serial or USB connection to an Alarm Decoder device such as *AD2SERIAL* or *AD2USB*.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `serialPort` (required) The name of the serial port used to connect to the Alarm Decoder device
|
||||
* `bitrate` Speed of the serial connection
|
||||
* `discovery` (default=false) Enable automatic discovery of zones and RF zones
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Bridge alarmdecoder:serialbridge:ad1 [ serialPort="/dev/ttyS1", bitrate=115200, discovery=true ] {
|
||||
Thing ...
|
||||
Thing ...
|
||||
}
|
||||
```
|
||||
|
||||
### keypad
|
||||
|
||||
The `keypad` thing reports keypad status and optionally sends keypad messages.
|
||||
For panels that support multiple keypad addresses, it can be configured with an address mask of one or more keypad(s) for which it will receive messages.
|
||||
When sending messages, it will send from the configured keypad address if only one is configured.
|
||||
If a mask containing multiple addresses or 0 (all) is configured, it will send messages from the Alarm Decoder's configured address.
|
||||
|
||||
Commands sent from the keypad thing are limited to the set of valid keypad command characters supported by the Alarm Decoder (0-9,*,#,<,>).
|
||||
In addition, the characters A-H will be translated to special keys 1-8.
|
||||
Command strings containing invalid characters will be ignored.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `addressMask` (default = 0) String containing the mask in hex of addresses for which the keypad thing will receive messages (0 = all addresses).
|
||||
* `sendCommands` (default = false) Allow keypad commands to be sent to the alarm system from openHAB. Enabling this means the alarm system will be only as secure as your openHAB system.
|
||||
* `sendStar` (default = false) When disarmed/faulted, automatically send the * character to obtain zone fault information.
|
||||
* `commandMapping` (optional) Comma separated list of key/value pairs mapping integers to command strings for `intcommand` channel.
|
||||
|
||||
Address masks
|
||||
|
||||
Each bit in the 4 bytes of the address mask represents a device address, ranging from device 0 to device 31.
|
||||
The first byte (left to right) represents devices 0-7, the second 8-15, the third 16-23, and the fourth 24-31.
|
||||
The mask itself is represented as a string containing a hexadecimal number.
|
||||
For example, a mask of 03000000 would indicate devices 0 and 1 as follows:
|
||||
|
||||
```
|
||||
Mask: 03000000
|
||||
Bytes: 03 00 00 00
|
||||
Bits: 00000011 00000000 00000000 00000000
|
||||
-------- -------- -------- --------
|
||||
Device# 111111 22221111 33222222
|
||||
76543210 54321098 32109876 10987654
|
||||
```
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Thing keypad keypad1 [ addressMask=0, sendCommands=true ]
|
||||
```
|
||||
|
||||
### zone
|
||||
|
||||
The `zone` thing reports status from zone expanders and relay expanders, and also from built-in zones via emulation.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `address` (required) Zone address
|
||||
* `channel` (required) Zone channel
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Thing zone frontdoor [ address=10, channel=1 ]
|
||||
```
|
||||
|
||||
### rfzone
|
||||
|
||||
The `rfzone` thing reports status from wireless zones, such as 5800 series RF devices, if your alarm panel has an RF receiver.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `serial` (required) Serial number of the RF zone
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Thing rfzone motion1 [ serial=0180010 ]
|
||||
```
|
||||
|
||||
### vzone
|
||||
|
||||
The `vzone` thing sends open/close commands a virtual zone.
|
||||
After enabling zone expander emulation on both the alarm panel and the Alarm Decoder device, it can be used to control the state of a virtual zone.
|
||||
The `command` channel is write-only, and accepts either the string "OPEN" or the string "CLOSED".
|
||||
The `state` channel is a switch type channel that reflects the current state of the virtual zone (ON=closed/OFF=open).
|
||||
|
||||
Parameters:
|
||||
|
||||
* `address` (required) Virtual zone number (0-99)
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Thing vzone watersensor [ address=41 ]
|
||||
```
|
||||
|
||||
### lrr
|
||||
|
||||
The `lrr` thing reports messages sent to a Long Range Radio (LRR) or emulated LRR device.
|
||||
These are normally specifically formatted messages as described in the [SIA DC-05-1999.09](http://www.alarmdecoder.com/wiki/index.php/File:SIA-ContactIDCodes_Protocol.pdf) standard for Contact ID reporting.
|
||||
They can also, depending on configuration, be other types of messages as described [here](http://www.alarmdecoder.com/wiki/index.php/LRR_Support).
|
||||
For panels that support multiple partitions, the partition for which a given lrr thing will receive messages can be defined.
|
||||
|
||||
* `partition` (default = 0) Partition for which to receive LRR events (0 = All)
|
||||
|
||||
Thing config file example:
|
||||
|
||||
```
|
||||
Thing lrr lrr [ partition=0 ]
|
||||
```
|
||||
|
||||
## Channels
|
||||
|
||||
The alarmdecoder things expose the following channels:
|
||||
|
||||
**zone**
|
||||
|
||||
| channel | type |RO/RW| description |
|
||||
|--------------|---------|-----|------------------------------|
|
||||
| contact | Contact |RO |Zone contact state |
|
||||
|
||||
**rfzone**
|
||||
|
||||
| channel | type |RO/RW| description |
|
||||
|--------------|---------|-----|------------------------------|
|
||||
| lowbat | Switch | RO |Low battery |
|
||||
| supervision | Switch | RO |Supervision warning |
|
||||
| loop1 | Contact | RO |Loop 1 state |
|
||||
| loop2 | Contact | RO |Loop 2 state |
|
||||
| loop3 | Contact | RO |Loop 3 state |
|
||||
| loop4 | Contact | RO |Loop 4 state |
|
||||
|
||||
**vzone**
|
||||
|
||||
| channel | type |RO/RW| description |
|
||||
|--------------|---------|-----|------------------------------|
|
||||
| command | String | WO |"OPEN" or "CLOSED" command |
|
||||
| state | Switch | RW |Zone state (ON = closed) |
|
||||
|
||||
**keypad**
|
||||
|
||||
| channel | type |RO/RW| description |
|
||||
|--------------|---------|-----|------------------------------|
|
||||
| zone | Number | RO |Zone number for status |
|
||||
| text | String | RO |Keypad message text |
|
||||
| ready | Switch | RO |Panel ready |
|
||||
| armedaway | Switch | RO |Armed/Away Indicator |
|
||||
| armedhome | Switch | RO |Armed/Stay Indicator |
|
||||
| backlight | Switch | RO |Keypad backlight on |
|
||||
| program | Switch | RO |Programming mode |
|
||||
| beeps | Number | RO |Number of beeps for message |
|
||||
| bypassed | Switch | RO |Zone bypassed |
|
||||
| acpower | Switch | RO |Panel on AC power |
|
||||
| chime | Switch | RO |Chime enabled |
|
||||
| alarmoccurred| Switch | RO |Alarm occurred in the past |
|
||||
| alarm | Switch | RO |Alarm is currently sounding |
|
||||
| lowbat | Switch | RO |Low battery warning |
|
||||
| delayoff | Switch | RO |Entry delay off |
|
||||
| fire | Switch | RO |Fire detected |
|
||||
| sysfault | Switch | RO |System fault |
|
||||
| perimeter | Switch | RO |Perimeter only |
|
||||
| command | String | RW |Keypad command |
|
||||
| intcommand | Number | RW |Integer keypad command |
|
||||
|
||||
*Note* - The `intcommand` channel is provided for backward compatibility with the OH1 version of the binding.
|
||||
The integer to command string mappings are provided by the optional keypad `commandMapping` parameter.
|
||||
The default mapping is "0=0,1=1,2=2,3=3,4=4,5=5,6=6,7=7,8=8,9=9,10=*,11=#".
|
||||
|
||||
**lrr**
|
||||
|
||||
| channel | type |RO/RW| description |
|
||||
|--------------|---------|-----|------------------------------|
|
||||
| partition | Number | RO |Partition number (0=system) |
|
||||
| eventdata | Number | RO |CID event data (user or zone) |
|
||||
| cidmessage | String | RO |SIA Contact ID Protocol msg. |
|
||||
| reportcode | String | RO |CID report code |
|
||||
|
||||
## Full Example
|
||||
|
||||
Example ad.things file:
|
||||
|
||||
```
|
||||
Bridge alarmdecoder:ipbridge:ad1 [ hostname="cerberus.home", tcpPort=10000, discovery=true ] {
|
||||
Thing zone frontdoor [ address=10, channel=1 ]
|
||||
Thing zone backdoor [ address=11, channel=1 ]
|
||||
Thing rfzone motion1 [ serial=0180010 ]
|
||||
Thing vzone watersensor [ address=41 ]
|
||||
Thing keypad keypad1 [ addressMask=0, sendCommands=true ]
|
||||
Thing lrr lrr [ partition=0 ]
|
||||
}
|
||||
```
|
||||
|
||||
Example ad.items file:
|
||||
|
||||
```
|
||||
Number KeypadZone "Zone [%d]" {channel="alarmdecoder:keypad:ad1:keypad1:zone"}
|
||||
String KeypadText "Message" {channel="alarmdecoder:keypad:ad1:keypad1:text"}
|
||||
Switch KeypadArmedAway "Armed Away" {channel="alarmdecoder:keypad:ad1:keypad1:armedaway"}
|
||||
Switch KeypadArmedHome "Armed Home" {channel="alarmdecoder:keypad:ad1:keypad1:armedhome"}
|
||||
Switch KeypadAlarm "Alarm" {channel="alarmdecoder:keypad:ad1:keypad1:alarm"}
|
||||
Switch KeypadFire "Fire" {channel="alarmdecoder:keypad:ad1:keypad1:fire"}
|
||||
String KeypadCmd "Command" {channel="alarmdecoder:keypad:ad1:keypad1:command"}
|
||||
|
||||
Contact FrontDoorContact "Front Door Zone" {channel="alarmdecoder:zone:ad1:frontdoor:contact"}
|
||||
|
||||
Switch Motion1Lowbat "Low Battery" {channel="alarmdecoder:rfzone:ad1:motion1:lowbat"}
|
||||
Switch Motion1Supervision "Supervision Warning" {channel="alarmdecoder:rfzone:ad1:motion1:supervision"}
|
||||
Contact Motion1Loop1 "Loop 1" {channel="alarmdecoder:rfzone:ad1:motion1:loop1"}
|
||||
Contact Motion1Loop2 "Loop 2" {channel="alarmdecoder:rfzone:ad1:motion1:loop2"}
|
||||
Contact Motion1Loop3 "Loop 3" {channel="alarmdecoder:rfzone:ad1:motion1:loop3"}
|
||||
Contact Motion1Loop4 "Loop 4" {channel="alarmdecoder:rfzone:ad1:motion1:loop4"}
|
||||
|
||||
String WaterSensorCmd "Virtual Zone Command" {channel="alarmdecoder:vzone:ad1:watersensor:command"}
|
||||
|
||||
Number LrrPartition "Partition Number [%d]" {channel="alarmdecoder:lrr:ad1:lrr:partition"}
|
||||
Number LrrEventData "CID Event Data [%d]" {channel="alarmdecoder:lrr:ad1:lrr:eventdata"}
|
||||
String LrrMessage "CID Message" {channel="alarmdecoder:lrr:ad1:lrr:cidmessage"}
|
||||
String LrrReportCode "CID Report Code" {channel="alarmdecoder:lrr:ad1:lrr:reportcode"}
|
||||
```
|
||||
|
||||
*Note: For brevity, not every possible keypad channel is linked to an item in the above example.*
|
||||
|
||||
## Thing Actions
|
||||
|
||||
The `ipbridge` and `serialbridge` things expose the following action to the automation engine:
|
||||
|
||||
*reboot* - Send command to reboot the Alarm Decoder device. Accepts no parameters.
|
||||
|
||||
## Quirks
|
||||
|
||||
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.
|
17
bundles/org.openhab.binding.alarmdecoder/pom.xml
Normal file
17
bundles/org.openhab.binding.alarmdecoder/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.alarmdecoder</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Alarm Decoder Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.alarmdecoder-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-alarmdecoder" description="alarmdecoder Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-serial</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.alarmdecoder/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AlarmDecoderBindingConstants} class defines common constants, which are
|
||||
* used throughout the binding.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
* @author Bill Forsyth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AlarmDecoderBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "alarmdecoder";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_IPBRIDGE = new ThingTypeUID(BINDING_ID, "ipbridge");
|
||||
public static final ThingTypeUID THING_TYPE_SERIALBRIDGE = new ThingTypeUID(BINDING_ID, "serialbridge");
|
||||
public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone");
|
||||
public static final ThingTypeUID THING_TYPE_RFZONE = new ThingTypeUID(BINDING_ID, "rfzone");
|
||||
public static final ThingTypeUID THING_TYPE_VZONE = new ThingTypeUID(BINDING_ID, "vzone");
|
||||
public static final ThingTypeUID THING_TYPE_KEYPAD = new ThingTypeUID(BINDING_ID, "keypad");
|
||||
public static final ThingTypeUID THING_TYPE_LRR = new ThingTypeUID(BINDING_ID, "lrr");
|
||||
|
||||
public static final Set<ThingTypeUID> DISCOVERABLE_DEVICE_TYPE_UIDS = Collections.unmodifiableSet(Stream
|
||||
.of(THING_TYPE_ZONE, THING_TYPE_RFZONE, THING_TYPE_KEYPAD, THING_TYPE_LRR).collect(Collectors.toSet()));
|
||||
|
||||
// Bridge properties
|
||||
public static final String PROPERTY_SERIALNUM = "serialNumber";
|
||||
public static final String PROPERTY_VERSION = "firmwareVersion";
|
||||
public static final String PROPERTY_CAPABILITIES = "capabilities";
|
||||
|
||||
// Channel IDs for ZoneHandler
|
||||
public static final String PROPERTY_ADDRESS = "address";
|
||||
public static final String PROPERTY_CHANNEL = "channel";
|
||||
public static final String PROPERTY_ID = "id";
|
||||
|
||||
public static final String CHANNEL_CONTACT = "contact";
|
||||
public static final String CHANNEL_STATE = "state";
|
||||
|
||||
// Channel IDs for VZoneHandler
|
||||
public static final String CHANNEL_COMMAND = "command";
|
||||
|
||||
// Channel IDs for RFZoneHandler
|
||||
public static final String PROPERTY_SERIAL = "serial";
|
||||
|
||||
public static final String CHANNEL_RF_LOWBAT = "lowbat";
|
||||
public static final String CHANNEL_RF_SUPERVISION = "supervision";
|
||||
public static final String CHANNEL_RF_LOOP1 = "loop1";
|
||||
public static final String CHANNEL_RF_LOOP2 = "loop2";
|
||||
public static final String CHANNEL_RF_LOOP3 = "loop3";
|
||||
public static final String CHANNEL_RF_LOOP4 = "loop4";
|
||||
|
||||
// Channel IDs for KeypadHandler
|
||||
public static final String CHANNEL_KP_ZONE = "zone";
|
||||
public static final String CHANNEL_KP_TEXT = "text";
|
||||
public static final String CHANNEL_KP_READY = "ready";
|
||||
public static final String CHANNEL_KP_ARMEDAWAY = "armedaway";
|
||||
public static final String CHANNEL_KP_ARMEDHOME = "armedhome";
|
||||
public static final String CHANNEL_KP_BACKLIGHT = "backlight";
|
||||
public static final String CHANNEL_KP_PRORGAM = "program";
|
||||
public static final String CHANNEL_KP_BEEPS = "beeps";
|
||||
public static final String CHANNEL_KP_BYPASSED = "bypassed";
|
||||
public static final String CHANNEL_KP_ACPOWER = "acpower";
|
||||
public static final String CHANNEL_KP_CHIME = "chime";
|
||||
public static final String CHANNEL_KP_ALARMOCCURRED = "alarmoccurred";
|
||||
public static final String CHANNEL_KP_ALARM = "alarm";
|
||||
public static final String CHANNEL_KP_LOWBAT = "lowbat";
|
||||
public static final String CHANNEL_KP_DELAYOFF = "delayoff";
|
||||
public static final String CHANNEL_KP_FIRE = "fire";
|
||||
public static final String CHANNEL_KP_SYSFAULT = "sysfault";
|
||||
public static final String CHANNEL_KP_PERIMETER = "perimeter";
|
||||
public static final String CHANNEL_KP_COMMAND = "command";
|
||||
public static final String CHANNEL_KP_INTCOMMAND = "intcommand";
|
||||
public static final String DEFAULT_MAPPING = "0=0,1=1,2=2,3=3,4=4,5=5,6=6,7=7,8=8,9=9,10=*,11=#";
|
||||
|
||||
// Channel IDs for LRRHandler
|
||||
public static final String CHANNEL_LRR_PARTITION = "partition";
|
||||
public static final String CHANNEL_LRR_EVENTDATA = "eventdata";
|
||||
public static final String CHANNEL_LRR_CIDMESSAGE = "cidmessage";
|
||||
public static final String CHANNEL_LRR_REPORTCODE = "reportcode";
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.ADBridgeHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.ZoneHandler;
|
||||
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.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AlarmDecoderDiscoveryService} handles discovery of devices as they are identified by the bridge handler.
|
||||
* Requests from the framework to startScan() are ignored, since no active scanning is possible.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AlarmDecoderDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AlarmDecoderDiscoveryService.class);
|
||||
|
||||
private ADBridgeHandler bridgeHandler;
|
||||
private final Set<String> discoveredZoneSet = new HashSet<>();
|
||||
private final Set<Integer> discoveredRFZoneSet = new HashSet<>();
|
||||
|
||||
public AlarmDecoderDiscoveryService(ADBridgeHandler bridgeHandler) throws IllegalArgumentException {
|
||||
super(DISCOVERABLE_DEVICE_TYPE_UIDS, 0, false);
|
||||
this.bridgeHandler = bridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
// Ignore start scan requests
|
||||
}
|
||||
|
||||
public void processZone(int address, int channel) {
|
||||
String token = ZoneHandler.zoneID(address, channel);
|
||||
if (!discoveredZoneSet.contains(token)) {
|
||||
notifyDiscoveryOfZone(address, channel, token);
|
||||
discoveredZoneSet.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
public void processRFZone(int serial) {
|
||||
if (!discoveredRFZoneSet.contains(serial)) {
|
||||
notifyDiscoveryOfRFZone(serial);
|
||||
discoveredRFZoneSet.add(serial);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDiscoveryOfZone(int address, int channel, String idString) {
|
||||
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
|
||||
ThingUID uid = new ThingUID(THING_TYPE_ZONE, bridgeUID, idString);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(PROPERTY_ADDRESS, address);
|
||||
properties.put(PROPERTY_CHANNEL, channel);
|
||||
properties.put(PROPERTY_ID, idString);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperties(properties)
|
||||
.withRepresentationProperty(PROPERTY_ID).build();
|
||||
thingDiscovered(result);
|
||||
logger.debug("Discovered Zone {}", uid);
|
||||
}
|
||||
|
||||
private void notifyDiscoveryOfRFZone(Integer serial) {
|
||||
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
|
||||
ThingUID uid = new ThingUID(THING_TYPE_RFZONE, bridgeUID, serial.toString());
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(PROPERTY_SERIAL, serial);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperties(properties)
|
||||
.withRepresentationProperty(PROPERTY_SERIAL).build();
|
||||
thingDiscovered(result);
|
||||
logger.debug("Discovered RF Zone{}", uid);
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.ADBridgeHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.IPBridgeHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.KeypadHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.LRRHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.RFZoneHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.SerialBridgeHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.VZoneHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.ZoneHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
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.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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AlarmDecoderHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.alarmdecoder", service = ThingHandlerFactory.class)
|
||||
public class AlarmDecoderHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_IPBRIDGE, THING_TYPE_SERIALBRIDGE, THING_TYPE_ZONE, THING_TYPE_RFZONE,
|
||||
THING_TYPE_VZONE, THING_TYPE_KEYPAD, THING_TYPE_LRR).collect(Collectors.toSet()));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AlarmDecoderHandlerFactory.class);
|
||||
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
@Activate
|
||||
public AlarmDecoderHandlerFactory(final @Reference SerialPortManager serialPortManager) {
|
||||
// Obtain the serial port manager service using an OSGi reference
|
||||
this.serialPortManager = serialPortManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegMap = new HashMap<>();
|
||||
// Marked as Nullable only to fix incorrect redundant null check complaints from null annotations
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_IPBRIDGE.equals(thingTypeUID)) {
|
||||
IPBridgeHandler bridgeHandler = new IPBridgeHandler((Bridge) thing);
|
||||
registerDiscoveryService(bridgeHandler);
|
||||
return bridgeHandler;
|
||||
} else if (THING_TYPE_SERIALBRIDGE.equals(thingTypeUID)) {
|
||||
SerialBridgeHandler bridgeHandler = new SerialBridgeHandler((Bridge) thing, serialPortManager);
|
||||
registerDiscoveryService(bridgeHandler);
|
||||
return bridgeHandler;
|
||||
} else if (THING_TYPE_ZONE.equals(thingTypeUID)) {
|
||||
return new ZoneHandler(thing);
|
||||
} else if (THING_TYPE_RFZONE.equals(thingTypeUID)) {
|
||||
return new RFZoneHandler(thing);
|
||||
} else if (THING_TYPE_VZONE.equals(thingTypeUID)) {
|
||||
return new VZoneHandler(thing);
|
||||
} else if (THING_TYPE_KEYPAD.equals(thingTypeUID)) {
|
||||
return new KeypadHandler(thing);
|
||||
} else if (THING_TYPE_LRR.equals(thingTypeUID)) {
|
||||
return new LRRHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof ADBridgeHandler) {
|
||||
ServiceRegistration<?> serviceReg = discoveryServiceRegMap.remove(thingHandler.getThing().getUID());
|
||||
if (serviceReg != null) {
|
||||
logger.debug("Unregistering discovery service.");
|
||||
serviceReg.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a discovery service for a bridge handler.
|
||||
*
|
||||
* @param bridgeHandler bridge handler for which to register the discovery service
|
||||
*/
|
||||
private synchronized void registerDiscoveryService(ADBridgeHandler bridgeHandler) {
|
||||
logger.debug("Registering discovery service.");
|
||||
AlarmDecoderDiscoveryService discoveryService = new AlarmDecoderDiscoveryService(bridgeHandler);
|
||||
bridgeHandler.setDiscoveryService(discoveryService);
|
||||
discoveryServiceRegMap.put(bridgeHandler.getThing().getUID(),
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, null));
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.actions;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.alarmdecoder.internal.handler.ADBridgeHandler;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link BridgeActions} class defines thing actions for alarmdecoder bridges.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@ThingActionsScope(name = "alarmdecoder")
|
||||
@NonNullByDefault
|
||||
public class BridgeActions implements ThingActions, IBridgeActions {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(BridgeActions.class);
|
||||
|
||||
private @Nullable ADBridgeHandler bridge;
|
||||
|
||||
public BridgeActions() {
|
||||
logger.trace("Alarm Decoder bridge actions service created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof ADBridgeHandler) {
|
||||
this.bridge = (ADBridgeHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reboot thing action
|
||||
*/
|
||||
@Override
|
||||
@RuleAction(label = "Reboot", description = "Reboot the Alarm Decoder device")
|
||||
public void reboot() {
|
||||
ADBridgeHandler bridge = this.bridge;
|
||||
if (bridge != null) {
|
||||
bridge.sendADCommand(ADCommand.reboot());
|
||||
logger.debug("Sending reboot command.");
|
||||
} else {
|
||||
logger.debug("Request for reboot action, but bridge is undefined.");
|
||||
}
|
||||
}
|
||||
|
||||
// Static method for Rules DSL backward compatibility
|
||||
public static void reboot(@Nullable ThingActions actions) {
|
||||
// if (actions instanceof BridgeActions) {
|
||||
// ((BridgeActions) actions).reboot();
|
||||
// } else {
|
||||
// throw new IllegalArgumentException("Instance is not a BridgeActions class.");
|
||||
// }
|
||||
invokeMethodOf(actions).reboot(); // Remove and uncomment above when core issue #1536 is fixed
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is
|
||||
* resolved.
|
||||
*/
|
||||
private static IBridgeActions invokeMethodOf(@Nullable ThingActions actions) {
|
||||
if (actions == null) {
|
||||
throw new IllegalArgumentException("actions cannot be null");
|
||||
}
|
||||
if (actions.getClass().getName().equals(BridgeActions.class.getName())) {
|
||||
if (actions instanceof IBridgeActions) {
|
||||
return (IBridgeActions) actions;
|
||||
} else {
|
||||
return (IBridgeActions) Proxy.newProxyInstance(IBridgeActions.class.getClassLoader(),
|
||||
new Class[] { IBridgeActions.class }, (Object proxy, Method method, Object[] args) -> {
|
||||
Method m = actions.getClass().getDeclaredMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
return m.invoke(actions, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Actions is not an instance of BridgeActions");
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.actions;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link IBridgeActions} defines the interface for all thing actions supported by the bridges.
|
||||
* This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is
|
||||
* resolved.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface IBridgeActions {
|
||||
|
||||
public void reboot();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link IPBridgeConfig} class contains fields mapping thing configuration parameters for IPBridgeHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IPBridgeConfig {
|
||||
public @Nullable String hostname;
|
||||
public int tcpPort = 10000;
|
||||
public boolean discovery = false;
|
||||
public int reconnect = 2;
|
||||
public int timeout = 5;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.DEFAULT_MAPPING;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link KeypadConfig} class contains fields mapping thing configuration parameters for KeypadHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class KeypadConfig {
|
||||
public String addressMask = "0";
|
||||
public boolean sendCommands = false;
|
||||
public boolean sendStar = false;
|
||||
public String commandMapping = DEFAULT_MAPPING;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LRRConfig} class contains fields mapping thing configuration parameters for LRRHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LRRConfig {
|
||||
public int partition = 0;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link RFZoneConfig} class contains fields mapping thing configuration parameters for RFZoneHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RFZoneConfig {
|
||||
public int serial = -1;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link SerialBridgeConfig} class contains fields mapping thing configuration parameters for SerialBridgeHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialBridgeConfig {
|
||||
public String serialPort = "";
|
||||
public int bitrate = 115200;
|
||||
public boolean discovery = false;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link VZoneConfig} class contains fields mapping thing configuration parameters for VZoneHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class VZoneConfig {
|
||||
public int address = -1;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ZoneConfig} class contains fields mapping thing configuration parameters for ZoneHandler.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZoneConfig {
|
||||
public int address = -1;
|
||||
public int channel = -1;
|
||||
}
|
@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
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;
|
||||
import org.openhab.binding.alarmdecoder.internal.AlarmDecoderDiscoveryService;
|
||||
import org.openhab.binding.alarmdecoder.internal.actions.BridgeActions;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMsgType;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.EXPMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.KeypadMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.LRRMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.RFXMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.VersionMessage;
|
||||
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.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract base class for bridge handlers responsible for communicating with the Nu Tech Alarm Decoder devices.
|
||||
* Based partly on and including code from the original OH1 alarmdecoder binding by Bernd Pfrommer.
|
||||
*
|
||||
* @author Bernd Pfrommer - Initial contribution (OH1 version)
|
||||
* @author Bob Adair - Re-factored into OH2 binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ADBridgeHandler extends BaseBridgeHandler {
|
||||
protected static final Charset AD_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ADBridgeHandler.class);
|
||||
|
||||
protected @Nullable BufferedReader reader = null;
|
||||
protected @Nullable BufferedWriter writer = null;
|
||||
protected @Nullable Thread msgReaderThread = null;
|
||||
private final Object msgReaderThreadLock = new Object();
|
||||
protected @Nullable AlarmDecoderDiscoveryService discoveryService;
|
||||
protected boolean discovery;
|
||||
protected boolean panelReadyReceived = false;
|
||||
protected volatile @Nullable Date lastReceivedTime;
|
||||
protected volatile boolean writeException;
|
||||
|
||||
protected @Nullable ScheduledFuture<?> connectionCheckJob;
|
||||
protected @Nullable ScheduledFuture<?> connectRetryJob;
|
||||
|
||||
public ADBridgeHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.trace("dispose called");
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singletonList(BridgeActions.class);
|
||||
}
|
||||
|
||||
public void setDiscoveryService(AlarmDecoderDiscoveryService discoveryService) {
|
||||
this.discoveryService = discoveryService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Accepts no commands, so do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command to the alarm decoder using a buffered writer. This could block if the buffer is full, so it should
|
||||
* eventually be replaced with a queuing mechanism and a separate writer thread.
|
||||
*
|
||||
* @param command Command string to send including terminator
|
||||
*/
|
||||
public void sendADCommand(ADCommand command) {
|
||||
logger.debug("Sending AD command: {}", command);
|
||||
try {
|
||||
BufferedWriter bw = writer;
|
||||
if (bw != null) {
|
||||
bw.write(command.toString());
|
||||
bw.flush();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.info("Exception while sending command: {}", e.getMessage());
|
||||
writeException = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void connect();
|
||||
|
||||
protected abstract void disconnect();
|
||||
|
||||
protected void scheduleConnectRetry(long waitMinutes) {
|
||||
logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
|
||||
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
protected void startMsgReader() {
|
||||
synchronized (msgReaderThreadLock) {
|
||||
Thread mrt = new Thread(this::readerThread, "AD Reader");
|
||||
mrt.setDaemon(true);
|
||||
mrt.start();
|
||||
msgReaderThread = mrt;
|
||||
}
|
||||
}
|
||||
|
||||
protected void stopMsgReader() {
|
||||
synchronized (msgReaderThreadLock) {
|
||||
Thread mrt = msgReaderThread;
|
||||
if (mrt != null) {
|
||||
logger.trace("Stopping reader thread.");
|
||||
mrt.interrupt();
|
||||
msgReaderThread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method executed by message reader thread
|
||||
*/
|
||||
private void readerThread() {
|
||||
logger.debug("Message reader thread started");
|
||||
String message = null;
|
||||
try {
|
||||
// Send version command to get device to respond with VER message.
|
||||
sendADCommand(ADCommand.getVersion());
|
||||
BufferedReader reader = this.reader;
|
||||
while (!Thread.interrupted() && reader != null && (message = reader.readLine()) != null) {
|
||||
logger.trace("Received msg: {}", message);
|
||||
ADMsgType msgType = ADMsgType.getMsgType(message);
|
||||
if (msgType != ADMsgType.INVALID) {
|
||||
lastReceivedTime = new Date();
|
||||
}
|
||||
try {
|
||||
switch (msgType) {
|
||||
case KPM:
|
||||
parseKeypadMessage(message);
|
||||
break;
|
||||
case REL:
|
||||
case EXP:
|
||||
parseRelayOrExpanderMessage(msgType, message);
|
||||
break;
|
||||
case RFX:
|
||||
parseRFMessage(message);
|
||||
break;
|
||||
case LRR:
|
||||
parseLRRMessage(message);
|
||||
break;
|
||||
case VER:
|
||||
parseVersionMessage(message);
|
||||
break;
|
||||
case INVALID:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (MessageParseException e) {
|
||||
logger.warn("Error {} while parsing message {}. Please report bug.", e.getMessage(), message);
|
||||
}
|
||||
}
|
||||
if (message == null) {
|
||||
logger.info("End of input stream detected");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("I/O error while reading from stream: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Runtime exception in reader thread", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} finally {
|
||||
logger.debug("Message reader thread exiting");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle keypad messages
|
||||
*
|
||||
* @param msg string containing incoming message payload
|
||||
* @throws MessageParseException
|
||||
*/
|
||||
private void parseKeypadMessage(String msg) throws MessageParseException {
|
||||
KeypadMessage kpMsg;
|
||||
|
||||
// Parse the message
|
||||
try {
|
||||
kpMsg = new KeypadMessage(msg);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new MessageParseException(e.getMessage());
|
||||
}
|
||||
|
||||
if (kpMsg.panelClear()) {
|
||||
// the panel is clear, so we can assume that all contacts that we
|
||||
// have not heard from are open
|
||||
notifyChildHandlersPanelReady();
|
||||
}
|
||||
|
||||
notifyChildHandlers(kpMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle relay and expander messages. The REL and EXP messages have identical format.
|
||||
*
|
||||
* @param mt message type of incoming message
|
||||
* @param msg string containing incoming message payload
|
||||
* @throws MessageParseException
|
||||
*/
|
||||
private void parseRelayOrExpanderMessage(ADMsgType mt, String msg) throws MessageParseException {
|
||||
// mt is unused at the moment
|
||||
EXPMessage expMsg;
|
||||
|
||||
try {
|
||||
expMsg = new EXPMessage(msg);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new MessageParseException(e.getMessage());
|
||||
}
|
||||
|
||||
notifyChildHandlers(expMsg);
|
||||
|
||||
AlarmDecoderDiscoveryService ds = discoveryService;
|
||||
if (discovery && ds != null) {
|
||||
ds.processZone(expMsg.address, expMsg.channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle RFX messages.
|
||||
*
|
||||
* @param msg string containing incoming message payload
|
||||
* @throws MessageParseException
|
||||
*/
|
||||
private void parseRFMessage(String msg) throws MessageParseException {
|
||||
RFXMessage rfxMsg;
|
||||
|
||||
try {
|
||||
rfxMsg = new RFXMessage(msg);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new MessageParseException(e.getMessage());
|
||||
}
|
||||
|
||||
notifyChildHandlers(rfxMsg);
|
||||
|
||||
AlarmDecoderDiscoveryService ds = discoveryService;
|
||||
if (discovery && ds != null) {
|
||||
ds.processRFZone(rfxMsg.serial);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle LRR messages.
|
||||
*
|
||||
* @param msg string containing incoming message payload
|
||||
* @throws MessageParseException
|
||||
*/
|
||||
private void parseLRRMessage(String msg) throws MessageParseException {
|
||||
LRRMessage lrrMsg;
|
||||
|
||||
// Parse the message
|
||||
try {
|
||||
lrrMsg = new LRRMessage(msg);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new MessageParseException(e.getMessage());
|
||||
}
|
||||
|
||||
notifyChildHandlers(lrrMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle version (VER) message. This just updates bridge properties.
|
||||
*
|
||||
* @param msg string containing incoming message payload
|
||||
* @throws MessageParseException
|
||||
*/
|
||||
private void parseVersionMessage(String msg) throws MessageParseException {
|
||||
VersionMessage verMsg;
|
||||
|
||||
try {
|
||||
verMsg = new VersionMessage(msg);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new MessageParseException(e.getMessage());
|
||||
}
|
||||
|
||||
logger.trace("Processing version message sn:{} ver:{} cap:{}", verMsg.serial, verMsg.version,
|
||||
verMsg.capabilities);
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put(PROPERTY_SERIALNUM, verMsg.serial);
|
||||
properties.put(PROPERTY_VERSION, verMsg.version);
|
||||
properties.put(PROPERTY_CAPABILITIES, verMsg.capabilities);
|
||||
updateProperties(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify appropriate child thing handlers of an AD message by calling their handleUpdate() methods.
|
||||
*
|
||||
* @param msg message to forward to child handler(s)
|
||||
*/
|
||||
private void notifyChildHandlers(ADMessage msg) {
|
||||
for (Thing thing : getThing().getThings()) {
|
||||
ADThingHandler handler = (ADThingHandler) thing.getHandler();
|
||||
//@formatter:off
|
||||
if (handler != null && ((handler instanceof ZoneHandler && msg instanceof EXPMessage) ||
|
||||
(handler instanceof RFZoneHandler && msg instanceof RFXMessage) ||
|
||||
(handler instanceof KeypadHandler && msg instanceof KeypadMessage) ||
|
||||
(handler instanceof LRRHandler && msg instanceof LRRMessage))) {
|
||||
handler.handleUpdate(msg);
|
||||
}
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify child thing handlers that the alarm panel is in the ready state. Since there is no way to poll, all
|
||||
* contact channels are initialized into the UNDEF state. This method is called when there is reason to assume that
|
||||
* there are no faulted zones, because the alarm panel is in state READY. Zone handlers that have not yet received
|
||||
* updates can then set their contact states to CLOSED. Only executes the first time panel is ready after bridge
|
||||
* connect/reconnect.
|
||||
*/
|
||||
private void notifyChildHandlersPanelReady() {
|
||||
if (!panelReadyReceived) {
|
||||
panelReadyReceived = true;
|
||||
logger.trace("Notifying child handlers that panel is in ready state");
|
||||
|
||||
// Notify child zone handlers by calling notifyPanelReady() for each
|
||||
for (Thing thing : getThing().getThings()) {
|
||||
ADThingHandler handler = (ADThingHandler) thing.getHandler();
|
||||
if (handler != null) {
|
||||
handler.notifyPanelReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown by message parsing code when it encounters a malformed message
|
||||
*/
|
||||
private static class MessageParseException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MessageParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link ADThingHandler} is the abstract base class for all AD thing handlers.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ADThingHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ADThingHandler.class);
|
||||
protected final AtomicBoolean firstUpdateReceived = new AtomicBoolean(false);
|
||||
|
||||
public ADThingHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize device state and set status for handler. Should be called at the end of initialize(). Also called by
|
||||
* bridgeStatusChanged() when bridge status changes from OFFLINE to ONLINE. Calls initChannelState() to initialize
|
||||
* channels if setting status to ONLINE.
|
||||
*/
|
||||
protected void initDeviceState() {
|
||||
logger.trace("Initializing device state");
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
|
||||
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
|
||||
initChannelState();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize channel states if necessary
|
||||
*/
|
||||
public abstract void initChannelState();
|
||||
|
||||
/**
|
||||
* Notify handler that panel is in ready state so that any un-updated contact channels can be set to default
|
||||
* (closed).
|
||||
*/
|
||||
public abstract void notifyPanelReady();
|
||||
|
||||
/**
|
||||
* Notify handler of a message from the AD via the bridge
|
||||
*
|
||||
* @param msg The ADMessage to handle
|
||||
*/
|
||||
public abstract void handleUpdate(ADMessage msg);
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
ThingStatus bridgeStatus = bridgeStatusInfo.getStatus();
|
||||
logger.debug("Bridge status changed to {} for AD handler", bridgeStatus);
|
||||
|
||||
if (bridgeStatus == ThingStatus.ONLINE
|
||||
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
|
||||
initDeviceState();
|
||||
|
||||
} else if (bridgeStatus == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command via the bridge
|
||||
*
|
||||
* @param command command to send
|
||||
*/
|
||||
protected void sendCommand(ADCommand command) {
|
||||
Bridge bridge = getBridge();
|
||||
ADBridgeHandler bridgeHandler = bridge == null ? null : (ADBridgeHandler) bridge.getHandler();
|
||||
|
||||
if (bridgeHandler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
|
||||
} else {
|
||||
bridgeHandler.sendADCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Date;
|
||||
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.alarmdecoder.internal.config.IPBridgeConfig;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handler responsible for communicating via TCP with the Nu Tech Alarm Decoder device.
|
||||
* Based on and including code from the original OH1 alarmdecoder binding.
|
||||
*
|
||||
* @author Bernd Pfrommer - Initial contribution (OH1 version)
|
||||
* @author Bob Adair - Re-factored into OH2 binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IPBridgeHandler extends ADBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(IPBridgeHandler.class);
|
||||
|
||||
private IPBridgeConfig config = new IPBridgeConfig();
|
||||
|
||||
private @Nullable Socket socket = null;
|
||||
|
||||
public IPBridgeHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing IP bridge handler");
|
||||
config = getConfigAs(IPBridgeConfig.class);
|
||||
discovery = config.discovery;
|
||||
|
||||
if (config.hostname == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "hostname not configured");
|
||||
return;
|
||||
}
|
||||
if (config.tcpPort <= 0 || config.tcpPort > 65535) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "invalid port number configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// set the thing status to UNKNOWN temporarily and let the background connect task decide the real status.
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
scheduler.submit(this::connect); // start the async connect task
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void connect() {
|
||||
disconnect(); // make sure we are disconnected
|
||||
writeException = false;
|
||||
try {
|
||||
Socket socket = new Socket(config.hostname, config.tcpPort);
|
||||
this.socket = socket;
|
||||
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), AD_CHARSET));
|
||||
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), AD_CHARSET));
|
||||
logger.debug("connected to {}:{}", config.hostname, config.tcpPort);
|
||||
panelReadyReceived = false;
|
||||
startMsgReader();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
// Start connection check job
|
||||
logger.debug("Scheduling connection check job with interval {} minutes.", config.reconnect);
|
||||
lastReceivedTime = new Date();
|
||||
connectionCheckJob = scheduler.scheduleWithFixedDelay(this::connectionCheck, config.reconnect,
|
||||
config.reconnect, TimeUnit.MINUTES);
|
||||
} catch (UnknownHostException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "unknown host");
|
||||
disconnect();
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
disconnect();
|
||||
scheduleConnectRetry(config.reconnect); // Possibly a retryable error. Try again later.
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized void connectionCheck() {
|
||||
logger.trace("Connection check job running");
|
||||
|
||||
Thread mrThread = msgReaderThread;
|
||||
if (mrThread != null && !mrThread.isAlive()) {
|
||||
logger.debug("Reader thread has exited abnormally. Restarting.");
|
||||
scheduler.submit(this::connect);
|
||||
} else if (writeException) {
|
||||
logger.debug("Write exception encountered. Resetting connection.");
|
||||
scheduler.submit(this::connect);
|
||||
} else {
|
||||
Date now = new Date();
|
||||
Date last = lastReceivedTime;
|
||||
if (last != null && config.timeout > 0
|
||||
&& ((last.getTime() + (config.timeout * 60 * 1000)) < now.getTime())) {
|
||||
logger.warn("Last valid message received at {}. Resetting connection.", last);
|
||||
scheduler.submit(this::connect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void disconnect() {
|
||||
logger.trace("Disconnecting");
|
||||
// stop scheduled connection check and retry jobs
|
||||
ScheduledFuture<?> crJob = connectRetryJob;
|
||||
if (crJob != null) {
|
||||
// use cancel(false) so we don't kill ourselves when connect retry job calls disconnect()
|
||||
crJob.cancel(false);
|
||||
connectRetryJob = null;
|
||||
}
|
||||
ScheduledFuture<?> ccJob = connectionCheckJob;
|
||||
if (ccJob != null) {
|
||||
// use cancel(false) so we don't kill ourselves when reconnect job calls disconnect()
|
||||
ccJob.cancel(false);
|
||||
connectionCheckJob = null;
|
||||
}
|
||||
|
||||
// Must close the socket first so the message reader thread will exit properly.
|
||||
// The BufferedReader.readLine() call used in readerThread() is not interruptable.
|
||||
Socket s = socket;
|
||||
if (s != null) {
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("error closing socket: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
socket = null;
|
||||
|
||||
stopMsgReader();
|
||||
|
||||
try {
|
||||
BufferedWriter bw = writer;
|
||||
if (bw != null) {
|
||||
bw.close();
|
||||
}
|
||||
BufferedReader br = reader;
|
||||
if (br != null) {
|
||||
br.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("error closing reader/writer: {}", e.getMessage());
|
||||
}
|
||||
writer = null;
|
||||
reader = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
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.alarmdecoder.internal.config.KeypadConfig;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADAddress;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.IntCommandMap;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.KeypadMessage;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link KeypadHandler} is responsible for handling keypad messages.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
* @author Bill Forsyth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class KeypadHandler extends ADThingHandler {
|
||||
|
||||
private static final Pattern VALID_COMMAND_PATTERN = Pattern.compile(ADCommand.KEYPAD_COMMAND_REGEX);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(KeypadHandler.class);
|
||||
|
||||
private KeypadConfig config = new KeypadConfig();
|
||||
private boolean singleAddress;
|
||||
private int sendingAddress;
|
||||
private @Nullable IntCommandMap intCommandMap;
|
||||
private @Nullable KeypadMessage previousMessage;
|
||||
private long addressMaskLong = 0;
|
||||
|
||||
public KeypadHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(KeypadConfig.class);
|
||||
|
||||
try {
|
||||
addressMaskLong = Long.parseLong(config.addressMask, 16);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Number format exception parsing addressMask parameter: {}", e.getMessage());
|
||||
addressMaskLong = -1;
|
||||
}
|
||||
|
||||
if (addressMaskLong < 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid addressMask setting");
|
||||
return;
|
||||
}
|
||||
// If 1 and only 1 device is set in the addressMask parameter, use that device number as the sending address
|
||||
singleAddress = ADAddress.singleAddress(addressMaskLong);
|
||||
if (singleAddress) {
|
||||
ADAddress device = ADAddress.getDevice(addressMaskLong);
|
||||
if (device != null) {
|
||||
sendingAddress = device.deviceNum();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
intCommandMap = new IntCommandMap(config.commandMapping);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid commmandMapping parameter supplied. Error: {}.", e.getMessage());
|
||||
intCommandMap = null;
|
||||
}
|
||||
|
||||
logger.debug("Keypad handler initializing for address mask {}", config.addressMask);
|
||||
|
||||
initDeviceState();
|
||||
|
||||
logger.trace("Keypad handler finished initializing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initChannelState() {
|
||||
previousMessage = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPanelReady() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
IntCommandMap intCommandMap = this.intCommandMap;
|
||||
|
||||
if (channelUID.getId().equals(CHANNEL_KP_COMMAND)) {
|
||||
if (command instanceof StringType) {
|
||||
String cmd = ((StringType) command).toString();
|
||||
handleKeypadCommand(cmd);
|
||||
}
|
||||
} else if (channelUID.getId().equals(CHANNEL_KP_INTCOMMAND)) {
|
||||
if (command instanceof Number) {
|
||||
int icmd = ((Number) command).intValue();
|
||||
if (intCommandMap != null) {
|
||||
String cmd = intCommandMap.getCommand(icmd);
|
||||
if (cmd != null) {
|
||||
handleKeypadCommand(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleKeypadCommand(String command) {
|
||||
String cmd = command;
|
||||
if (cmd.length() > 0) {
|
||||
if (!config.sendCommands) {
|
||||
logger.info("Sending keypad commands is disabled. Enable using the sendCommands keypad parameter.");
|
||||
return;
|
||||
}
|
||||
|
||||
// check that received command is valid
|
||||
Matcher matcher = VALID_COMMAND_PATTERN.matcher(cmd);
|
||||
if (!matcher.matches()) {
|
||||
logger.info("Invalid characters in command. Ignoring command: {}", cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace A-H in command string with special key strings
|
||||
cmd = cmd.replace("A", ADCommand.SPECIAL_KEY_1);
|
||||
cmd = cmd.replace("B", ADCommand.SPECIAL_KEY_2);
|
||||
cmd = cmd.replace("C", ADCommand.SPECIAL_KEY_3);
|
||||
cmd = cmd.replace("D", ADCommand.SPECIAL_KEY_4);
|
||||
cmd = cmd.replace("E", ADCommand.SPECIAL_KEY_5);
|
||||
cmd = cmd.replace("F", ADCommand.SPECIAL_KEY_6);
|
||||
cmd = cmd.replace("G", ADCommand.SPECIAL_KEY_7);
|
||||
cmd = cmd.replace("H", ADCommand.SPECIAL_KEY_8);
|
||||
|
||||
if (singleAddress) {
|
||||
sendCommand(ADCommand.addressedMessage(sendingAddress, cmd)); // Send from keypad address
|
||||
} else {
|
||||
sendCommand(new ADCommand(cmd)); // Send from AD address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(ADMessage msg) {
|
||||
// This will ignore a received message unless it is a KeypadMessage and either this handler's address mask is 0
|
||||
// (all), the message's address mask is 0 (all), or any bits in this handler's address mask match bits set in
|
||||
// the message's address mask.
|
||||
if (!(msg instanceof KeypadMessage)) {
|
||||
return;
|
||||
}
|
||||
KeypadMessage kpMsg = (KeypadMessage) msg;
|
||||
|
||||
long msgAddressMask = kpMsg.getLongAddressMask();
|
||||
|
||||
if (!(((addressMaskLong & msgAddressMask) != 0) || addressMaskLong == 0 || msgAddressMask == 0)) {
|
||||
return;
|
||||
}
|
||||
logger.trace("Keypad handler for address mask {} received update: {}", config.addressMask, kpMsg);
|
||||
|
||||
if (kpMsg.equals(previousMessage)) {
|
||||
return; // ignore repeated messages
|
||||
}
|
||||
|
||||
if (config.sendStar) {
|
||||
if (kpMsg.alphaMessage.contains("Hit * for faults") || kpMsg.alphaMessage.contains("Press * to show faults")
|
||||
|| kpMsg.alphaMessage.contains("Press * Key")
|
||||
|| kpMsg.alphaMessage.contains("Press * to show faults")) {
|
||||
logger.debug("Sending * command to show faults.");
|
||||
if (singleAddress) {
|
||||
sendCommand(ADCommand.addressedMessage(sendingAddress, "*")); // Send from keypad address
|
||||
} else {
|
||||
sendCommand(new ADCommand("*")); // send from AD address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateState(CHANNEL_KP_ZONE, new DecimalType(kpMsg.getZone()));
|
||||
updateState(CHANNEL_KP_TEXT, new StringType(kpMsg.alphaMessage));
|
||||
|
||||
updateState(CHANNEL_KP_READY, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_READY)));
|
||||
updateState(CHANNEL_KP_ARMEDAWAY, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_ARMEDAWAY)));
|
||||
updateState(CHANNEL_KP_ARMEDHOME, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_ARMEDHOME)));
|
||||
updateState(CHANNEL_KP_BACKLIGHT, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_BACKLIGHT)));
|
||||
updateState(CHANNEL_KP_PRORGAM, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_PRORGAM)));
|
||||
|
||||
updateState(CHANNEL_KP_BEEPS, new DecimalType(kpMsg.nbeeps));
|
||||
|
||||
updateState(CHANNEL_KP_BYPASSED, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_BYPASSED)));
|
||||
updateState(CHANNEL_KP_ACPOWER, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_ACPOWER)));
|
||||
updateState(CHANNEL_KP_CHIME, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_CHIME)));
|
||||
updateState(CHANNEL_KP_ALARMOCCURRED, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_ALARMOCCURRED)));
|
||||
updateState(CHANNEL_KP_ALARM, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_ALARM)));
|
||||
updateState(CHANNEL_KP_LOWBAT, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_LOWBAT)));
|
||||
updateState(CHANNEL_KP_DELAYOFF, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_DELAYOFF)));
|
||||
updateState(CHANNEL_KP_FIRE, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_FIRE)));
|
||||
updateState(CHANNEL_KP_SYSFAULT, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_SYSFAULT)));
|
||||
updateState(CHANNEL_KP_PERIMETER, OnOffType.from(kpMsg.getStatus(KeypadMessage.BIT_PERIMETER)));
|
||||
|
||||
previousMessage = kpMsg;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.config.LRRConfig;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.LRRMessage;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link LRRHandler} is responsible for handling long range radio (LRR) messages.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
* @author Bill Forsyth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LRRHandler extends ADThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LRRHandler.class);
|
||||
|
||||
private LRRConfig config = new LRRConfig();
|
||||
|
||||
public LRRHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(LRRConfig.class);
|
||||
|
||||
if (config.partition < 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||
return;
|
||||
}
|
||||
logger.debug("LRR handler initializing for partition {}", config.partition);
|
||||
|
||||
initDeviceState();
|
||||
|
||||
logger.trace("LRR handler finished initializing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initChannelState() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPanelReady() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// All channels are read-only, so ignore all commands.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(ADMessage msg) {
|
||||
if (!(msg instanceof LRRMessage)) {
|
||||
return;
|
||||
}
|
||||
LRRMessage lrrMsg = (LRRMessage) msg;
|
||||
|
||||
if (config.partition == lrrMsg.partition || config.partition == 0 || lrrMsg.partition == 0) {
|
||||
logger.trace("LRR handler for partition {} received update: {}", config.partition, msg);
|
||||
updateState(CHANNEL_LRR_PARTITION, new DecimalType(lrrMsg.partition));
|
||||
updateState(CHANNEL_LRR_EVENTDATA, new DecimalType(lrrMsg.eventData));
|
||||
updateState(CHANNEL_LRR_CIDMESSAGE, new StringType(lrrMsg.cidMessage));
|
||||
updateState(CHANNEL_LRR_REPORTCODE, new StringType(lrrMsg.reportCode));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.config.RFZoneConfig;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.RFXMessage;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
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.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link RFZoneHandler} is responsible for handling wired zones (i.e. RFX messages).
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
* @author Bill Forsyth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RFZoneHandler extends ADThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RFZoneHandler.class);
|
||||
|
||||
private RFZoneConfig config = new RFZoneConfig();
|
||||
|
||||
public RFZoneHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(RFZoneConfig.class);
|
||||
|
||||
if (config.serial < 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid serial setting");
|
||||
return;
|
||||
}
|
||||
logger.debug("RF Zone handler initializing for serial {}", config.serial);
|
||||
|
||||
initDeviceState();
|
||||
|
||||
logger.trace("RF Zone handler finished initializing");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set contact channel states to "UNDEF" at init time. The real states will be set either when the first message
|
||||
* arrives for the zone, or they will be set to "CLOSED" the first time the panel goes into the "READY" state.
|
||||
*/
|
||||
@Override
|
||||
public void initChannelState() {
|
||||
UnDefType state = UnDefType.UNDEF;
|
||||
updateState(CHANNEL_RF_LOWBAT, state);
|
||||
updateState(CHANNEL_RF_SUPERVISION, state);
|
||||
updateState(CHANNEL_RF_LOOP1, state);
|
||||
updateState(CHANNEL_RF_LOOP2, state);
|
||||
updateState(CHANNEL_RF_LOOP3, state);
|
||||
updateState(CHANNEL_RF_LOOP4, state);
|
||||
firstUpdateReceived.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPanelReady() {
|
||||
logger.trace("RF Zone handler for {} received panel ready notification.", config.serial);
|
||||
if (firstUpdateReceived.compareAndSet(false, true)) {
|
||||
updateState(CHANNEL_RF_LOOP1, OpenClosedType.CLOSED);
|
||||
updateState(CHANNEL_RF_LOOP2, OpenClosedType.CLOSED);
|
||||
updateState(CHANNEL_RF_LOOP3, OpenClosedType.CLOSED);
|
||||
updateState(CHANNEL_RF_LOOP4, OpenClosedType.CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Does not accept any commands
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(ADMessage msg) {
|
||||
if (!(msg instanceof RFXMessage)) {
|
||||
return;
|
||||
}
|
||||
RFXMessage rfxMsg = (RFXMessage) msg;
|
||||
|
||||
if (config.serial == rfxMsg.serial) {
|
||||
logger.trace("RF Zone handler for serial {} received update: {}", config.serial, rfxMsg.data);
|
||||
firstUpdateReceived.set(true);
|
||||
|
||||
updateState(CHANNEL_RF_LOWBAT, (rfxMsg.data & RFXMessage.BIT_LOWBAT) == 0 ? OnOffType.OFF : OnOffType.ON);
|
||||
updateState(CHANNEL_RF_SUPERVISION,
|
||||
(rfxMsg.data & RFXMessage.BIT_SUPER) == 0 ? OnOffType.OFF : OnOffType.ON);
|
||||
|
||||
updateState(CHANNEL_RF_LOOP1,
|
||||
(rfxMsg.data & RFXMessage.BIT_LOOP1) == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
|
||||
updateState(CHANNEL_RF_LOOP2,
|
||||
(rfxMsg.data & RFXMessage.BIT_LOOP2) == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
|
||||
updateState(CHANNEL_RF_LOOP3,
|
||||
(rfxMsg.data & RFXMessage.BIT_LOOP3) == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
|
||||
updateState(CHANNEL_RF_LOOP4,
|
||||
(rfxMsg.data & RFXMessage.BIT_LOOP4) == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.alarmdecoder.internal.config.SerialBridgeConfig;
|
||||
import org.openhab.core.io.transport.serial.PortInUseException;
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
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.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handler responsible for communicating via a serial port with the Nu Tech Alarm Decoder device.
|
||||
* Based on code from the original OH1 alarmdecoder binding. Some OHC serial transport code taken from the Zigbee
|
||||
* binding.
|
||||
*
|
||||
* @author Bernd Pfrommer - Initial contribution (OH1 version)
|
||||
* @author Bob Adair - Re-factored into OH2 binding and rewrote to use OHC serial transport.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialBridgeHandler extends ADBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SerialBridgeHandler.class);
|
||||
|
||||
private SerialBridgeConfig config = new SerialBridgeConfig();
|
||||
private final SerialPortManager serialPortManager;
|
||||
private @NonNullByDefault({}) SerialPortIdentifier portIdentifier;
|
||||
private @Nullable SerialPort serialPort;
|
||||
private int serialPortSpeed = 115200;
|
||||
|
||||
public SerialBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
|
||||
super(bridge);
|
||||
this.serialPortManager = serialPortManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing serial bridge handler");
|
||||
config = getConfigAs(SerialBridgeConfig.class);
|
||||
discovery = config.discovery;
|
||||
|
||||
if (config.serialPort.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "no serial port configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.bitrate > 0) {
|
||||
serialPortSpeed = config.bitrate;
|
||||
}
|
||||
|
||||
portIdentifier = serialPortManager.getIdentifier(config.serialPort);
|
||||
if (portIdentifier == null) {
|
||||
logger.debug("Serial Error: Port {} does not exist.", config.serialPort);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Configured serial port does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
logger.trace("Finished initializing serial bridge handler");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void connect() {
|
||||
disconnect(); // make sure we are disconnected
|
||||
try {
|
||||
SerialPort serialPort = portIdentifier.open("org.openhab.binding.alarmdecoder", 100);
|
||||
serialPort.setSerialPortParams(serialPortSpeed, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
|
||||
SerialPort.PARITY_NONE);
|
||||
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
|
||||
// Note: The V1 code called disableReceiveFraming() and disableReceiveThreshold() here
|
||||
|
||||
this.serialPort = serialPort;
|
||||
reader = new BufferedReader(new InputStreamReader(serialPort.getInputStream(), AD_CHARSET));
|
||||
writer = new BufferedWriter(new OutputStreamWriter(serialPort.getOutputStream(), AD_CHARSET));
|
||||
|
||||
logger.debug("connected to serial port: {}", config.serialPort);
|
||||
panelReadyReceived = false;
|
||||
startMsgReader();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (PortInUseException e) {
|
||||
logger.debug("Cannot open serial port: {}, it is already in use", config.serialPort);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Serial port already in use");
|
||||
} catch (UnsupportedCommOperationException | IOException | IllegalStateException e) {
|
||||
logger.debug("Error connecting to serial port: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void disconnect() {
|
||||
logger.trace("Disconnecting");
|
||||
SerialPort sp = serialPort;
|
||||
if (sp != null) {
|
||||
logger.trace("Closing serial port");
|
||||
sp.close();
|
||||
serialPort = null;
|
||||
}
|
||||
|
||||
stopMsgReader();
|
||||
|
||||
BufferedReader br = reader;
|
||||
if (br != null) {
|
||||
logger.trace("Closing reader");
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException e) {
|
||||
logger.info("IO Exception closing reader: {}", e.getMessage());
|
||||
} finally {
|
||||
reader = null;
|
||||
}
|
||||
}
|
||||
|
||||
BufferedWriter bw = writer;
|
||||
if (bw != null) {
|
||||
logger.trace("Closing writer");
|
||||
try {
|
||||
bw.close();
|
||||
} catch (IOException e) {
|
||||
logger.info("IO Exception closing writer: {}", e.getMessage());
|
||||
} finally {
|
||||
writer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.config.VZoneConfig;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link VZoneHandler} is responsible for sending state commands to virtual zones.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class VZoneHandler extends ADThingHandler {
|
||||
|
||||
public static final String CMD_OPEN = "OPEN";
|
||||
public static final String CMD_CLOSED = "CLOSED";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(VZoneHandler.class);
|
||||
|
||||
private VZoneConfig config = new VZoneConfig();
|
||||
|
||||
public VZoneHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(VZoneConfig.class);
|
||||
|
||||
if (config.address < 0 || config.address > 99) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid address setting");
|
||||
return;
|
||||
}
|
||||
logger.debug("Virtual zone handler initializing for address {}", config.address);
|
||||
initDeviceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initChannelState() {
|
||||
UnDefType state = UnDefType.UNDEF;
|
||||
updateState(CHANNEL_STATE, state);
|
||||
firstUpdateReceived.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPanelReady() {
|
||||
logger.trace("Virtual zone handler for {} received panel ready notification.", config.address);
|
||||
if (firstUpdateReceived.compareAndSet(false, true)) {
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
|
||||
if (command instanceof StringType) {
|
||||
String cmd = ((StringType) command).toString();
|
||||
if (CMD_OPEN.equalsIgnoreCase(cmd)) {
|
||||
sendCommand(ADCommand.setZone(config.address, ADCommand.ZONE_OPEN));
|
||||
setChannelState(OnOffType.OFF);
|
||||
} else if (CMD_CLOSED.equalsIgnoreCase(cmd)) {
|
||||
sendCommand(ADCommand.setZone(config.address, ADCommand.ZONE_CLOSED));
|
||||
setChannelState(OnOffType.ON);
|
||||
} else {
|
||||
logger.debug("Virtual zone handler {} received invalid command: {}", config.address, cmd);
|
||||
}
|
||||
}
|
||||
} else if (channelUID.getId().equals(CHANNEL_STATE)) {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.OFF) {
|
||||
sendCommand(ADCommand.setZone(config.address, ADCommand.ZONE_OPEN));
|
||||
setChannelState(OnOffType.OFF);
|
||||
} else if (command == OnOffType.ON) {
|
||||
sendCommand(ADCommand.setZone(config.address, ADCommand.ZONE_CLOSED));
|
||||
setChannelState(OnOffType.ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setChannelState(OnOffType state) {
|
||||
updateState(CHANNEL_STATE, state);
|
||||
firstUpdateReceived.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(ADMessage msg) {
|
||||
// There can be no update requests
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.handler;
|
||||
|
||||
import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.alarmdecoder.internal.config.ZoneConfig;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
|
||||
import org.openhab.binding.alarmdecoder.internal.protocol.EXPMessage;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
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.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ZoneHandler} is responsible for handling wired zones (i.e. REL & EXP messages).
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
* @author Bill Forsyth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZoneHandler extends ADThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ZoneHandler.class);
|
||||
|
||||
private ZoneConfig config = new ZoneConfig();
|
||||
|
||||
public ZoneHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/** Construct zone id from address and channel */
|
||||
public static final String zoneID(int address, int channel) {
|
||||
return String.format("%d-%d", address, channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(ZoneConfig.class);
|
||||
|
||||
if (config.address < 0 || config.channel < 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid address/channel setting");
|
||||
return;
|
||||
}
|
||||
logger.debug("Zone handler initializing for address {} channel {}", config.address, config.channel);
|
||||
|
||||
String id = zoneID(config.address, config.channel);
|
||||
updateProperty(PROPERTY_ID, id); // set representation property used by discovery
|
||||
|
||||
initDeviceState();
|
||||
logger.trace("Zone handler finished initializing");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set contact channel state to "UNDEF" at init time. The real state will be set either when the first message
|
||||
* arrives for the zone, or it should be set to "CLOSED" the first time the panel goes into the "READY" state.
|
||||
*/
|
||||
@Override
|
||||
public void initChannelState() {
|
||||
UnDefType state = UnDefType.UNDEF;
|
||||
updateState(CHANNEL_CONTACT, state);
|
||||
firstUpdateReceived.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPanelReady() {
|
||||
logger.trace("Zone handler for {},{} received panel ready notification.", config.address, config.channel);
|
||||
if (firstUpdateReceived.compareAndSet(false, true)) {
|
||||
updateState(CHANNEL_CONTACT, OpenClosedType.CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// All channels are read-only, so ignore all commands.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(ADMessage msg) {
|
||||
if (!(msg instanceof EXPMessage)) {
|
||||
return;
|
||||
}
|
||||
EXPMessage expMsg = (EXPMessage) msg;
|
||||
|
||||
if (config.address == expMsg.address && config.channel == expMsg.channel) {
|
||||
logger.trace("Zone handler for {},{} received update: {}", config.address, config.channel, expMsg.data);
|
||||
|
||||
firstUpdateReceived.set(true);
|
||||
OpenClosedType state = (expMsg.data == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
|
||||
updateState(CHANNEL_CONTACT, state);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.protocol;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Defines keypad device addresses used in an AD keypad address mask.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ADAddress {
|
||||
KEYPAD0(0x01000000, 0),
|
||||
KEYPAD1(0x02000000, 1),
|
||||
KEYPAD2(0x04000000, 2),
|
||||
KEYPAD3(0x08000000, 3),
|
||||
KEYPAD4(0x10000000, 4),
|
||||
KEYPAD5(0x20000000, 5),
|
||||
KEYPAD6(0x40000000, 6),
|
||||
KEYPAD7(0x80000000, 7),
|
||||
|
||||
KEYPAD8(0x00010000, 8),
|
||||
KEYPAD9(0x00020000, 9),
|
||||
KEYPAD10(0x00040000, 10),
|
||||
KEYPAD11(0x00080000, 11),
|
||||
KEYPAD12(0x00100000, 12),
|
||||
KEYPAD13(0x00200000, 13),
|
||||
KEYPAD14(0x00400000, 14),
|
||||
KEYPAD15(0x00800000, 15),
|
||||
|
||||
KEYPAD16(0x00000100, 16),
|
||||
KEYPAD17(0x00000200, 17),
|
||||
KEYPAD18(0x00000400, 18),
|
||||
KEYPAD19(0x00000800, 19),
|
||||
KEYPAD20(0x00001000, 20),
|
||||
KEYPAD21(0x00002000, 21),
|
||||
KEYPAD22(0x00004000, 22),
|
||||
KEYPAD23(0x00008000, 23),
|
||||
|
||||
KEYPAD24(0x00000001, 24),
|
||||
KEYPAD25(0x00000002, 25),
|
||||
KEYPAD26(0x00000004, 26),
|
||||
KEYPAD27(0x00000008, 27),
|
||||
KEYPAD28(0x00000010, 28),
|
||||
KEYPAD29(0x00000020, 29),
|
||||
KEYPAD30(0x00000040, 30),
|
||||
KEYPAD31(0x00000080, 31);
|
||||
|
||||
private final long mask;
|
||||
private final int device;
|
||||
|
||||
ADAddress(long mask, int device) {
|
||||
this.mask = mask;
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
/** Returns the device bit mask **/
|
||||
public long mask() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
/** Returns the device number (0-31) **/
|
||||
public int deviceNum() {
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first device address found in addressMask or null if none are found
|
||||
*
|
||||
* @param addressMask
|
||||
*/
|
||||
public static @Nullable ADAddress getDevice(long addressMask) {
|
||||
for (ADAddress address : ADAddress.values()) {
|
||||
if ((address.mask() & addressMask) != 0) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Collection of the device addresses found in addressMask.
|
||||
* Returns an empty collection if none are found.
|
||||
*
|
||||
* @param addressMask
|
||||
*/
|
||||
public static Collection<ADAddress> getDevices(long addressMask) {
|
||||
Collection<ADAddress> addressCollection = new ArrayList<>();
|
||||
for (ADAddress address : ADAddress.values()) {
|
||||
if ((address.mask() & addressMask) != 0) {
|
||||
addressCollection.add(address);
|
||||
}
|
||||
}
|
||||
return addressCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if 1 and only 1 address bit is set in addressMask
|
||||
*/
|
||||
public static boolean singleAddress(long addressMask) {
|
||||
return (Long.bitCount(addressMask) == 1);
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.protocol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link ADCCommand} class represents an alarm decoder command, and contains the static methods and definitions
|
||||
* used to construct one. Not all supported AD commands are necessarily used by the current binding.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class ADCommand {
|
||||
|
||||
public static final String SPECIAL_KEY_1 = "\u0001\u0001\u0001";
|
||||
public static final String SPECIAL_KEY_2 = "\u0002\u0002\u0002";
|
||||
public static final String SPECIAL_KEY_3 = "\u0003\u0003\u0003";
|
||||
public static final String SPECIAL_KEY_4 = "\u0004\u0004\u0004";
|
||||
public static final String SPECIAL_KEY_5 = "\u0005\u0005\u0005";
|
||||
public static final String SPECIAL_KEY_6 = "\u0006\u0006\u0006";
|
||||
public static final String SPECIAL_KEY_7 = "\u0007\u0007\u0007";
|
||||
public static final String SPECIAL_KEY_8 = "\u0008\u0008\u0008";
|
||||
|
||||
public static final int ZONE_OPEN = 1;
|
||||
public static final int ZONE_CLOSED = 0;
|
||||
|
||||
// public static final String KEYPAD_COMMAND_CHARACTERS = "0123456789*#<>";
|
||||
public static final String KEYPAD_COMMAND_REGEX = "^[0-9A-H*#<>]+$";
|
||||
|
||||
private static final String TERM = "\r\n";
|
||||
|
||||
private static final String COMMAND_REBOOT = "=";
|
||||
private static final String COMMAND_CONFIG = "C";
|
||||
private static final String COMMAND_ZONE = "L";
|
||||
private static final String COMMAND_ERROR = "E";
|
||||
private static final String COMMAND_VERSION = "V";
|
||||
private static final String COMMAND_ADDRMSG = "K";
|
||||
private static final String COMMAND_ACKCRC = "R";
|
||||
|
||||
public final String command;
|
||||
|
||||
public ADCommand(String command) {
|
||||
this.command = command + TERM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public static ADCommand reboot() {
|
||||
return new ADCommand(COMMAND_REBOOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD configuration command. If configParam is null, a query configuration command will be created.
|
||||
* If configParam consists of one or more NAME=value pairs (separated by '&' characters), a set configuration
|
||||
* command will be created. The validity of configParam is not checked.
|
||||
*
|
||||
* @param configParam String containing parameters to set or null
|
||||
* @return ADCommand object containing the constructed command
|
||||
*/
|
||||
public static ADCommand config(@Nullable String configParam) {
|
||||
if (configParam == null) {
|
||||
return new ADCommand(COMMAND_CONFIG);
|
||||
} else {
|
||||
return new ADCommand(COMMAND_CONFIG + configParam);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD command to set the state of an emulated zone.
|
||||
*
|
||||
* @param zone The emulated zone number (0-99) for the command.
|
||||
* @param state The new state (0 or 1) for the emulated zone.
|
||||
* @return ADCommand object containing the constructed command
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public static ADCommand setZone(int zone, int state) throws IllegalArgumentException {
|
||||
if (zone < 0 || zone > 99 || state < 0 || state > 1) {
|
||||
throw new IllegalArgumentException("Invalid parameter(s)");
|
||||
}
|
||||
return new ADCommand(String.format("%s%02d%d", COMMAND_ZONE, zone, state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD command to get and clear the error counters.
|
||||
*
|
||||
* @return ADCommand object containing the constructed command
|
||||
*/
|
||||
public static ADCommand getErrors() {
|
||||
return new ADCommand(COMMAND_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD command to request a version info message.
|
||||
*
|
||||
* @return ADCommand object containing the constructed command
|
||||
*/
|
||||
public static ADCommand getVersion() {
|
||||
return new ADCommand(COMMAND_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD command to send a message from a specific partition or keypad address, rather than from the Alarm
|
||||
* Decoder unit's configured address.
|
||||
*
|
||||
* @param address The keypad address or partition (0-99) from which to send the command
|
||||
* @param message A String containing the message to send. Length must be > 0.
|
||||
* @return ADCommand object containing the constructed command
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public static ADCommand addressedMessage(int address, String message) throws IllegalArgumentException {
|
||||
if (address < 0 || address > 99 || message.length() < 1) {
|
||||
throw new IllegalArgumentException("Invalid parameter(s)");
|
||||
}
|
||||
return new ADCommand(String.format("%s%02d%s", COMMAND_ADDRMSG, address, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AD command to acknowledge that a received CRC message was valid.
|
||||
*
|
||||
* @return ADCommand object containing the constructed command
|
||||
*/
|
||||
public static ADCommand ackCRC() {
|
||||
return new ADCommand(COMMAND_ACKCRC);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.protocol;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Superclass for all Alarm Decoder protocol message types.
|
||||
* Includes code from the original OH1 alarmdecoder binding by Bernd Pfrommer.
|
||||
*
|
||||
* @author Bob Adair - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ADMessage {
|
||||
|
||||
protected static final Pattern SPLIT_REGEX = Pattern.compile("[^\\,\"]+|\"[^\"]*\"");
|
||||
|
||||
/** string containing the original unparsed message */
|
||||
public final String message;
|
||||
|
||||
public ADMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/** Utility routine to split an AD message into its component parts */
|
||||
protected static List<String> splitMsg(String msg) {
|
||||
List<String> l = new ArrayList<String>();
|
||||
Matcher regexMatcher = SPLIT_REGEX.matcher(msg);
|
||||
while (regexMatcher.find()) {
|
||||
l.add(regexMatcher.group());
|
||||
}
|
||||
return l;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.alarmdecoder.internal.protocol;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The various message types that come from the ad2usb/ad2pi interface
|
||||
*
|
||||
* @author Bernd Pfrommer - Initial contribution (OH1)
|
||||
* @author Bob Adair - Re-factored and removed methods unused in OH2 binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ADMsgType {
|
||||
EXP, // zone expander message
|
||||
KPM, // keypad message
|
||||
LRR, // long range radio message
|
||||
REL, // relay message
|
||||
RFX, // wireless message
|
||||
VER, // version message
|
||||
INVALID; // invalid message
|
||||
|
||||
/** hash map from protocol message heading to type */
|
||||
private static HashMap<String, @Nullable ADMsgType> startToMsgType = new HashMap<>();
|
||||
|
||||
static {
|
||||
startToMsgType.put("!REL", ADMsgType.REL);
|
||||
startToMsgType.put("!SER", ADMsgType.INVALID);
|
||||
startToMsgType.put("!RFX", ADMsgType.RFX);
|
||||
startToMsgType.put("!EXP", ADMsgType.EXP);
|
||||
startToMsgType.put("!LRR", ADMsgType.LRR);
|
||||
startToMsgType.put("!VER", ADMsgType.VER);
|
||||
startToMsgType.put("!KPM", ADMsgType.KPM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message type from message. Relies on static map startToMsgType.
|
||||
*
|
||||
* @param s message string
|
||||
* @return message type
|
||||
*/
|
||||
public static ADMsgType getMsgType(@Nullable String s) {
|
||||
if (s == null || s.length() < 4) {
|
||||
return ADMsgType.INVALID;
|
||||
}
|
||||
if (s.startsWith("[")) {
|
||||
return ADMsgType.KPM;
|
||||
}
|
||||
ADMsgType mt = startToMsgType.get(s.substring(0, 4));
|
||||
if (mt == null) {
|
||||
mt = ADMsgType.INVALID;
|
||||
}
|
||||
return mt;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user