Codebase as of f11ddbc2a3 as an initial commit for the shrunk repo

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer 2010-02-20 19:23:32 +01:00 committed by Kai Kreuzer
commit 4be0e341d8
11883 changed files with 1320197 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
.java text=auto
.xml text=auto

47
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,47 @@
---
name: "\U0001F41B Bug report"
about: Something isn't working correctly with an add-on. This is the wrong place for user-interfaces or openHAB Core issues.
labels: bug
---
<!-- Provide a general summary of the issue in the *Title* above -->
<!-- If the issue is related to a binding, please include its short name in -->
<!-- square brackets in the title - Example: "[astro] My issue..." -->
<!-- Important: Please contact the openHAB community forum for questions or -->
<!-- for configuration and usage guidance: https://community.openhab.org -->
<!-- Feel free to delete any comment lines in the template (starting with "<!--") -->
## Expected Behavior
<!-- If you're describing a bug, tell us what should happen -->
<!-- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!-- If describing a bug, tell us what happens instead of the expected behavior -->
<!-- Include related log information (preferably debug level) and related configs -->
<!-- Use a file attachment for log and config information longer than a few lines -->
<!-- Enclose multi-line log/code snippets with ``` on new lines for proper formatting -->
<!-- If suggesting a change/improvement, explain the difference from current behavior -->
<!-- For improvements, discuss at community.openhab.org first and include link to topic -->
## Possible Solution
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
## Steps to Reproduce (for Bugs)
<!-- Provide a link to a live example, or an unambiguous set of steps to -->
<!-- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
## Context
<!-- How has this issue affected you? What are you trying to accomplish? -->
<!-- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment
<!-- Include as many relevant details about the environment you experienced the bug in -->
* Version used: (e.g., openHAB and add-on versions)
* Environment name and version (e.g. Chrome 76, Java 8, Node.js 12.9, ...):
* Operating System and version (desktop or mobile, Windows 10, Raspbian Buster, ...):

View File

@ -0,0 +1,17 @@
---
name: "Documentation issue"
about: Some information within the add-on documentation is wrong or missing
labels: documentation
---
<!-- Please report only add-on related documentation issues here -->
<!-- Documentation issues within user interfaces or the core should be -->
<!-- reported at https://github.com/openhab/openhab-docs/issues/new -->
<!-- Provide a general summary of the documentation issue in the *Title* above -->
<!-- If the documentation issue is related to a specific add-on, please include its short name in -->
<!-- square brackets in the title - Example: "[astro] My documentation issue..." -->
<!-- Important: Please contact the openHAB community forum for questions or -->
<!-- for configuration and usage guidance: https://community.openhab.org -->

View File

@ -0,0 +1,19 @@
---
name: "Feature request"
about: You think that your favorite add-on should gain another feature
labels: enhancement
---
<!-- Provide a general summary of the feature request in the *Title* above -->
<!-- If the feature request is related to an add-on, please include its short name in -->
<!-- square brackets in the title - Example: "[astro] My feature request..." -->
<!-- Important: Please contact the openHAB community forum for questions or -->
<!-- for configuration and usage guidance: https://community.openhab.org -->
## Your Environment
<!-- Include as many relevant details about the environment when applicable -->
* Version used: (e.g., openHAB and add-on versions)
* Environment name and version (e.g. Chrome 76, Java 8, Node.js 12.9, ...):
* Operating System and version (desktop or mobile, Windows 10, Raspbian Buster, ...):

View File

@ -0,0 +1,10 @@
---
name: "\U0001F914 Support/Usage Question"
about: For usage questions, please use the openHAB community board!
labels: question
---
This is an issue tracker for reporting problems and requesting new features. For usage questions, please use the openHAB community board where there are a lot more people ready to help you out. Thanks!
https://community.openhab.org/

62
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,62 @@
<!--
Thanks for contributing to the openHAB project!
Please describe the goal and effect of your PR here.
Pay attention to the below notes and to *the guidelines* for this repository.
Feel free to delete any comment sections in the template (starting with "<!--").
-->
<!-- TITLE -->
<!--
Please provide a PR summary in the *Title* above, according to the following schema:
- If related to one specific add-on: Mention the add-on shortname in square brackets
e.g. "[exec]", "[netatmo]" or "[tesla]"
- If the PR is work in progress: Add "[WIP]"
- Give a short meaningful description in imperative mood
e.g. "Add support for device XYZ" or "Fix wrongly handled exception"
for a new add-on/binding: "Initial contribution"
Examples:
- "[homematic] Improve communication with weak signal devices"
- "[timemachine][WIP] Initial contribution"
- "Update contribution guidelines on new signing rules"
-->
<!-- DESCRIPTION -->
<!--
Please give a few sentences describing the overall goals of the pull request.
Give enough details to make the improvement and changes of the PR understandable
to both developers and tech-savy users.
Please keep the following in mind:
- What is the classification of the PR, e.g. Bugfix, Improvement, Novel Addition, ... ?
- Did you describe the PRs motivation and goal?
- Did you provide a link to any prior discussion, e.g. an issue or community forum thread?
- Did you describe new features for the end user?
- Did you describe any noteworthy changes in usage for the end user?
- Was the documentation updated accordingly, e.g. the add-on README?
- Does your contribution follow the coding guidelines:
https://www.openhab.org/docs/developer/development/guidelines.html
- Did you check for any (relevant) issues from the static code analysis:
https://www.openhab.org/docs/developer/development/bindings.html#static-code-analysis
- Did you sign-off your work:
https://www.openhab.org/docs/developer/contributing/contributing.html#sign-your-work
-->
<!-- TESTING -->
<!--
Your Pull Request will automatically be built and available under the following folder:
https://openhab.jfrog.io/openhab/libs-pullrequest-local/org/openhab/
It is a good practice to add a URL to your built JAR in this Pull Request description,
so it is easier for the community to test your Add-on.
If your Pull Request contains a new binding, it will likely take some time
before it is reviewed and processed by maintainers.
That said, consider submitting your Add-on in the Eclipse IoT Marketplace
See this thread for more info:
https://community.openhab.org/t/24491
Don't forget to submit a thread about your Add-on in the openHAB community:
https://community.openhab.org/c/add-ons
-->

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
.antlr*
.idea
.DS_Store
*.iml
npm-debug.log
.build.log
.metadata/
bin/
target/
src-gen/
xtend-gen/
.history/
*/plugin.xml_gen
**/.settings/org.eclipse.*
bundles/**/src/main/history
features/**/src/main/history
features/**/src/main/feature
.vscode
.factorypath
*.jar

26
.travis.yml Normal file
View File

@ -0,0 +1,26 @@
os: linux
dist: bionic
language: java
jdk:
- openjdk8
- openjdk11
cache:
directories:
- $HOME/.m2
before_cache:
# remove resolver-status.properties, they change with each run and invalidate the cache
- find $HOME/.m2 -name resolver-status.properties -exec rm {} \;
notifications:
webhooks: https://www.travisbuddy.com/
travisBuddy:
insertMode: update
successBuildLog: true
install: true
script: ./buildci.sh "$TRAVIS_COMMIT_RANGE"

312
CODEOWNERS Normal file
View File

@ -0,0 +1,312 @@
# This file helps GitHub doing automatic review requests for new PRs.
# It should always list the active maintainers of certain add-ons.
# As a fallback, if no specific maintainer is listed below, assign the PR to the repo maintainers team:
* @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
/bundles/org.openhab.binding.ambientweather/ @mhilbush
/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.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
/bundles/org.openhab.binding.digiplex/ @rmichalak
/bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele
/bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor
/bundles/org.openhab.binding.dmx/ @J-N-K
/bundles/org.openhab.binding.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
/bundles/org.openhab.binding.fronius/ @trokohl
/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
/bundles/org.openhab.binding.hue/ @cweitkamp
/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
/bundles/org.openhab.binding.kodi/ @pail23 @cweitkamp
/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
/bundles/org.openhab.binding.mihome/ @pboos
/bundles/org.openhab.binding.miio/ @marcelrv
/bundles/org.openhab.binding.millheat/ @seime
/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
/bundles/org.openhab.binding.neohub/ @andrewfg
/bundles/org.openhab.binding.nest/ @wborn
/bundles/org.openhab.binding.netatmo/ @clinique @cweitkamp @lolodomo
/bundles/org.openhab.binding.network/ @davidgraeff @mettke
/bundles/org.openhab.binding.networkupstools/ @Hilbrand
/bundles/org.openhab.binding.nibeheatpump/ @paulianttila
/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
/bundles/org.openhab.binding.onewire/ @J-N-K
/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
/bundles/org.openhab.binding.phc/ @gnlpfjh
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
/bundles/org.openhab.binding.pixometer/ @Confectrician
/bundles/org.openhab.binding.pjlinkdevice/ @nils
/bundles/org.openhab.binding.plclogo/ @falkena
/bundles/org.openhab.binding.plugwise/ @wborn
/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
/bundles/org.openhab.binding.robonect/ @reyem
/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
/bundles/org.openhab.binding.silvercrestwifisocket/ @jmvaz
/bundles/org.openhab.binding.sinope/ @chaton78
/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.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
/bundles/org.openhab.binding.weatherunderground/ @lolodomo
/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.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.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.jsonpath/ @clinique
/bundles/org.openhab.transform.map/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.regex/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.scale/ @clinique
/bundles/org.openhab.transform.xpath/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.xslt/ @openhab/add-ons-maintainers
/bundles/org.openhab.voice.googletts/ @gbicskei
/bundles/org.openhab.voice.mactts/ @kaikreuzer
/bundles/org.openhab.voice.marytts/ @kaikreuzer
/bundles/org.openhab.voice.picotts/ @FlorianSW
/bundles/org.openhab.voice.pollytts/ @hillmanr
/bundles/org.openhab.voice.voicerss/ @JochenHiller
/itests/org.openhab.binding.astro.tests/ @gerrieg
/itests/org.openhab.binding.avmfritz.tests/ @cweitkamp
/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
/itests/org.openhab.binding.ntp.tests/ @marcelrv
/itests/org.openhab.binding.systeminfo.tests/ @svilenvul
/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.persistence.mapdb.tests/ @mkhl
# PLEASE HELP ADDING FURTHER LINES HERE!

174
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,174 @@
# Contributing to openHAB
Want to hack on openHAB? Awesome! Here are instructions to get you
started. They are probably not perfect, please let us know if anything
feels wrong or incomplete.
## Build Environment
For instructions on setting up your development environment, please
see our dedicated [IDE setup guide](https://www.openhab.org/docs/developer/).
## Contribution guidelines
### Pull requests are always welcome
We are always thrilled to receive pull requests, and do our best to
process them as fast as possible. Not sure if that typo is worth a pull
request? Do it! We will appreciate it.
If your pull request is not accepted on the first try, don't be
discouraged! If there's a problem with the implementation, hopefully you
received feedback on what to improve.
We're trying very hard to keep openHAB lean and focused. We don't want it
to do everything for everybody. This means that we might decide against
incorporating a new feature. However, there might be a way to implement
that feature *on top of* openHAB.
### Discuss your design in the discussion forum
We recommend discussing your plans [in the discussion forum](https://community.openhab.org/c/add-ons)
before starting to code - especially for more ambitious contributions.
This gives other contributors a chance to point you in the right
direction, give feedback on your design, and maybe point out if someone
else is working on the same thing.
### Create issues...
Any significant improvement should be documented as [a GitHub
issue](https://github.com/openhab/openhab-addons/issues?labels=enhancement&page=1&state=open) before anybody
starts working on it.
### ...but check for existing issues first!
Please take a moment to check that an issue doesn't already exist
documenting your bug report or improvement proposal. If it does, it
never hurts to add a quick "+1" or "I have this problem too". This will
help prioritize the most common problems and requests.
### Conventions
Fork the repo and make changes on your fork in a feature branch.
Submit unit tests for your changes. openHAB has a great test framework built in; use
it! Take a look at existing tests for inspiration. Run the full test suite on
your branch before submitting a pull request.
Update the documentation when creating or modifying features. Test
your documentation changes for clarity, concision, and correctness, as
well as a clean documentation build.
Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance.
Pull requests descriptions should be as clear as possible and include a
reference to all the issues that they address.
Pull requests must not contain commits from other users or branches.
Commit messages must start with a capitalized and short summary (max. 50
chars) written in the imperative, followed by an optional, more detailed
explanatory text which is separated from the summary by an empty line.
Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Be
sure to post a comment after pushing. The new commits will show up in the pull
request automatically, but the reviewers will not be notified unless you
comment.
Commits that fix or close an issue should include a reference like `Fixes #XXX`,
which will automatically close the issue when merged.
### Sign your work
The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right to
pass it on as an open-source patch. The rules are pretty simple: if you
can certify the below (from
[developercertificate.org](https://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
using your real name (sorry, no pseudonyms or anonymous contributions.) and an
e-mail address under which you can be reached (sorry, no GitHub noreply e-mail
addresses (such as username@users.noreply.github.com) or other non-reachable
addresses are allowed).
On the command line you can use `git commit -s` to sign off the commit.
### How can I become a maintainer?
* Step 1: learn the component inside out
* Step 2: make yourself useful by contributing code, bugfixes, support etc.
* Step 3: volunteer on [the discussion group](https://github.com/openhab/openhab-addons/issues?labels=question&page=1&state=open)
Don't forget: being a maintainer is a time investment. Make sure you will have time to make yourself available.
You don't have to be a maintainer to make a difference on the project!
## Community Guidelines
We want to keep the openHAB community awesome, growing and collaborative. We
need your help to keep it that way. To help with this we have come up with some
general guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members: no
regional, racial, gender, or other abuse will be tolerated. We like nice people
way better than mean ones!
* Encourage diversity and participation: Make everyone in our community
feel welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break the
law.
* Stay on topic: Make sure that you are posting to the correct channel
and avoid off-topic discussions. Remember when you update an issue or
respond to an email you are potentially sending to a large number of
people. Please consider this before you update. Also remember that
nobody likes spam.

277
LICENSE Normal file
View File

@ -0,0 +1,277 @@
Eclipse Public License - v 2.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial content
Distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from
and are Distributed by that particular Contributor. A Contribution
"originates" from a Contributor if it was added to the Program by
such Contributor itself or anyone acting on such Contributor's behalf.
Contributions do not include changes or additions to the Program that
are not Modified Works.
"Contributor" means any person or entity that Distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which
are necessarily infringed by the use or sale of its Contribution alone
or when combined with the Program.
"Program" means the Contributions Distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement
or any Secondary License (as applicable), including Contributors.
"Derivative Works" shall mean any work, whether in Source Code or other
form, that is based on (or derived from) the Program and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship.
"Modified Works" shall mean any work in Source Code or other form that
results from an addition to, deletion from, or modification of the
contents of the Program, including, for purposes of clarity any new file
in Source Code form that contains any contents of the Program. Modified
Works shall not include works that contain only declarations,
interfaces, types, classes, structures, or files of the Program solely
in each case in order to link to, bind by name, or subclass the Program
or Modified Works thereof.
"Distribute" means the acts of a) distributing or b) making available
in any manner that enables the transfer of a copy.
"Source Code" means the form of a Program preferred for making
modifications, including but not limited to software source code,
documentation source, and configuration files.
"Secondary License" means either the GNU General Public License,
Version 2.0, or any later versions of that license, including any
exceptions or additional permissions as identified by the initial
Contributor.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free copyright
license to reproduce, prepare Derivative Works of, publicly display,
publicly perform, Distribute and sublicense the Contribution of such
Contributor, if any, and such Derivative Works.
b) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free patent
license under Licensed Patents to make, use, sell, offer to sell,
import and otherwise transfer the Contribution of such Contributor,
if any, in Source Code or other form. This patent license shall
apply to the combination of the Contribution and the Program if, at
the time the Contribution is added by the Contributor, such addition
of the Contribution causes such combination to be covered by the
Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are
provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity.
Each Contributor disclaims any liability to Recipient for claims
brought by any other entity based on infringement of intellectual
property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual
property rights needed, if any. For example, if a third party
patent license is required to allow Recipient to Distribute the
Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has
sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.
e) Notwithstanding the terms of any Secondary License, no
Contributor makes additional grants to any Recipient (other than
those set forth in this Agreement) as a result of such Recipient's
receipt of the Program under the terms of a Secondary License
(if permitted under the terms of Section 3).
3. REQUIREMENTS
3.1 If a Contributor Distributes the Program in any form, then:
a) the Program must also be made available as Source Code, in
accordance with section 3.2, and the Contributor must accompany
the Program with a statement that the Source Code for the Program
is available under this Agreement, and informs Recipients how to
obtain it in a reasonable manner on or through a medium customarily
used for software exchange; and
b) the Contributor may Distribute the Program under a license
different than this Agreement, provided that such license:
i) effectively disclaims on behalf of all other Contributors all
warranties and conditions, express and implied, including
warranties or conditions of title and non-infringement, and
implied warranties or conditions of merchantability and fitness
for a particular purpose;
ii) effectively excludes on behalf of all other Contributors all
liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;
iii) does not attempt to limit or alter the recipients' rights
in the Source Code under section 3.2; and
iv) requires any subsequent distribution of the Program by any
party to be under a license that satisfies the requirements
of this section 3.
3.2 When the Program is Distributed as Source Code:
a) it must be made available under this Agreement, or if the
Program (i) is combined with other material in a separate file or
files made available under a Secondary License, and (ii) the initial
Contributor attached to the Source Code the notice described in
Exhibit A of this Agreement, then the Program may be made available
under the terms of such Secondary Licenses, and
b) a copy of this Agreement must be included with each copy of
the Program.
3.3 Contributors may not remove or alter any copyright, patent,
trademark, attribution notices, disclaimers of warranty, or limitations
of liability ("notices") contained within the Program from any copy of
the Program which they Distribute, provided that Contributors may add
their own appropriate notices.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities
with respect to end users, business partners and the like. While this
license is intended to facilitate the commercial use of the Program,
the Contributor who includes the Program in a commercial product
offering should do so in a manner which does not create potential
liability for other Contributors. Therefore, if a Contributor includes
the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and indemnify every
other Contributor ("Indemnified Contributor") against any losses,
damages and costs (collectively "Losses") arising from claims, lawsuits
and other legal actions brought by a third party against the Indemnified
Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program
in a commercial product offering. The obligations in this section do not
apply to any claims or Losses relating to any actual or alleged
intellectual property infringement. In order to qualify, an Indemnified
Contributor must: a) promptly notify the Commercial Contributor in
writing of such claim, and b) allow the Commercial Contributor to control,
and cooperate with the Commercial Contributor in, the defense and any
related settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those performance
claims and warranties, and if a court requires any other Contributor to
pay any damages as a result, the Commercial Contributor must pay
those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all
risks associated with its exercise of rights under this Agreement,
including but not limited to the risks and costs of program errors,
compliance with applicable laws, damage to or loss of data, programs
or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further
action by the parties hereto, such provision shall be reformed to the
minimum extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other software
or hardware) infringes such Recipient's patent(s), then such Recipient's
rights granted under Section 2(b) shall terminate as of the date such
litigation is filed.
All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of
time after becoming aware of such noncompliance. If all Recipient's
rights under this Agreement terminate, Recipient agrees to cease use
and distribution of the Program as soon as reasonably practicable.
However, Recipient's obligations under this Agreement and any licenses
granted by Recipient relating to the Program shall continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement,
but in order to avoid inconsistency the Agreement is copyrighted and
may only be modified in the following manner. The Agreement Steward
reserves the right to publish new versions (including revisions) of
this Agreement from time to time. No one other than the Agreement
Steward has the right to modify this Agreement. The Eclipse Foundation
is the initial Agreement Steward. The Eclipse Foundation may assign the
responsibility to serve as the Agreement Steward to a suitable separate
entity. Each new version of the Agreement will be given a distinguishing
version number. The Program (including Contributions) may always be
Distributed subject to the version of the Agreement under which it was
received. In addition, after a new version of the Agreement is published,
Contributor may elect to Distribute the Program (including its
Contributions) under the new version.
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
receives no rights or licenses to the intellectual property of any
Contributor under this Agreement, whether expressly, by implication,
estoppel or otherwise. All rights in the Program not expressly granted
under this Agreement are reserved. Nothing in this Agreement is intended
to be enforceable by any entity that is not a Contributor or Recipient.
No third-party beneficiary rights are created under this Agreement.
Exhibit A - Form of Secondary Licenses Notice
"This Source Code may also be made available under the following
Secondary Licenses when the conditions for such availability set forth
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
version(s), and exceptions or additional permissions here}."
Simply including a copy of this Agreement, including this Exhibit A
is not sufficient to license the Source Code under Secondary Licenses.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to
look for such a notice.
You may add additional accurate notices of copyright ownership.

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# openHAB Add-ons
<img align="right" width="220" src="./logo.png" />
[![Build Status](https://travis-ci.com/openhab/openhab-addons.svg)](https://travis-ci.com/openhab/openhab-addons)
[![EPL-2.0](https://img.shields.io/badge/license-EPL%202-green.svg)](https://opensource.org/licenses/EPL-2.0)
[![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=2164344)](https://www.bountysource.com/teams/openhab/issues?tracker_ids=2164344)
This repository contains the official set of add-ons that are implemented on top of openHAB Core APIs.
Add-ons that got accepted in here will be maintained (e.g. adapted to new core APIs)
by the [openHAB Add-on maintainers](https://github.com/orgs/openhab/teams/add-ons-maintainers).
To get started with binding development, follow our guidelines and tutorials over at https://www.openhab.org/docs/developer.
If you are interested in openHAB Core development, we invite you to come by on https://github.com/openhab/openhab-core.
## Add-ons in other repositories
Some add-ons are not in this repository, but still part of the official [openHAB distribution](https://github.com/openhab/openhab-distro).
An incomplete list of other repositories follows below:
* https://github.com/openhab/org.openhab.binding.zwave
* https://github.com/openhab/org.openhab.binding.zigbee
* https://github.com/openhab/openhab-webui
## Development / Repository Organization
openHAB add-ons are [Java](https://en.wikipedia.org/wiki/Java_(programming_language)) `.jar` files.
The openHAB build system is based on [Maven](https://maven.apache.org/what-is-maven.html).
The official IDE (Integrated development environment) is Eclipse.
You find the following repository structure:
```
.
+-- bom Maven buildsystem: Bill of materials
| +-- openhab-addons Lists all extensions for other repos to reference them
| +-- ... Other boms
|
+-- bundles Official openHAB extensions
| +-- org.openhab.binding.airquality
| +-- org.openhab.binding.astro
| +-- ...
|
+-- features Part of the runtime dependency resolver ("Karaf features")
|
+-- itests Integration tests. Those tests require parts of the framework to run.
| +-- org.openhab.binding.astro.tests
| +-- org.openhab.binding.avmfritz.tests
| +-- ...
|
+-- src/etc Auxilary buildsystem files: The license header for automatic checks for example
+-- tools Static code analyser instructions
|
+-- CODEOWNERS This file assigns people to directories so that they are informed if a pull-request
would modify their add-ons.
```
### Command line build
To build all add-ons from the command-line, type in:
`mvn clean install`
Optionally you can skip tests (`-DskipTests`) or skip some static analysis (`-DskipChecks`).
This does improve the build time but could hide problems in your code.
For binding development you want to run that command without skipping checks and tests.
To check if your code is following the [code style](https://www.openhab.org/docs/developer/guidelines.html#b-code-formatting-rules-style) run `mvn spotless:check`.
If Maven prints `[INFO] Spotless check skipped` then run `mvn spotless:check -Dspotless.check.skip=false` instead as the check is not mandatory yet.
To reformat you code run `mvn spotless:apply`.
Subsequent calls can include the `-o` for offline as in: `mvn clean install -DskipChecks -o` which will be a bit faster.
For integration tests you might need to run: `mvn clean install -DwithResolver -DskipChecks`
You find a generated `.jar` file per bundle in the respective bundle `/target` directory.
### How to develop via an Integrated Development Environment (IDE)
We have assembled some step-by-step guides for different IDEs on our developer documentation website:
https://www.openhab.org/docs/developer/#setup-the-development-environment
Happy coding!

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.openhab-addons</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

1469
bom/openhab-addons/pom.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
<?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 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-1.8">
<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>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.addons.bom.openhab-core-index</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>

View File

@ -0,0 +1,39 @@
<?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.bom</groupId>
<artifactId>org.openhab.addons.reactor.bom</artifactId>
<version>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.addons.bom.openhab-core-index</artifactId>
<name>openHAB Add-ons :: BOM :: openHAB Core Index</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.openhab-core</artifactId>
<version>${ohc.version}</version>
<type>pom</type>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

86
bom/pom.xml Normal file
View File

@ -0,0 +1,86 @@
<?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</groupId>
<artifactId>org.openhab.addons.reactor</artifactId>
<version>2.5.10-SNAPSHOT</version>
</parent>
<groupId>org.openhab.addons.bom</groupId>
<artifactId>org.openhab.addons.reactor.bom</artifactId>
<packaging>pom</packaging>
<name>openHAB Add-ons :: BOM</name>
<modules>
<module>runtime-index</module>
<module>test-index</module>
<module>openhab-core-index</module>
<module>openhab-addons</module>
</modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<inherited>false</inherited>
<executions>
<execution>
<id>create-bom</id>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy file="${basedirRoot}/../../bundles/pom.xml" overwrite="true" tofile="${basedirRoot}/../../bom/openhab-addons/pom.xml"/>
<!-- rewrite footer -->
<replaceregexp file="${basedirRoot}/../../bom/openhab-addons/pom.xml" match="/modules[\s\S]*dependencies&gt;" replace="/dependencies&gt;"/>
<!-- rewrite header -->
<replaceregexp file="${basedirRoot}/../../bom/openhab-addons/pom.xml" match="\S*parent[\s\S]*modules&gt;\S*" replace="header"/>
<replace file="{basedirRoot}/../../bom/openhab-addons/pom.xml">
<replacetoken>header</replacetoken>
<replacevalue><![CDATA[<parent>
<groupId>org.openhab.addons.bom</groupId>
<artifactId>org.openhab.addons.reactor.bom</artifactId>
<version>${project.version}</version>
</parent>
<artifactId>org.openhab.addons.bom.openhab-addons</artifactId>
<packaging>pom</packaging>
<name>openHAB Add-ons :: BOM :: openHAB Add-ons</name>
<dependencies>]]></replacevalue>
</replace>
<!-- rewrite content -->
<replace file="{basedirRoot}/../../bom/openhab-addons/pom.xml">
<replacetoken><![CDATA[<module>]]></replacetoken>
<replacevalue><![CDATA[<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>]]></replacevalue>
</replace>
<replace file="{basedirRoot}/../../bom/openhab-addons/pom.xml">
<replacetoken><![CDATA[</module>]]></replacetoken>
<replacevalue><![CDATA[</artifactId>
<version>@dollar{project.version}</version>
</dependency>]]></replacevalue>
</replace>
<replace file="{basedirRoot}/../../bom/openhab-addons/pom.xml">
<replacetoken>@dollar</replacetoken>
<replacevalue>$</replacevalue>
</replace>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@ -0,0 +1,27 @@
<?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 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-1.8">
<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>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.addons.bom.runtime-index</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>

39
bom/runtime-index/pom.xml Normal file
View File

@ -0,0 +1,39 @@
<?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.bom</groupId>
<artifactId>org.openhab.addons.reactor.bom</artifactId>
<version>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.addons.bom.runtime-index</artifactId>
<name>openHAB Add-ons :: BOM :: Runtime Index</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.runtime</artifactId>
<version>${ohc.version}</version>
<type>pom</type>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

27
bom/test-index/.classpath Normal file
View File

@ -0,0 +1,27 @@
<?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 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-1.8">
<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
bom/test-index/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.addons.bom.test-index</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>

50
bom/test-index/pom.xml Normal file
View File

@ -0,0 +1,50 @@
<?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.bom</groupId>
<artifactId>org.openhab.addons.reactor.bom</artifactId>
<version>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.addons.bom.test-index</artifactId>
<name>openHAB Add-ons :: BOM :: Test Index</name>
<dependencies>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.enroute.junit.wrapper</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test</artifactId>
<version>${ohc.version}</version>
<type>pom</type>
<scope>compile</scope>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

84
buildci.sh Executable file
View File

@ -0,0 +1,84 @@
#!/bin/bash
set -o pipefail # exit build with error when pipes fail
function prevent_timeout() {
local i=0
while [[ -e /proc/$1 ]]; do
# print zero width char every 3 minutes while building
if [[ "$i" -eq "180" ]]; then printf %b '\u200b'; i=0; else i=$((i+1)); fi
sleep 1
done
}
function print_reactor_summary() {
sed -ne '/\[INFO\] Reactor Summary.*:/,$ p' "$1" | sed 's/\[INFO\] //'
}
function mvnp() {
local command=(mvn $@)
exec "${command[@]}" 2>&1 | # execute, redirect stderr to stdout
stdbuf -o0 grep -vE "Download(ed|ing) from [a-z.]+: https:" | # filter out downloads
tee .build.log | # write output to log
stdbuf -oL grep -aE '^\[INFO\] Building .+ \[.+\]$' | # filter progress
stdbuf -o0 sed -uE 's/^\[INFO\] Building (.*[^ ])[ ]+\[([0-9]+\/[0-9]+)\]$/\2| \1/' | # prefix project name with progress
stdbuf -o0 sed -e :a -e 's/^.\{1,6\}|/ &/;ta' & # right align progress with padding
local pid=$!
prevent_timeout ${pid} &
wait ${pid}
}
COMMITS=${1:-"master...HEAD"}
# Determine if this is a single changed addon -> Perform build with tests + integration tests and all SAT checks
CHANGED_BUNDLE_DIR=`git diff --dirstat=files,0 ${COMMITS} bundles/ | sed 's/^[ 0-9.]\+% bundles\///g' | grep -o -P "^([^/]*)" | uniq`
# Determine if this is a single changed itest -> Perform build with tests + integration tests and all SAT checks
# for this we have to remove '.tests' from the folder name.
CHANGED_ITEST_DIR=`git diff --dirstat=files,0 ${COMMITS} itests/ | sed 's/^[ 0-9.]\+% itests\///g' | sed 's/\.tests\///g' | uniq`
CDIR=`pwd`
# if a bundle and (optionally the linked itests) where changed build the module and its tests
if [[ ! -z "$CHANGED_BUNDLE_DIR" && -e "bundles/$CHANGED_BUNDLE_DIR" && ( "$CHANGED_BUNDLE_DIR" == "$CHANGED_ITEST_DIR" || -z "$CHANGED_ITEST_DIR" ) ]]; then
CHANGED_DIR="$CHANGED_BUNDLE_DIR"
fi
# if no bundle was changed but only itests
if [[ -z "$CHANGED_BUNDLE_DIR" ]] && [[ -e "bundles/$CHANGED_ITEST_DIR" ]]; then
CHANGED_DIR="$CHANGED_ITEST_DIR"
fi
if [[ ! -z "$CHANGED_DIR" ]] && [[ -e "bundles/$CHANGED_DIR" ]]; then
echo "Single addon pull request: Building $CHANGED_DIR"
echo "MAVEN_OPTS='-Xms1g -Xmx2g -Dorg.slf4j.simpleLogger.log.org.openhab.tools.analysis.report.ReportUtility=DEBUG -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN'" > ~/.mavenrc
ARTIFACT_ID=$(mvn -f bundles/${CHANGED_DIR}/pom.xml help:evaluate -Dexpression=project.artifactId -q -DforceStdout)
mvn clean install -B -am -pl ":$ARTIFACT_ID" 2>&1 |
stdbuf -o0 grep -vE "Download(ed|ing) from [a-z.]+: https:" | # Filter out Download(s)
stdbuf -o0 grep -v "target/code-analysis" | # filter out some debug code from reporting utility
tee ${CDIR}/.build.log
if [[ $? -ne 0 ]]; then
exit 1
fi
# add the postfix to make sure we actually find the correct itest
if [[ -e "itests/$CHANGED_DIR.tests" ]]; then
echo "Single addon pull request: Building itest $CHANGED_DIR"
cd "itests/$CHANGED_DIR.tests"
mvn clean install -B 2>&1 |
stdbuf -o0 grep -vE "Download(ed|ing) from [a-z.]+: https:" | # Filter out Download(s)
stdbuf -o0 grep -v "target/code-analysis" | # filter out some debug code from reporting utility
tee -a ${CDIR}/.build.log
if [[ $? -ne 0 ]]; then
exit 1
fi
fi
else
echo "Build all"
echo "MAVEN_OPTS='-Xms1g -Xmx2g -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn'" > ~/.mavenrc
mvnp clean install -B -DskipChecks=true
if [[ $? -eq 0 ]]; then
print_reactor_summary .build.log
else
tail -n 1000 .build.log
exit 1
fi
fi

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<profiles>
<profile>
<id>openHAB-snapshots</id>
<repositories>
<repository>
<id>archetype</id>
<url>https://openhab.jfrog.io/openhab/libs-snapshot</url>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>openHAB-snapshots</activeProfile>
</activeProfiles>
</settings>

View File

@ -0,0 +1,44 @@
@echo off
SETLOCAL
SET ARGC=0
FOR %%x IN (%*) DO SET /A ARGC+=1
IF %ARGC% NEQ 3 (
echo Usage: %0 BindingIdInCamelCase Author GithubUser
exit /B 1
)
SET OpenhabCoreVersion="2.5.0"
SET OpenhabVersion="2.5.9-SNAPSHOT"
SET BindingIdInCamelCase=%~1
SET BindingIdInLowerCase=%BindingIdInCamelCase%
SET Author=%~2
SET GithubUser=%~3
call :LoCase BindingIdInLowerCase
call mvn -s archetype-settings.xml archetype:generate -N -DarchetypeGroupId=org.openhab.core.tools.archetypes -DarchetypeArtifactId=org.openhab.core.tools.archetypes.binding -DarchetypeVersion=%OpenhabCoreVersion% -DgroupId=org.openhab.binding -DartifactId=org.openhab.binding.%BindingIdInLowerCase% -Dpackage=org.openhab.binding.%BindingIdInLowerCase% -Dversion=%OpenhabVersion% -DbindingId=%BindingIdInLowerCase% -DbindingIdCamelCase=%BindingIdInCamelCase% -DvendorName=openHAB -Dnamespace=org.openhab -Dauthor="%Author%" -DgithubUser="%GithubUser%"
COPY ..\src\etc\NOTICE org.openhab.binding.%BindingIdInLowerCase%\
:: temporary fix
:: replace ${project.version} by ${ohc.version} in src/main/feature/feature.xml
@powershell -command "(Get-Content org.openhab.binding.$env:BindingIdInLowerCase/src/main/feature/feature.xml).replace('-core/${project.version}', '-core/${ohc.version}') | Set-Content org.openhab.binding.$env:BindingIdInLowerCase/src/main/feature/feature.xml"
(SET BindingIdInLowerCase=)
(SET BindingIdInCamelCase=)
(SET Author=)
(SET GithubUser=)
GOTO:EOF
:LoCase
:: Subroutine to convert a variable VALUE to all lower case.
:: The argument for this subroutine is the variable NAME.
FOR %%i IN ("A=a" "B=b" "C=c" "D=d" "E=e" "F=f" "G=g" "H=h" "I=i" "J=j" "K=k" "L=l" "M=m" "N=n" "O=o" "P=p" "Q=q" "R=r" "S=s" "T=t" "U=u" "V=v" "W=w" "X=x" "Y=y" "Z=z") DO CALL SET "%1=%%%1:%%~i%%"
GOTO:EOF
ENDLOCAL

View File

@ -0,0 +1,35 @@
#!/bin/bash
[ $# -lt 3 ] && { echo "Usage: $0 <BindingIdInCamelCase> <Author> <GitHub Username>"; exit 1; }
openHABCoreVersion=2.5.0
openHABVersion=2.5.9-SNAPSHOT
camelcaseId=$1
id=`echo $camelcaseId | tr '[:upper:]' '[:lower:]'`
author=$2
githubUser=$3
mvn -s archetype-settings.xml archetype:generate -N \
-DarchetypeGroupId=org.openhab.core.tools.archetypes \
-DarchetypeArtifactId=org.openhab.core.tools.archetypes.binding \
-DarchetypeVersion=$openHABCoreVersion \
-DgroupId=org.openhab.binding \
-DartifactId=org.openhab.binding.$id \
-Dpackage=org.openhab.binding.$id \
-Dversion=$openHABVersion \
-DbindingId=$id \
-DbindingIdCamelCase=$camelcaseId \
-DvendorName=openHAB \
-Dnamespace=org.openhab \
-Dauthor="$author" \
-DgithubUser="$githubUser"
directory="org.openhab.binding.$id/"
cp ../src/etc/NOTICE "$directory"
# temporary fix
# replace ${project.version} by ${ohc.version} in src/main/feature/feature.xml
sed -i -e "s|\-core\/\${project.version}|\-core\/\${ohc.version}|g" "$directory/src/main/feature/feature.xml"

View 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-1.8">
<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>

View 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>

View 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

View 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
}
}
```

View File

@ -0,0 +1,15 @@
<?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>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.adorne</artifactId>
<name>openHAB Add-ons :: Bundles :: Adorne Binding</name>
</project>

View File

@ -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>

View File

@ -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.eclipse.smarthome.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";
}

View File

@ -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.eclipse.smarthome.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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
import org.eclipse.smarthome.config.discovery.DiscoveryService;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.util.UIDUtils;
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.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) {
}
}

View File

@ -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.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
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();
}
}

View File

@ -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.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.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;
}
}

View File

@ -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.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.types.Command;
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.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);
}
}
}

View File

@ -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.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingStatusInfo;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.openhab.binding.adorne.internal.configuration.AdorneSwitchConfiguration;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
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
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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.eclipse.smarthome.core.thing.ThingTypeUID;
import org.openhab.binding.adorne.internal.AdorneDeviceState;
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
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);
}
}

View File

@ -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>

View File

@ -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://eclipse.org/smarthome/schemas/thing-description/v1.0.0 http://eclipse.org/smarthome/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>

View File

@ -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>

View File

@ -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"}

View File

@ -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
}
}

View File

@ -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]
}

View 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-1.8">
<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>

View 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>

View 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

View 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
```

View File

@ -0,0 +1,15 @@
<?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>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.airquality</artifactId>
<name>openHAB Add-ons :: Bundles :: Airquality Binding</name>
</project>

View File

@ -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>

View File

@ -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.eclipse.smarthome.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.eclipse.smarthome.core.library.types.StringType;
import org.eclipse.smarthome.core.library.unit.SIUnits;
import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.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);
}

View File

@ -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;
}

View File

@ -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.eclipse.smarthome.core.i18n.TimeZoneProvider;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
import org.openhab.binding.airquality.internal.handler.AirQualityHandler;
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;
}
}

View File

@ -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.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
import org.eclipse.smarthome.config.discovery.DiscoveryService;
import org.eclipse.smarthome.core.i18n.LocationProvider;
import org.eclipse.smarthome.core.library.types.PointType;
import org.eclipse.smarthome.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");
}
}
}
}

View File

@ -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.eclipse.smarthome.core.i18n.TimeZoneProvider;
import org.eclipse.smarthome.core.library.types.DateTimeType;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.HSBType;
import org.eclipse.smarthome.core.library.types.PointType;
import org.eclipse.smarthome.core.library.types.QuantityType;
import org.eclipse.smarthome.core.library.types.StringType;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
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.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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View 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-1.8">
<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>

View 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>

View 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

View 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
}
}
```

View File

@ -0,0 +1,24 @@
<?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>2.5.10-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>

View File

@ -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>

View File

@ -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.eclipse.smarthome.core.thing.DefaultSystemChannelTypeProvider;
import org.eclipse.smarthome.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)));
}

View File

@ -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.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
import org.openhab.binding.airvisualnode.internal.handler.AirVisualNodeHandler;
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;
}
}

View File

@ -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;
}

View File

@ -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.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
import org.eclipse.smarthome.config.discovery.DiscoveryResult;
import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
import org.eclipse.smarthome.config.discovery.DiscoveryService;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants;
import org.openhab.binding.airvisualnode.internal.config.AirVisualNodeConfig;
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);
}
}
}
}

View File

@ -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.eclipse.smarthome.core.library.unit.MetricPrefix.MICRO;
import static org.eclipse.smarthome.core.library.unit.SIUnits.CELSIUS;
import static org.eclipse.smarthome.core.library.unit.SIUnits.CUBIC_METRE;
import static org.eclipse.smarthome.core.library.unit.SIUnits.GRAM;
import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.ONE;
import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.PARTS_PER_MILLION;
import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.PERCENT;
import static org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants.*;
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.eclipse.smarthome.core.library.types.DateTimeType;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.QuantityType;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.openhab.binding.airvisualnode.internal.config.AirVisualNodeConfig;
import org.openhab.binding.airvisualnode.internal.json.NodeData;
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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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&#8322; 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&#179;</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>

View 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-1.8">
<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>

View 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>

View 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

View 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.

View File

@ -0,0 +1,15 @@
<?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>2.5.10-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.alarmdecoder</artifactId>
<name>openHAB Add-ons :: Bundles :: Alarm Decoder Binding</name>
</project>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More