Codebase as of c53e4aed26 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 bbf1a7fd29
302 changed files with 29726 additions and 0 deletions

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

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.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

23
.travis.yml Normal file
View File

@ -0,0 +1,23 @@
os: linux
dist: focal
language: java
jdk: 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"

247
CODEOWNERS Normal file
View File

@ -0,0 +1,247 @@
# 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.airquality/ @kubawolanin
/bundles/org.openhab.binding.airvisualnode/ @3cky
/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.avmfritz/ @cweitkamp
/bundles/org.openhab.binding.bigassfan/ @mhilbush
/bundles/org.openhab.binding.bluetooth/ @cdjackson @kaikreuzer
/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.ruuvitag/ @ssalonen
/bundles/org.openhab.binding.boschindego/ @jofleck
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
/bundles/org.openhab.binding.buienradar/ @gedejong
/bundles/org.openhab.binding.chromecast/ @kaikreuzer
/bundles/org.openhab.binding.cm11a/ @BobRak
/bundles/org.openhab.binding.coolmasternet/ @projectgus
/bundles/org.openhab.binding.daikin/ @caffineehacker @psmedley
/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.dscalarm/ @RSStephens
/bundles/org.openhab.binding.dsmr/ @Hilbrand
/bundles/org.openhab.binding.dwdunwetter/ @limdul79
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.enocean/ @fruggy83
/bundles/org.openhab.binding.enturno/ @klocsson
/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.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.globalcache/ @mhilbush
/bundles/org.openhab.binding.gpstracker/ @gbicskei
/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.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.icloud/ @pgfeller
/bundles/org.openhab.binding.ihc/ @paulianttila
/bundles/org.openhab.binding.innogysmarthome/ @ollie-dev
/bundles/org.openhab.binding.ipp/ @peuter
/bundles/org.openhab.binding.irtrans/ @kgoderis
/bundles/org.openhab.binding.jeelink/ @vbier
/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.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.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.lutron/ @actong @bobadair
/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.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.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.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.ntp/ @marcelrv
/bundles/org.openhab.binding.nuki/ @mkatter
/bundles/org.openhab.binding.oceanic/ @kgoderis
/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.openuv/ @clinique
/bundles/org.openhab.binding.openweathermap/ @cweitkamp
/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.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.samsungtv/ @paulianttila
/bundles/org.openhab.binding.satel/ @druciak
/bundles/org.openhab.binding.seneye/ @nikotanghe
/bundles/org.openhab.binding.sensebox/ @hakan42
/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.snmp/ @J-N-K
/bundles/org.openhab.binding.solaredge/ @alexf2015
/bundles/org.openhab.binding.solarlog/ @johannrichard
/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.synopanalyzer/ @clinique
/bundles/org.openhab.binding.systeminfo/ @svilenvul
/bundles/org.openhab.binding.tado/ @dfrommi
/bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag
/bundles/org.openhab.binding.telegram/ @ZzetT
/bundles/org.openhab.binding.tellstick/ @jarlebh
/bundles/org.openhab.binding.tesla/ @kgoderis
/bundles/org.openhab.binding.toon/ @jongj
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
/bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
/bundles/org.openhab.binding.unifi/ @mgbowman
/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.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.xmltv/ @clinique
/bundles/org.openhab.binding.xmppclient/ @pavel-gololobov
/bundles/org.openhab.binding.yamahareceiver/ @davidgraeff @zarusz
/bundles/org.openhab.binding.yeelight/ @claell
/bundles/org.openhab.binding.zoneminder/ @Mr-Eskildsen
/bundles/org.openhab.binding.zway/ @pathec
/bundles/org.openhab.extensionservice.marketplace/ @kaikreuzer
/bundles/org.openhab.extensionservice.marketplace.automation/ @kaikreuzer
/bundles/org.openhab.io.azureiothub/ @nikotanghe
/bundles/org.openhab.io.homekit/ @beowulfe
/bundles/org.openhab.io.hueemulation/ @davidgraeff @digitaldan
/bundles/org.openhab.io.imperihome/ @pdegeus
/bundles/org.openhab.io.javasound/ @kaikreuzer
/bundles/org.openhab.io.mqttembeddedbroker/ @davidgraeff
/bundles/org.openhab.io.neeo/ @tmrobert8
/bundles/org.openhab.io.openhabcloud/ @kaikreuzer
/bundles/org.openhab.io.transport.modbus/ @ssalonen
/bundles/org.openhab.io.webaudio/ @kaikreuzer
/bundles/org.openhab.persistence.mapdb/ @mkhl
/bundles/org.openhab.persistence.influxdb/ @lujop
/bundles/org.openhab.transform.exec/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.javascript/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.jinja/ @jochen314
/bundles/org.openhab.transform.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.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>

View File

@ -0,0 +1,66 @@
<?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>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.addons.bom.openhab-addons</artifactId>
<packaging>pom</packaging>
<name>openHAB Add-ons :: BOM :: openHAB Add-ons</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.nest</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.dynamodb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.influxdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.mapdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.mongodb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.rrd4j</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.voice.googletts</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</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-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

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,41 @@
<?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>3.0.0-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>

91
bom/pom.xml Normal file
View File

@ -0,0 +1,91 @@
<?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>3.0.0-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-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

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>

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

@ -0,0 +1,41 @@
<?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>3.0.0-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-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

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

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

@ -0,0 +1,49 @@
<?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>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.addons.bom.test-index</artifactId>
<name>openHAB Add-ons :: BOM :: Test Index</name>
<dependencies>
<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>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test-index</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>

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,40 @@
@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 OpenhabVersion="3.0.0-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=%OpenhabVersion% -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%\
(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,31 @@
#!/bin/bash
[ $# -lt 3 ] && { echo "Usage: $0 <BindingIdInCamelCase> <Author> <GitHub Username>"; exit 1; }
openHABVersion=3.0.0-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=$openHABVersion \
-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"

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 kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<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.nest</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,260 @@
# Nest Binding
The Nest binding integrates devices by [Nest](https://nest.com) using the [Nest API](https://developers.nest.com/documentation/cloud/get-started) (REST).
Because the Nest API runs on Nest's servers a connection with the Internet is required for sending and receiving information.
The binding uses HTTPS to connect to the Nest API using ports 443 and 9553. Make sure outbound connections to these ports are not blocked by a firewall.
> Note: This binding can only be used with Nest devices if you have an existing Nest developer account signed up for the Works with Nest (WWN) program.
New integrations using the WWN program are no longer accepted because WWN is being retired.
To keep using this binding do **NOT** migrate your Nest Account to a Google Account.
For more information see [What's happening at Nest?](https://nest.com/whats-happening/).
## Supported Things
The table below lists the Nest binding thing types:
| Things | Description | Thing Type |
|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
| Nest Account | An account for using the Nest REST API | account |
| Nest Cam (Indoor, IQ, Outdoor), Dropcam | A Nest Cam registered with your account | camera |
| Nest Protect | The smoke detector/Nest Protect for the account | smoke_detector |
| Structure | The Nest structure defines the house the account has setup on Nest. You will only have more than one structure if you have more than one house | structure |
| Nest Thermostat (E) | A Thermostat to control the various aspects of the house's HVAC system | thermostat |
## Authorization
The Nest API uses OAuth for authorization.
Therefore the binding needs some authorization parameters before it can access your Nest account via the Nest API.
To get these authorization parameters you first need to sign up as a [Nest Developer](https://developer.nest.com) and [register a new Product](https://developer.nest.com/products/new) (free and instant).
While registering a new Product (on the Product Details page) make sure to:
* Leave both "OAuth Redirect URI" fields empty to enable PIN-based authorization.
* Grant all the permissions you intend to use. When in doubt, enable the permission because the binding needs to be reauthorized when permissions change at a later time.
After creating the Product, your browser shows the Product Overview page.
This page contains the **Product ID** and **Product Secret** authorization parameters that are used by the binding.
Take note of both parameters or keep this page open in a browser tab.
Now copy and paste the "Authorization URL" in a new browser tab.
Accept the permissions and you will be presented the **Pincode** authorization parameter that is also used by the binding.
You can return to the Product Overview page at a later time by opening the [Products](https://console.developers.nest.com/products) page and selecting your Product.
## Discovery
The binding will discover all Nest Things from your account when you add and configure a "Nest Account" Thing.
See the Authorization paragraph above for details on how to obtain the Product ID, Product Secret and Pincode configuration parameters.
Once the binding has successfully authorized with the Nest API, it obtains an Access Token using the Pincode.
The configured Pincode is cleared because it can only be used once.
The obtained Access Token is saved as an advanced configuration parameter of the "Nest Account".
You can reuse an Access Token for authorization but not the Pincode.
A new Pincode can again be generated via the "Authorization URL" (see Authorization paragraph).
## Channels
### Account Channels
The account Thing Type does not have any channels.
### Camera Channels
**Camera group channels**
Information about the camera.
| Channel Type ID | Item Type | Description | Read Write |
|-----------------------|-----------|---------------------------------------------------|:----------:|
| app_url | String | The app URL to see the camera | R |
| audio_input_enabled | Switch | If the audio input is currently enabled | R |
| last_online_change | DateTime | Timestamp of the last online status change | R |
| public_share_enabled | Switch | If public sharing is currently enabled | R |
| public_share_url | String | The URL to see the public share of the camera | R |
| snapshot_url | String | The URL to use for a snapshot of the video stream | R |
| streaming | Switch | If the camera is currently streaming | R/W |
| video_history_enabled | Switch | If the video history is currently enabled | R |
| web_url | String | The web URL to see the camera | R |
**Last event group channels**
Information about the last camera event (requires Nest Aware subscription).
| Channel Type ID | Item Type | Description | Read Write |
|--------------------|-----------|------------------------------------------------------------------------------------|:----------:|
| activity_zones | String | Identifiers for activity zones that detected the event (comma separated) | R |
| animated_image_url | String | The URL showing an animated image for the camera event | R |
| app_url | String | The app URL for the camera event, allows you to see the camera event in an app | R |
| end_time | DateTime | Timestamp when the camera event ended | R |
| has_motion | Switch | If motion was detected in the camera event | R |
| has_person | Switch | If a person was detected in the camera event | R |
| has_sound | Switch | If sound was detected in the camera event | R |
| image_url | String | The URL showing an image for the camera event | R |
| start_time | DateTime | Timestamp when the camera event started | R |
| urls_expire_time | DateTime | Timestamp when the camera event URLs expire | R |
| web_url | String | The web URL for the camera event, allows you to see the camera event in a web page | R |
### Smoke Detector Channels
| Channel Type ID | Item Type | Description | Read Write |
|-----------------------|-----------|-----------------------------------------------------------------------------------|:----------:|
| co_alarm_state | String | The carbon monoxide alarm state of the Nest Protect (OK, EMERGENCY, WARNING) | R |
| last_connection | DateTime | Timestamp of the last successful interaction with Nest | R |
| last_manual_test_time | DateTime | Timestamp of the last successful manual test | R |
| low_battery | Switch | Reports whether the battery of the Nest protect is low (if it is battery powered) | R |
| manual_test_active | Switch | Manual test active at the moment | R |
| smoke_alarm_state | String | The smoke alarm state of the Nest Protect (OK, EMERGENCY, WARNING) | R |
| ui_color_state | String | The current color of the ring on the smoke detector (GRAY, GREEN, YELLOW, RED) | R |
### Structure Channels
| Channel Type ID | Item Type | Description | Read Write |
|------------------------------|-----------|--------------------------------------------------------------------------------------------------------|:----------:|
| away | String | Away state of the structure (HOME, AWAY) | R/W |
| country_code | String | Country code of the structure ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) | R |
| co_alarm_state | String | Carbon Monoxide alarm state (OK, EMERGENCY, WARNING) | R |
| eta_begin | DateTime | Estimated time of arrival at home, will setup the heat to turn on and be warm | R |
| peak_period_end_time | DateTime | Peak period end for the Rush Hour Rewards program | R |
| peak_period_start_time | DateTime | Peak period start for the Rush Hour Rewards program | R |
| postal_code | String | Postal code of the structure | R |
| rush_hour_rewards_enrollment | Switch | If rush hour rewards system is enabled or not | R |
| security_state | String | Security state of the structure (OK, DETER) | R |
| smoke_alarm_state | String | Smoke alarm state (OK, EMERGENCY, WARNING) | R |
| time_zone | String | The time zone for the structure ([IANA time zone format](https://www.iana.org/time-zones)) | R |
### Thermostat Channels
| Channel Type ID | Item Type | Description | Read Write |
|-----------------------------|----------------------|----------------------------------------------------------------------------------------|:----------:|
| can_cool | Switch | If the thermostat can actually turn on cooling | R |
| can_heat | Switch | If the thermostat can actually turn on heating | R |
| eco_max_set_point | Number:Temperature | The eco range max set point temperature | R |
| eco_min_set_point | Number:Temperature | The eco range min set point temperature | R |
| fan_timer_active | Switch | If the fan timer is engaged | R/W |
| fan_timer_duration | Number:Time | Length of time that the fan is set to run (15, 30, 45, 60, 120, 240, 480, 960 minutes) | R/W |
| fan_timer_timeout | DateTime | Timestamp when the fan stops running | R |
| has_fan | Switch | If the thermostat can control the fan | R |
| has_leaf | Switch | If the thermostat is currently in a leaf mode | R |
| humidity | Number:Dimensionless | Indicates the current relative humidity | R |
| last_connection | DateTime | Timestamp of the last successful interaction with Nest | R |
| locked | Switch | If the thermostat has the temperature locked to only be within a set range | R |
| locked_max_set_point | Number:Temperature | The locked range max set point temperature | R |
| locked_min_set_point | Number:Temperature | The locked range min set point temperature | R |
| max_set_point | Number:Temperature | The max set point temperature | R/W |
| min_set_point | Number:Temperature | The min set point temperature | R/W |
| mode | String | Current mode of the Nest thermostat (HEAT, COOL, HEAT_COOL, ECO, OFF) | R/W |
| previous_mode | String | The previous mode of the Nest thermostat (HEAT, COOL, HEAT_COOL, ECO, OFF) | R |
| state | String | The active state of the Nest thermostat (HEATING, COOLING, OFF) | R |
| temperature | Number:Temperature | Current temperature | R |
| time_to_target | Number:Time | Time left to the target temperature approximately | R |
| set_point | Number:Temperature | The set point temperature | R/W |
| sunlight_correction_active | Switch | If sunlight correction is active | R |
| sunlight_correction_enabled | Switch | If sunlight correction is enabled | R |
| using_emergency_heat | Switch | If the system is currently using emergency heat | R |
Note that the Nest API rounds Thermostat values so they will differ from what shows up in the Nest App.
The Nest API applies the following rounding:
* degrees Celsius to 0.5 degrees
* degrees Fahrenheit to whole degrees
* humidity to 5%
## Example
You can use the discovery functionality of the binding to obtain the deviceId and structureId values for defining Nest things in files.
Another way to get the deviceId and structureId values is by querying the Nest API yourself. First [obtain an Access Token](https://developers.nest.com/documentation/cloud/sample-code-auth) (or use the Access Token obtained by the binding).
Then use it with one of the [API Read Examples](https://developers.nest.com/documentation/cloud/how-to-read-data).
### demo.things:
```
Bridge nest:account:demo_account [ productId="8fdf9885-ca07-4252-1aa3-f3d5ca9589e0", productSecret="QITLR3iyUlWaj9dbvCxsCKp4f", accessToken="c.6rse1xtRk2UANErcY0XazaqPHgbvSSB6owOrbZrZ6IXrmqhsr9QTmcfaiLX1l0ULvlI5xLp01xmKeiojHqozLQbNM8yfITj1LSdK28zsUft1aKKH2mDlOeoqZKBdVIsxyZk4orH0AvKEZ5aY" ] {
camera fish_cam [ deviceId="qw0NNE8ruxA9AGJkTaFH3KeUiJaONWKiH9Gh3RwwhHClonIexTtufQ" ]
smoke_detector hallway_smoke [ deviceId="Tzvibaa3lLKnHpvpi9OQeCI_z5rfkBAV" ]
structure home [ structureId="20wKjydArmMV3kOluTA7JRcZg8HKBzTR-G_2nRXuIN1Bd6laGLOJQw" ]
thermostat living_thermostat [ deviceId="ZqAKzSv6TO6PjBnOCXf9LSI_z5rfkBAV" ]
}
```
### demo.items:
```
/* Camera */
String Cam_App_URL "App URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#app_url" }
Switch Cam_Audio_Input_Enabled "Audio Input Enabled" { channel="nest:camera:demo_account:fish_cam:camera#audio_input_enabled" }
DateTime Cam_Last_Online_Change "Last Online Change [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:camera#last_online_change" }
String Cam_Snapshot_URL "Snapshot URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#snapshot_url" }
Switch Cam_Streaming "Streaming" { channel="nest:camera:demo_account:fish_cam:camera#streaming" }
Switch Cam_Public_Share_Enabled "Public Share Enabled" { channel="nest:camera:demo_account:fish_cam:camera#public_share_enabled" }
String Cam_Public_Share_URL "Public Share URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#public_share_url" }
Switch Cam_Video_History_Enabled "Video History Enabled" { channel="nest:camera:demo_account:fish_cam:camera#video_history_enabled" }
String Cam_Web_URL "Web URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#web_url" }
String Cam_LE_Activity_Zones "Last Event Activity Zones [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#activity_zones" }
String Cam_LE_Animated_Image_URL "Last Event Animated Image URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#animated_image_url" }
String Cam_LE_App_URL "Last Event App URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#app_url" }
DateTime Cam_LE_End_Time "Last Event End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#end_time" }
Switch Cam_LE_Has_Motion "Last Event Has Motion" { channel="nest:camera:demo_account:fish_cam:last_event#has_motion" }
Switch Cam_LE_Has_Person "Last Event Has Person" { channel="nest:camera:demo_account:fish_cam:last_event#has_person" }
Switch Cam_LE_Has_Sound "Last Event Has Sound" { channel="nest:camera:demo_account:fish_cam:last_event#has_sound" }
String Cam_LE_Image_URL "Last Event Image URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#image_url" }
DateTime Cam_LE_Start_Time "Last Event Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#start_time" }
DateTime Cam_LE_URLs_Expire_Time "Last Event URLs Expire Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#urls_expire_time" }
String Cam_LE_Web_URL "Last Event Web URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#web_url" }
/* Smoke Detector */
String Smoke_CO_Alarm "CO Alarm [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:co_alarm_state" }
Switch Smoke_Battery_Low "Battery Low" { channel="nest:smoke_detector:demo_account:hallway_smoke:low_battery" }
Switch Smoke_Manual_Test "Manual Test" { channel="nest:smoke_detector:demo_account:hallway_smoke:manual_test_active" }
DateTime Smoke_Last_Connection "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:smoke_detector:demo_account:hallway_smoke:last_connection" }
DateTime Smoke_Last_Manual_Test "Last Manual Test [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:smoke_detector:demo_account:hallway_smoke:last_manual_test_time" }
String Smoke_Smoke_Alarm "Smoke Alarm [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:smoke_alarm_state" }
String Smoke_UI_Color "UI Color [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:ui_color_state" }
/* Thermostat */
Switch Thermostat_Can_Cool "Can Cool" { channel="nest:thermostat:demo_account:living_thermostat:can_cool" }
Switch Thermostat_Can_Heat "Can Heat" { channel="nest:thermostat:demo_account:living_thermostat:can_heat" }
Number:Temperature Therm_EMaxSP "Eco Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:eco_max_set_point" }
Number:Temperature Therm_EMinSP "Eco Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:eco_min_set_point" }
Switch Thermostat_FT_Active "Fan Timer Active" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_active" }
Number:Time Thermostat_FT_Duration "Fan Timer Duration [%d %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_duration" }
DateTime Thermostat_FT_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_timeout" }
Switch Thermostat_Has_Fan "Has Fan" { channel="nest:thermostat:demo_account:living_thermostat:has_fan" }
Switch Thermostat_Has_Leaf "Has Leaf" { channel="nest:thermostat:demo_account:living_thermostat:has_leaf" }
Number:Dimensionless Therm_Hum "Humidity [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:humidity" }
DateTime Thermostat_Last_Conn "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:thermostat:demo_account:living_thermostat:last_connection" }
Switch Thermostat_Locked "Locked" { channel="nest:thermostat:demo_account:living_thermostat:locked" }
Number:Temperature Therm_LMaxSP "Locked Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:locked_max_set_point" }
Number:Temperature Therm_LMinSP "Locked Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:locked_min_set_point" }
Number:Temperature Therm_Max_SP "Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:max_set_point" }
Number:Temperature Therm_Min_SP "Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:min_set_point" }
String Thermostat_Mode "Mode [%s]" { channel="nest:thermostat:demo_account:living_thermostat:mode" }
String Thermostat_Previous_Mode "Previous Mode [%s]" { channel="nest:thermostat:demo_account:living_thermostat:previous_mode" }
String Thermostat_State "State [%s]" { channel="nest:thermostat:demo_account:living_thermostat:state" }
Number:Temperature Thermostat_SP "Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:set_point" }
Switch Thermostat_Sunlight_CA "Sunlight Correction Active" { channel="nest:thermostat:demo_account:living_thermostat:sunlight_correction_active" }
Switch Thermostat_Sunlight_CE "Sunlight Correction Enabled" { channel="nest:thermostat:demo_account:living_thermostat:sunlight_correction_enabled" }
Number:Temperature Therm_Temp "Temperature [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:temperature" }
Number:Time Therm_Time_To_Target "Time To Target [%d %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:time_to_target" }
Switch Thermostat_Using_Em_Heat "Using Emergency Heat" { channel="nest:thermostat:demo_account:living_thermostat:using_emergency_heat" }
/* Structure */
String Home_Away "Away [%s]" { channel="nest:structure:demo_account:home:away" }
String Home_Country_Code "Country Code [%s]" { channel="nest:structure:demo_account:home:country_code" }
String Home_CO_Alarm_State "CO Alarm State [%s]" { channel="nest:structure:demo_account:home:co_alarm_state" }
DateTime Home_ETA "ETA [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:eta_begin" }
DateTime Home_PP_End_Time "PP End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:peak_period_end_time" }
DateTime Home_PP_Start_Time "PP Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:peak_period_start_time" }
String Home_Postal_Code "Postal Code [%s]" { channel="nest:structure:demo_account:home:postal_code" }
Switch Home_Rush_Hour_Rewards "Rush Hour Rewards" { channel="nest:structure:demo_account:home:rush_hour_rewards_enrollment" }
String Home_Security_State "Security State [%s]" { channel="nest:structure:demo_account:home:security_state" }
String Home_Smoke_Alarm_State "Smoke Alarm State [%s]" { channel="nest:structure:demo_account:home:smoke_alarm_state" }
String Home_Time_Zone "Time Zone [%s]" { channel="nest:structure:demo_account:home:time_zone" }
```
## Attribution
This documentation contains parts written by John Cocula which were copied from the 1.0 Nest binding.

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.nest</artifactId>
<name>openHAB Add-ons :: Bundles :: Nest Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.nest-${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-nest" description="Nest Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nest/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,148 @@
/**
* 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.nest.internal;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NestBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
public class NestBindingConstants {
public static final String BINDING_ID = "nest";
/** The URL to use to connect to Nest with. */
public static final String NEST_URL = "https://developer-api.nest.com";
/** The URL to get the access token when talking to Nest. */
public static final String NEST_ACCESS_TOKEN_URL = "https://api.home.nest.com/oauth2/access_token";
/** The path to set values on the thermostat when talking to Nest. */
public static final String NEST_THERMOSTAT_UPDATE_PATH = "/devices/thermostats/";
/** The path to set values on the structure when talking to Nest. */
public static final String NEST_STRUCTURE_UPDATE_PATH = "/structures/";
/** The path to set values on the camera when talking to Nest. */
public static final String NEST_CAMERA_UPDATE_PATH = "/devices/cameras/";
/** The path to set values on the camera when talking to Nest. */
public static final String NEST_SMOKE_ALARM_UPDATE_PATH = "/devices/smoke_co_alarms/";
/** The JSON content type used when talking to Nest. */
public static final String JSON_CONTENT_TYPE = "application/json";
/** To keep the streaming REST connection alive Nest sends every 30 seconds a message. */
public static final long KEEP_ALIVE_MILLIS = Duration.ofSeconds(30).toMillis();
/** To avoid API throttling errors (429 Too Many Requests) Nest recommends making at most one call per minute. */
public static final int MIN_SECONDS_BETWEEN_API_CALLS = 60;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "camera");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke_detector");
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_STRUCTURE = new ThingTypeUID(BINDING_ID, "structure");
// List of all channel group prefixes
public static final String CHANNEL_GROUP_CAMERA_PREFIX = "camera#";
public static final String CHANNEL_GROUP_LAST_EVENT_PREFIX = "last_event#";
// List of all Channel IDs
// read only channels (common)
public static final String CHANNEL_LAST_CONNECTION = "last_connection";
// read/write channels (thermostat)
public static final String CHANNEL_MODE = "mode";
public static final String CHANNEL_SET_POINT = "set_point";
public static final String CHANNEL_MAX_SET_POINT = "max_set_point";
public static final String CHANNEL_MIN_SET_POINT = "min_set_point";
public static final String CHANNEL_FAN_TIMER_ACTIVE = "fan_timer_active";
public static final String CHANNEL_FAN_TIMER_DURATION = "fan_timer_duration";
// read only channels (thermostat)
public static final String CHANNEL_ECO_MAX_SET_POINT = "eco_max_set_point";
public static final String CHANNEL_ECO_MIN_SET_POINT = "eco_min_set_point";
public static final String CHANNEL_LOCKED = "locked";
public static final String CHANNEL_LOCKED_MAX_SET_POINT = "locked_max_set_point";
public static final String CHANNEL_LOCKED_MIN_SET_POINT = "locked_min_set_point";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_PREVIOUS_MODE = "previous_mode";
public static final String CHANNEL_STATE = "state";
public static final String CHANNEL_CAN_HEAT = "can_heat";
public static final String CHANNEL_CAN_COOL = "can_cool";
public static final String CHANNEL_FAN_TIMER_TIMEOUT = "fan_timer_timeout";
public static final String CHANNEL_HAS_FAN = "has_fan";
public static final String CHANNEL_HAS_LEAF = "has_leaf";
public static final String CHANNEL_SUNLIGHT_CORRECTION_ENABLED = "sunlight_correction_enabled";
public static final String CHANNEL_SUNLIGHT_CORRECTION_ACTIVE = "sunlight_correction_active";
public static final String CHANNEL_TIME_TO_TARGET = "time_to_target";
public static final String CHANNEL_USING_EMERGENCY_HEAT = "using_emergency_heat";
// read/write channels (camera)
public static final String CHANNEL_CAMERA_STREAMING = "camera#streaming";
// read only channels (camera)
public static final String CHANNEL_CAMERA_AUDIO_INPUT_ENABLED = "camera#audio_input_enabled";
public static final String CHANNEL_CAMERA_VIDEO_HISTORY_ENABLED = "camera#video_history_enabled";
public static final String CHANNEL_CAMERA_WEB_URL = "camera#web_url";
public static final String CHANNEL_CAMERA_APP_URL = "camera#app_url";
public static final String CHANNEL_CAMERA_PUBLIC_SHARE_ENABLED = "camera#public_share_enabled";
public static final String CHANNEL_CAMERA_PUBLIC_SHARE_URL = "camera#public_share_url";
public static final String CHANNEL_CAMERA_SNAPSHOT_URL = "camera#snapshot_url";
public static final String CHANNEL_CAMERA_LAST_ONLINE_CHANGE = "camera#last_online_change";
public static final String CHANNEL_LAST_EVENT_HAS_SOUND = "last_event#has_sound";
public static final String CHANNEL_LAST_EVENT_HAS_MOTION = "last_event#has_motion";
public static final String CHANNEL_LAST_EVENT_HAS_PERSON = "last_event#has_person";
public static final String CHANNEL_LAST_EVENT_START_TIME = "last_event#start_time";
public static final String CHANNEL_LAST_EVENT_END_TIME = "last_event#end_time";
public static final String CHANNEL_LAST_EVENT_URLS_EXPIRE_TIME = "last_event#urls_expire_time";
public static final String CHANNEL_LAST_EVENT_WEB_URL = "last_event#web_url";
public static final String CHANNEL_LAST_EVENT_APP_URL = "last_event#app_url";
public static final String CHANNEL_LAST_EVENT_IMAGE_URL = "last_event#image_url";
public static final String CHANNEL_LAST_EVENT_ANIMATED_IMAGE_URL = "last_event#animated_image_url";
public static final String CHANNEL_LAST_EVENT_ACTIVITY_ZONES = "last_event#activity_zones";
// read/write channels (smoke detector)
// read only channels (smoke detector)
public static final String CHANNEL_UI_COLOR_STATE = "ui_color_state";
public static final String CHANNEL_LOW_BATTERY = "low_battery";
public static final String CHANNEL_CO_ALARM_STATE = "co_alarm_state"; // Also in structure
public static final String CHANNEL_SMOKE_ALARM_STATE = "smoke_alarm_state"; // Also in structure
public static final String CHANNEL_MANUAL_TEST_ACTIVE = "manual_test_active";
public static final String CHANNEL_LAST_MANUAL_TEST_TIME = "last_manual_test_time";
// read/write channel (structure)
public static final String CHANNEL_AWAY = "away";
// read only channels (structure)
public static final String CHANNEL_COUNTRY_CODE = "country_code";
public static final String CHANNEL_POSTAL_CODE = "postal_code";
public static final String CHANNEL_PEAK_PERIOD_START_TIME = "peak_period_start_time";
public static final String CHANNEL_PEAK_PERIOD_END_TIME = "peak_period_end_time";
public static final String CHANNEL_TIME_ZONE = "time_zone";
public static final String CHANNEL_ETA_BEGIN = "eta_begin";
public static final String CHANNEL_RUSH_HOUR_REWARDS_ENROLLMENT = "rush_hour_rewards_enrollment";
public static final String CHANNEL_SECURITY_STATE = "security_state";
}

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.nest.internal;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.discovery.NestDiscoveryService;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.handler.NestCameraHandler;
import org.openhab.binding.nest.internal.handler.NestSmokeDetectorHandler;
import org.openhab.binding.nest.internal.handler.NestStructureHandler;
import org.openhab.binding.nest.internal.handler.NestThermostatHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link NestHandlerFactory} is responsible for creating things and thing
* handlers. It also sets up the discovery service to track things from the bridge
* when the bridge is created.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest")
public class NestHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_THERMOSTAT,
THING_TYPE_CAMERA, THING_TYPE_BRIDGE, THING_TYPE_STRUCTURE, THING_TYPE_SMOKE_DETECTOR).collect(toSet());
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryService = new HashMap<>();
@Activate
public NestHandlerFactory(@Reference ClientBuilder clientBuilder,
@Reference SseEventSourceFactory eventSourceFactory) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
}
/**
* The things this factory supports creating.
*/
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Creates a handler for the specific thing. THis also creates the discovery service
* when the bridge is created.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_THERMOSTAT.equals(thingTypeUID)) {
return new NestThermostatHandler(thing);
}
if (THING_TYPE_CAMERA.equals(thingTypeUID)) {
return new NestCameraHandler(thing);
}
if (THING_TYPE_STRUCTURE.equals(thingTypeUID)) {
return new NestStructureHandler(thing);
}
if (THING_TYPE_SMOKE_DETECTOR.equals(thingTypeUID)) {
return new NestSmokeDetectorHandler(thing);
}
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
NestBridgeHandler handler = new NestBridgeHandler((Bridge) thing, clientBuilder, eventSourceFactory);
NestDiscoveryService service = new NestDiscoveryService(handler);
service.activate();
// Register the discovery service.
discoveryService.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), service, new Hashtable<>()));
return handler;
}
return null;
}
/**
* Removes the handler for the specific thing. This also handles disabling the discovery
* service when the bridge is removed.
*/
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NestBridgeHandler) {
ServiceRegistration<?> reg = discoveryService.get(thingHandler.getThing().getUID());
if (reg != null) {
// Unregister the discovery service.
NestDiscoveryService service = (NestDiscoveryService) bundleContext.getService(reg.getReference());
service.deactivate();
reg.unregister();
discoveryService.remove(thingHandler.getThing().getUID());
}
}
super.removeHandler(thingHandler);
}
}

View File

@ -0,0 +1,49 @@
/**
* 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.nest.internal;
import java.io.Reader;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Utility class for sharing utility methods between objects.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public final class NestUtils {
private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private NestUtils() {
// hidden utility class constructor
}
public static <T> T fromJson(String json, Class<T> dataClass) {
return GSON.fromJson(json, dataClass);
}
public static <T> T fromJson(Reader reader, Class<T> dataClass) {
return GSON.fromJson(reader, dataClass);
}
public static String toJson(Object object) {
return GSON.toJson(object);
}
}

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.nest.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The configuration for the Nest bridge, allowing it to talk to Nest.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
public class NestBridgeConfiguration {
public static final String PRODUCT_ID = "productId";
/** Product ID from the Nest product page. */
public String productId = "";
public static final String PRODUCT_SECRET = "productSecret";
/** Product secret from the Nest product page. */
public String productSecret = "";
public static final String PINCODE = "pincode";
/** Product pincode from the Nest authorization page. */
public @Nullable String pincode;
public static final String ACCESS_TOKEN = "accessToken";
/** The access token to use once retrieved from Nest. */
public @Nullable String accessToken;
}

View File

@ -0,0 +1,28 @@
/**
* 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.nest.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration for Nest devices.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Add device configuration to allow file based configuration
*/
@NonNullByDefault
public class NestDeviceConfiguration {
public static final String DEVICE_ID = "deviceId";
/** Device ID which can be retrieved with the Nest API. */
public String deviceId = "";
}

View File

@ -0,0 +1,28 @@
/**
* 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.nest.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration for structures.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Add device configuration to allow file based configuration
*/
@NonNullByDefault
public class NestStructureConfiguration {
public static final String STRUCTURE_ID = "structureId";
/** Structure ID which can be retrieved with the Nest API. */
public String structureId = "";
}

View File

@ -0,0 +1,79 @@
/**
* 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.nest.internal.data;
/**
* Deals with the access token data that comes back from Nest when it is requested.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class AccessTokenData {
private String accessToken;
private Long expiresIn;
public String getAccessToken() {
return accessToken;
}
public Long getExpiresIn() {
return expiresIn;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AccessTokenData other = (AccessTokenData) obj;
if (accessToken == null) {
if (other.accessToken != null) {
return false;
}
} else if (!accessToken.equals(other.accessToken)) {
return false;
}
if (expiresIn == null) {
if (other.expiresIn != null) {
return false;
}
} else if (!expiresIn.equals(other.expiresIn)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accessToken == null) ? 0 : accessToken.hashCode());
result = prime * result + ((expiresIn == null) ? 0 : expiresIn.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("AccessTokenData [accessToken=").append(accessToken).append(", expiresIn=").append(expiresIn)
.append("]");
return builder.toString();
}
}

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.nest.internal.data;
/**
* The data for a camera activity zone.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract ActivityZone object from Camera
*/
public class ActivityZone {
private String name;
private int id;
public String getName() {
return name;
}
public int getId() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ActivityZone other = (ActivityZone) obj;
if (id != other.id) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CameraActivityZone [name=").append(name).append(", id=").append(id).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,167 @@
/**
* 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.nest.internal.data;
import java.util.Date;
/**
* Default properties shared across all Nest devices.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class BaseNestDevice implements NestIdentifiable {
private String deviceId;
private String name;
private String nameLong;
private Date lastConnection;
private Boolean isOnline;
private String softwareVersion;
private String structureId;
private String whereId;
@Override
public String getId() {
return deviceId;
}
public String getName() {
return name;
}
public String getDeviceId() {
return deviceId;
}
public Date getLastConnection() {
return lastConnection;
}
public Boolean isOnline() {
return isOnline;
}
public String getNameLong() {
return nameLong;
}
public String getSoftwareVersion() {
return softwareVersion;
}
public String getStructureId() {
return structureId;
}
public String getWhereId() {
return whereId;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BaseNestDevice other = (BaseNestDevice) obj;
if (deviceId == null) {
if (other.deviceId != null) {
return false;
}
} else if (!deviceId.equals(other.deviceId)) {
return false;
}
if (isOnline == null) {
if (other.isOnline != null) {
return false;
}
} else if (!isOnline.equals(other.isOnline)) {
return false;
}
if (lastConnection == null) {
if (other.lastConnection != null) {
return false;
}
} else if (!lastConnection.equals(other.lastConnection)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (nameLong == null) {
if (other.nameLong != null) {
return false;
}
} else if (!nameLong.equals(other.nameLong)) {
return false;
}
if (softwareVersion == null) {
if (other.softwareVersion != null) {
return false;
}
} else if (!softwareVersion.equals(other.softwareVersion)) {
return false;
}
if (structureId == null) {
if (other.structureId != null) {
return false;
}
} else if (!structureId.equals(other.structureId)) {
return false;
}
if (whereId == null) {
if (other.whereId != null) {
return false;
}
} else if (!whereId.equals(other.whereId)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((deviceId == null) ? 0 : deviceId.hashCode());
result = prime * result + ((isOnline == null) ? 0 : isOnline.hashCode());
result = prime * result + ((lastConnection == null) ? 0 : lastConnection.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((nameLong == null) ? 0 : nameLong.hashCode());
result = prime * result + ((softwareVersion == null) ? 0 : softwareVersion.hashCode());
result = prime * result + ((structureId == null) ? 0 : structureId.hashCode());
result = prime * result + ((whereId == null) ? 0 : whereId.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("BaseNestDevice [deviceId=").append(deviceId).append(", name=").append(name)
.append(", nameLong=").append(nameLong).append(", lastConnection=").append(lastConnection)
.append(", isOnline=").append(isOnline).append(", softwareVersion=").append(softwareVersion)
.append(", structureId=").append(structureId).append(", whereId=").append(whereId).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,209 @@
/**
* 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.nest.internal.data;
import java.util.Date;
import java.util.List;
/**
* The data for the camera.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Camera extends BaseNestDevice {
private Boolean isStreaming;
private Boolean isAudioInputEnabled;
private Date lastIsOnlineChange;
private Boolean isVideoHistoryEnabled;
private String webUrl;
private String appUrl;
private Boolean isPublicShareEnabled;
private List<ActivityZone> activityZones;
private String publicShareUrl;
private String snapshotUrl;
private CameraEvent lastEvent;
public Boolean isStreaming() {
return isStreaming;
}
public Boolean isAudioInputEnabled() {
return isAudioInputEnabled;
}
public Date getLastIsOnlineChange() {
return lastIsOnlineChange;
}
public Boolean isVideoHistoryEnabled() {
return isVideoHistoryEnabled;
}
public String getWebUrl() {
return webUrl;
}
public String getAppUrl() {
return appUrl;
}
public Boolean isPublicShareEnabled() {
return isPublicShareEnabled;
}
public List<ActivityZone> getActivityZones() {
return activityZones;
}
public String getPublicShareUrl() {
return publicShareUrl;
}
public String getSnapshotUrl() {
return snapshotUrl;
}
public CameraEvent getLastEvent() {
return lastEvent;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Camera other = (Camera) obj;
if (activityZones == null) {
if (other.activityZones != null) {
return false;
}
} else if (!activityZones.equals(other.activityZones)) {
return false;
}
if (appUrl == null) {
if (other.appUrl != null) {
return false;
}
} else if (!appUrl.equals(other.appUrl)) {
return false;
}
if (isAudioInputEnabled == null) {
if (other.isAudioInputEnabled != null) {
return false;
}
} else if (!isAudioInputEnabled.equals(other.isAudioInputEnabled)) {
return false;
}
if (isPublicShareEnabled == null) {
if (other.isPublicShareEnabled != null) {
return false;
}
} else if (!isPublicShareEnabled.equals(other.isPublicShareEnabled)) {
return false;
}
if (isStreaming == null) {
if (other.isStreaming != null) {
return false;
}
} else if (!isStreaming.equals(other.isStreaming)) {
return false;
}
if (isVideoHistoryEnabled == null) {
if (other.isVideoHistoryEnabled != null) {
return false;
}
} else if (!isVideoHistoryEnabled.equals(other.isVideoHistoryEnabled)) {
return false;
}
if (lastEvent == null) {
if (other.lastEvent != null) {
return false;
}
} else if (!lastEvent.equals(other.lastEvent)) {
return false;
}
if (lastIsOnlineChange == null) {
if (other.lastIsOnlineChange != null) {
return false;
}
} else if (!lastIsOnlineChange.equals(other.lastIsOnlineChange)) {
return false;
}
if (publicShareUrl == null) {
if (other.publicShareUrl != null) {
return false;
}
} else if (!publicShareUrl.equals(other.publicShareUrl)) {
return false;
}
if (snapshotUrl == null) {
if (other.snapshotUrl != null) {
return false;
}
} else if (!snapshotUrl.equals(other.snapshotUrl)) {
return false;
}
if (webUrl == null) {
if (other.webUrl != null) {
return false;
}
} else if (!webUrl.equals(other.webUrl)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((activityZones == null) ? 0 : activityZones.hashCode());
result = prime * result + ((appUrl == null) ? 0 : appUrl.hashCode());
result = prime * result + ((isAudioInputEnabled == null) ? 0 : isAudioInputEnabled.hashCode());
result = prime * result + ((isPublicShareEnabled == null) ? 0 : isPublicShareEnabled.hashCode());
result = prime * result + ((isStreaming == null) ? 0 : isStreaming.hashCode());
result = prime * result + ((isVideoHistoryEnabled == null) ? 0 : isVideoHistoryEnabled.hashCode());
result = prime * result + ((lastEvent == null) ? 0 : lastEvent.hashCode());
result = prime * result + ((lastIsOnlineChange == null) ? 0 : lastIsOnlineChange.hashCode());
result = prime * result + ((publicShareUrl == null) ? 0 : publicShareUrl.hashCode());
result = prime * result + ((snapshotUrl == null) ? 0 : snapshotUrl.hashCode());
result = prime * result + ((webUrl == null) ? 0 : webUrl.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Camera [isStreaming=").append(isStreaming).append(", isAudioInputEnabled=")
.append(isAudioInputEnabled).append(", lastIsOnlineChange=").append(lastIsOnlineChange)
.append(", isVideoHistoryEnabled=").append(isVideoHistoryEnabled).append(", webUrl=").append(webUrl)
.append(", appUrl=").append(appUrl).append(", isPublicShareEnabled=").append(isPublicShareEnabled)
.append(", activityZones=").append(activityZones).append(", publicShareUrl=").append(publicShareUrl)
.append(", snapshotUrl=").append(snapshotUrl).append(", lastEvent=").append(lastEvent)
.append(", getId()=").append(getId()).append(", getName()=").append(getName())
.append(", getDeviceId()=").append(getDeviceId()).append(", getLastConnection()=")
.append(getLastConnection()).append(", isOnline()=").append(isOnline()).append(", getNameLong()=")
.append(getNameLong()).append(", getSoftwareVersion()=").append(getSoftwareVersion())
.append(", getStructureId()=").append(getStructureId()).append(", getWhereId()=").append(getWhereId())
.append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,203 @@
/**
* 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.nest.internal.data;
import java.util.Date;
import java.util.List;
/**
* The data for a camera event.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract CameraEvent object from Camera
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class CameraEvent {
private Boolean hasSound;
private Boolean hasMotion;
private Boolean hasPerson;
private Date startTime;
private Date endTime;
private Date urlsExpireTime;
private String webUrl;
private String appUrl;
private String imageUrl;
private String animatedImageUrl;
private List<String> activityZoneIds;
public Boolean isHasSound() {
return hasSound;
}
public Boolean isHasMotion() {
return hasMotion;
}
public Boolean isHasPerson() {
return hasPerson;
}
public Date getStartTime() {
return startTime;
}
public Date getEndTime() {
return endTime;
}
public Date getUrlsExpireTime() {
return urlsExpireTime;
}
public String getWebUrl() {
return webUrl;
}
public String getAppUrl() {
return appUrl;
}
public String getImageUrl() {
return imageUrl;
}
public String getAnimatedImageUrl() {
return animatedImageUrl;
}
public List<String> getActivityZones() {
return activityZoneIds;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CameraEvent other = (CameraEvent) obj;
if (activityZoneIds == null) {
if (other.activityZoneIds != null) {
return false;
}
} else if (!activityZoneIds.equals(other.activityZoneIds)) {
return false;
}
if (animatedImageUrl == null) {
if (other.animatedImageUrl != null) {
return false;
}
} else if (!animatedImageUrl.equals(other.animatedImageUrl)) {
return false;
}
if (appUrl == null) {
if (other.appUrl != null) {
return false;
}
} else if (!appUrl.equals(other.appUrl)) {
return false;
}
if (endTime == null) {
if (other.endTime != null) {
return false;
}
} else if (!endTime.equals(other.endTime)) {
return false;
}
if (hasMotion == null) {
if (other.hasMotion != null) {
return false;
}
} else if (!hasMotion.equals(other.hasMotion)) {
return false;
}
if (hasPerson == null) {
if (other.hasPerson != null) {
return false;
}
} else if (!hasPerson.equals(other.hasPerson)) {
return false;
}
if (hasSound == null) {
if (other.hasSound != null) {
return false;
}
} else if (!hasSound.equals(other.hasSound)) {
return false;
}
if (imageUrl == null) {
if (other.imageUrl != null) {
return false;
}
} else if (!imageUrl.equals(other.imageUrl)) {
return false;
}
if (startTime == null) {
if (other.startTime != null) {
return false;
}
} else if (!startTime.equals(other.startTime)) {
return false;
}
if (urlsExpireTime == null) {
if (other.urlsExpireTime != null) {
return false;
}
} else if (!urlsExpireTime.equals(other.urlsExpireTime)) {
return false;
}
if (webUrl == null) {
if (other.webUrl != null) {
return false;
}
} else if (!webUrl.equals(other.webUrl)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((activityZoneIds == null) ? 0 : activityZoneIds.hashCode());
result = prime * result + ((animatedImageUrl == null) ? 0 : animatedImageUrl.hashCode());
result = prime * result + ((appUrl == null) ? 0 : appUrl.hashCode());
result = prime * result + ((endTime == null) ? 0 : endTime.hashCode());
result = prime * result + ((hasMotion == null) ? 0 : hasMotion.hashCode());
result = prime * result + ((hasPerson == null) ? 0 : hasPerson.hashCode());
result = prime * result + ((hasSound == null) ? 0 : hasSound.hashCode());
result = prime * result + ((imageUrl == null) ? 0 : imageUrl.hashCode());
result = prime * result + ((startTime == null) ? 0 : startTime.hashCode());
result = prime * result + ((urlsExpireTime == null) ? 0 : urlsExpireTime.hashCode());
result = prime * result + ((webUrl == null) ? 0 : webUrl.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Event [hasSound=").append(hasSound).append(", hasMotion=").append(hasMotion)
.append(", hasPerson=").append(hasPerson).append(", startTime=").append(startTime).append(", endTime=")
.append(endTime).append(", urlsExpireTime=").append(urlsExpireTime).append(", webUrl=").append(webUrl)
.append(", appUrl=").append(appUrl).append(", imageUrl=").append(imageUrl).append(", animatedImageUrl=")
.append(animatedImageUrl).append(", activityZoneIds=").append(activityZoneIds).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.nest.internal.data;
import java.util.Date;
/**
* Used to set and update the ETA values for Nest.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract ETA object from Structure
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class ETA {
private String tripId;
private Date estimatedArrivalWindowBegin;
private Date estimatedArrivalWindowEnd;
public String getTripId() {
return tripId;
}
public void setTripId(String tripId) {
this.tripId = tripId;
}
public Date getEstimatedArrivalWindowBegin() {
return estimatedArrivalWindowBegin;
}
public void setEstimatedArrivalWindowBegin(Date estimatedArrivalWindowBegin) {
this.estimatedArrivalWindowBegin = estimatedArrivalWindowBegin;
}
public Date getEstimatedArrivalWindowEnd() {
return estimatedArrivalWindowEnd;
}
public void setEstimatedArrivalWindowEnd(Date estimatedArrivalWindowEnd) {
this.estimatedArrivalWindowEnd = estimatedArrivalWindowEnd;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ETA other = (ETA) obj;
if (estimatedArrivalWindowBegin == null) {
if (other.estimatedArrivalWindowBegin != null) {
return false;
}
} else if (!estimatedArrivalWindowBegin.equals(other.estimatedArrivalWindowBegin)) {
return false;
}
if (estimatedArrivalWindowEnd == null) {
if (other.estimatedArrivalWindowEnd != null) {
return false;
}
} else if (!estimatedArrivalWindowEnd.equals(other.estimatedArrivalWindowEnd)) {
return false;
}
if (tripId == null) {
if (other.tripId != null) {
return false;
}
} else if (!tripId.equals(other.tripId)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((estimatedArrivalWindowBegin == null) ? 0 : estimatedArrivalWindowBegin.hashCode());
result = prime * result + ((estimatedArrivalWindowEnd == null) ? 0 : estimatedArrivalWindowEnd.hashCode());
result = prime * result + ((tripId == null) ? 0 : tripId.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ETA [tripId=").append(tripId).append(", estimatedArrivalWindowBegin=")
.append(estimatedArrivalWindowBegin).append(", estimatedArrivalWindowEnd=")
.append(estimatedArrivalWindowEnd).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,106 @@
/**
* 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.nest.internal.data;
/**
* The data of Nest API errors.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling
* @author Wouter Born - Add equals and hashCode methods
*/
public class ErrorData {
private String error;
private String type;
private String message;
private String instance;
public String getError() {
return error;
}
public String getType() {
return type;
}
public String getMessage() {
return message;
}
public String getInstance() {
return instance;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ErrorData other = (ErrorData) obj;
if (error == null) {
if (other.error != null) {
return false;
}
} else if (!error.equals(other.error)) {
return false;
}
if (instance == null) {
if (other.instance != null) {
return false;
}
} else if (!instance.equals(other.instance)) {
return false;
}
if (message == null) {
if (other.message != null) {
return false;
}
} else if (!message.equals(other.message)) {
return false;
}
if (type == null) {
if (other.type != null) {
return false;
}
} else if (!type.equals(other.type)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((error == null) ? 0 : error.hashCode());
result = prime * result + ((instance == null) ? 0 : instance.hashCode());
result = prime * result + ((message == null) ? 0 : message.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ErrorData [error=").append(error).append(", type=").append(type).append(", message=")
.append(message).append(", instance=").append(instance).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,96 @@
/**
* 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.nest.internal.data;
import java.util.Map;
/**
* All the Nest devices broken up by type.
*
* @author David Bennett - Initial contribution
*/
public class NestDevices {
private Map<String, Thermostat> thermostats;
private Map<String, SmokeDetector> smokeCoAlarms;
private Map<String, Camera> cameras;
/** Id to thermostat mapping */
public Map<String, Thermostat> getThermostats() {
return thermostats;
}
/** Id to camera mapping */
public Map<String, Camera> getCameras() {
return cameras;
}
/** Id to smoke detector */
public Map<String, SmokeDetector> getSmokeCoAlarms() {
return smokeCoAlarms;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
NestDevices other = (NestDevices) obj;
if (cameras == null) {
if (other.cameras != null) {
return false;
}
} else if (!cameras.equals(other.cameras)) {
return false;
}
if (smokeCoAlarms == null) {
if (other.smokeCoAlarms != null) {
return false;
}
} else if (!smokeCoAlarms.equals(other.smokeCoAlarms)) {
return false;
}
if (thermostats == null) {
if (other.thermostats != null) {
return false;
}
} else if (!thermostats.equals(other.thermostats)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((cameras == null) ? 0 : cameras.hashCode());
result = prime * result + ((smokeCoAlarms == null) ? 0 : smokeCoAlarms.hashCode());
result = prime * result + ((thermostats == null) ? 0 : thermostats.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("NestDevices [thermostats=").append(thermostats).append(", smokeCoAlarms=").append(smokeCoAlarms)
.append(", cameras=").append(cameras).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
/**
* Interface for uniquely identifiable Nest objects (device or a structure).
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Simplify working with deviceId and structureId
*/
public interface NestIdentifiable {
/**
* Returns the identifier that uniquely identifies the Nest object (deviceId or structureId).
*/
String getId();
}

View File

@ -0,0 +1,79 @@
/**
* 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.nest.internal.data;
/**
* The meta data in the data downloads from Nest.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class NestMetadata {
private String accessToken;
private String clientVersion;
public String getAccessToken() {
return accessToken;
}
public String getClientVersion() {
return clientVersion;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
NestMetadata other = (NestMetadata) obj;
if (accessToken == null) {
if (other.accessToken != null) {
return false;
}
} else if (!accessToken.equals(other.accessToken)) {
return false;
}
if (clientVersion == null) {
if (other.clientVersion != null) {
return false;
}
} else if (!clientVersion.equals(other.clientVersion)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accessToken == null) ? 0 : accessToken.hashCode());
result = prime * result + ((clientVersion == null) ? 0 : clientVersion.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("NestMetadata [accessToken=").append(accessToken).append(", clientVersion=")
.append(clientVersion).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* Data for the Nest smoke detector.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class SmokeDetector extends BaseNestDevice {
private BatteryHealth batteryHealth;
private AlarmState coAlarmState;
private Date lastManualTestTime;
private AlarmState smokeAlarmState;
private Boolean isManualTestActive;
private UiColorState uiColorState;
public UiColorState getUiColorState() {
return uiColorState;
}
public BatteryHealth getBatteryHealth() {
return batteryHealth;
}
public AlarmState getCoAlarmState() {
return coAlarmState;
}
public Date getLastManualTestTime() {
return lastManualTestTime;
}
public AlarmState getSmokeAlarmState() {
return smokeAlarmState;
}
public Boolean isManualTestActive() {
return isManualTestActive;
}
public enum BatteryHealth {
@SerializedName("ok")
OK,
@SerializedName("replace")
REPLACE
}
public enum AlarmState {
@SerializedName("ok")
OK,
@SerializedName("emergency")
EMERGENCY,
@SerializedName("warning")
WARNING
}
public enum UiColorState {
@SerializedName("gray")
GRAY,
@SerializedName("green")
GREEN,
@SerializedName("yellow")
YELLOW,
@SerializedName("red")
RED
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SmokeDetector other = (SmokeDetector) obj;
if (batteryHealth != other.batteryHealth) {
return false;
}
if (coAlarmState != other.coAlarmState) {
return false;
}
if (isManualTestActive == null) {
if (other.isManualTestActive != null) {
return false;
}
} else if (!isManualTestActive.equals(other.isManualTestActive)) {
return false;
}
if (lastManualTestTime == null) {
if (other.lastManualTestTime != null) {
return false;
}
} else if (!lastManualTestTime.equals(other.lastManualTestTime)) {
return false;
}
if (smokeAlarmState != other.smokeAlarmState) {
return false;
}
if (uiColorState != other.uiColorState) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((batteryHealth == null) ? 0 : batteryHealth.hashCode());
result = prime * result + ((coAlarmState == null) ? 0 : coAlarmState.hashCode());
result = prime * result + ((isManualTestActive == null) ? 0 : isManualTestActive.hashCode());
result = prime * result + ((lastManualTestTime == null) ? 0 : lastManualTestTime.hashCode());
result = prime * result + ((smokeAlarmState == null) ? 0 : smokeAlarmState.hashCode());
result = prime * result + ((uiColorState == null) ? 0 : uiColorState.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("SmokeDetector [batteryHealth=").append(batteryHealth).append(", coAlarmState=")
.append(coAlarmState).append(", lastManualTestTime=").append(lastManualTestTime)
.append(", smokeAlarmState=").append(smokeAlarmState).append(", isManualTestActive=")
.append(isManualTestActive).append(", uiColorState=").append(uiColorState).append(", getId()=")
.append(getId()).append(", getName()=").append(getName()).append(", getDeviceId()=")
.append(getDeviceId()).append(", getLastConnection()=").append(getLastConnection())
.append(", isOnline()=").append(isOnline()).append(", getNameLong()=").append(getNameLong())
.append(", getSoftwareVersion()=").append(getSoftwareVersion()).append(", getStructureId()=")
.append(getStructureId()).append(", getWhereId()=").append(getWhereId()).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,311 @@
/**
* 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.nest.internal.data;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.openhab.binding.nest.internal.data.SmokeDetector.AlarmState;
import com.google.gson.annotations.SerializedName;
/**
* The structure details from Nest.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Structure implements NestIdentifiable {
private String structureId;
private List<String> thermostats;
private List<String> smokeCoAlarms;
private List<String> cameras;
private String countryCode;
private String postalCode;
private Date peakPeriodStartTime;
private Date peakPeriodEndTime;
private String timeZone;
private Date etaBegin;
private SmokeDetector.AlarmState coAlarmState;
private SmokeDetector.AlarmState smokeAlarmState;
private Boolean rhrEnrollment;
private Map<String, Where> wheres;
private HomeAwayState away;
private String name;
private ETA eta;
private SecurityState wwnSecurityState;
@Override
public String getId() {
return structureId;
}
public HomeAwayState getAway() {
return away;
}
public void setAway(HomeAwayState away) {
this.away = away;
}
public String getStructureId() {
return structureId;
}
public List<String> getThermostats() {
return thermostats;
}
public List<String> getSmokeCoAlarms() {
return smokeCoAlarms;
}
public List<String> getCameras() {
return cameras;
}
public String getCountryCode() {
return countryCode;
}
public String getPostalCode() {
return postalCode;
}
public Date getPeakPeriodStartTime() {
return peakPeriodStartTime;
}
public Date getPeakPeriodEndTime() {
return peakPeriodEndTime;
}
public String getTimeZone() {
return timeZone;
}
public Date getEtaBegin() {
return etaBegin;
}
public AlarmState getCoAlarmState() {
return coAlarmState;
}
public AlarmState getSmokeAlarmState() {
return smokeAlarmState;
}
public Boolean isRhrEnrollment() {
return rhrEnrollment;
}
public Map<String, Where> getWheres() {
return wheres;
}
public ETA getEta() {
return eta;
}
public String getName() {
return name;
}
public SecurityState getWwnSecurityState() {
return wwnSecurityState;
}
public enum HomeAwayState {
@SerializedName("home")
HOME,
@SerializedName("away")
AWAY,
@SerializedName("unknown")
UNKNOWN
}
public enum SecurityState {
@SerializedName("ok")
OK,
@SerializedName("deter")
DETER
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Structure other = (Structure) obj;
if (away != other.away) {
return false;
}
if (cameras == null) {
if (other.cameras != null) {
return false;
}
} else if (!cameras.equals(other.cameras)) {
return false;
}
if (coAlarmState != other.coAlarmState) {
return false;
}
if (countryCode == null) {
if (other.countryCode != null) {
return false;
}
} else if (!countryCode.equals(other.countryCode)) {
return false;
}
if (eta == null) {
if (other.eta != null) {
return false;
}
} else if (!eta.equals(other.eta)) {
return false;
}
if (etaBegin == null) {
if (other.etaBegin != null) {
return false;
}
} else if (!etaBegin.equals(other.etaBegin)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (peakPeriodEndTime == null) {
if (other.peakPeriodEndTime != null) {
return false;
}
} else if (!peakPeriodEndTime.equals(other.peakPeriodEndTime)) {
return false;
}
if (peakPeriodStartTime == null) {
if (other.peakPeriodStartTime != null) {
return false;
}
} else if (!peakPeriodStartTime.equals(other.peakPeriodStartTime)) {
return false;
}
if (postalCode == null) {
if (other.postalCode != null) {
return false;
}
} else if (!postalCode.equals(other.postalCode)) {
return false;
}
if (rhrEnrollment == null) {
if (other.rhrEnrollment != null) {
return false;
}
} else if (!rhrEnrollment.equals(other.rhrEnrollment)) {
return false;
}
if (smokeAlarmState != other.smokeAlarmState) {
return false;
}
if (smokeCoAlarms == null) {
if (other.smokeCoAlarms != null) {
return false;
}
} else if (!smokeCoAlarms.equals(other.smokeCoAlarms)) {
return false;
}
if (structureId == null) {
if (other.structureId != null) {
return false;
}
} else if (!structureId.equals(other.structureId)) {
return false;
}
if (thermostats == null) {
if (other.thermostats != null) {
return false;
}
} else if (!thermostats.equals(other.thermostats)) {
return false;
}
if (timeZone == null) {
if (other.timeZone != null) {
return false;
}
} else if (!timeZone.equals(other.timeZone)) {
return false;
}
if (wheres == null) {
if (other.wheres != null) {
return false;
}
} else if (!wheres.equals(other.wheres)) {
return false;
}
if (wwnSecurityState != other.wwnSecurityState) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((away == null) ? 0 : away.hashCode());
result = prime * result + ((cameras == null) ? 0 : cameras.hashCode());
result = prime * result + ((coAlarmState == null) ? 0 : coAlarmState.hashCode());
result = prime * result + ((countryCode == null) ? 0 : countryCode.hashCode());
result = prime * result + ((eta == null) ? 0 : eta.hashCode());
result = prime * result + ((etaBegin == null) ? 0 : etaBegin.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((peakPeriodEndTime == null) ? 0 : peakPeriodEndTime.hashCode());
result = prime * result + ((peakPeriodStartTime == null) ? 0 : peakPeriodStartTime.hashCode());
result = prime * result + ((postalCode == null) ? 0 : postalCode.hashCode());
result = prime * result + ((rhrEnrollment == null) ? 0 : rhrEnrollment.hashCode());
result = prime * result + ((smokeAlarmState == null) ? 0 : smokeAlarmState.hashCode());
result = prime * result + ((smokeCoAlarms == null) ? 0 : smokeCoAlarms.hashCode());
result = prime * result + ((structureId == null) ? 0 : structureId.hashCode());
result = prime * result + ((thermostats == null) ? 0 : thermostats.hashCode());
result = prime * result + ((timeZone == null) ? 0 : timeZone.hashCode());
result = prime * result + ((wheres == null) ? 0 : wheres.hashCode());
result = prime * result + ((wwnSecurityState == null) ? 0 : wwnSecurityState.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Structure [structureId=").append(structureId).append(", thermostats=").append(thermostats)
.append(", smokeCoAlarms=").append(smokeCoAlarms).append(", cameras=").append(cameras)
.append(", countryCode=").append(countryCode).append(", postalCode=").append(postalCode)
.append(", peakPeriodStartTime=").append(peakPeriodStartTime).append(", peakPeriodEndTime=")
.append(peakPeriodEndTime).append(", timeZone=").append(timeZone).append(", etaBegin=").append(etaBegin)
.append(", coAlarmState=").append(coAlarmState).append(", smokeAlarmState=").append(smokeAlarmState)
.append(", rhrEnrollment=").append(rhrEnrollment).append(", wheres=").append(wheres).append(", away=")
.append(away).append(", name=").append(name).append(", eta=").append(eta).append(", wwnSecurityState=")
.append(wwnSecurityState).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,572 @@
/**
* 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.nest.internal.data;
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import java.util.Date;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import com.google.gson.annotations.SerializedName;
/**
* Gson class to encapsulate the data for the Nest thermostat.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Thermostat extends BaseNestDevice {
private Boolean canCool;
private Boolean canHeat;
private Boolean isUsingEmergencyHeat;
private Boolean hasFan;
private Boolean fanTimerActive;
private Date fanTimerTimeout;
private Boolean hasLeaf;
private String temperatureScale;
private Double ambientTemperatureC;
private Double ambientTemperatureF;
private Integer humidity;
private Double targetTemperatureC;
private Double targetTemperatureF;
private Double targetTemperatureHighC;
private Double targetTemperatureHighF;
private Double targetTemperatureLowC;
private Double targetTemperatureLowF;
private Mode hvacMode;
private Mode previousHvacMode;
private State hvacState;
private Double ecoTemperatureHighC;
private Double ecoTemperatureHighF;
private Double ecoTemperatureLowC;
private Double ecoTemperatureLowF;
private Boolean isLocked;
private Double lockedTempMaxC;
private Double lockedTempMaxF;
private Double lockedTempMinC;
private Double lockedTempMinF;
private Boolean sunlightCorrectionEnabled;
private Boolean sunlightCorrectionActive;
private Integer fanTimerDuration;
private String timeToTarget;
private String whereName;
public Unit<Temperature> getTemperatureUnit() {
if ("C".equals(temperatureScale)) {
return CELSIUS;
} else if ("F".equals(temperatureScale)) {
return FAHRENHEIT;
} else {
return null;
}
}
public Double getTargetTemperature() {
if (getTemperatureUnit() == CELSIUS) {
return targetTemperatureC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return targetTemperatureF;
} else {
return null;
}
}
public Double getTargetTemperatureHigh() {
if (getTemperatureUnit() == CELSIUS) {
return targetTemperatureHighC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return targetTemperatureHighF;
} else {
return null;
}
}
public Double getTargetTemperatureLow() {
if (getTemperatureUnit() == CELSIUS) {
return targetTemperatureLowC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return targetTemperatureLowF;
} else {
return null;
}
}
public Mode getMode() {
return hvacMode;
}
public Double getEcoTemperatureHigh() {
if (getTemperatureUnit() == CELSIUS) {
return ecoTemperatureHighC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return ecoTemperatureHighF;
} else {
return null;
}
}
public Double getEcoTemperatureLow() {
if (getTemperatureUnit() == CELSIUS) {
return ecoTemperatureLowC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return ecoTemperatureLowF;
} else {
return null;
}
}
public Boolean isLocked() {
return isLocked;
}
public Double getLockedTempMax() {
if (getTemperatureUnit() == CELSIUS) {
return lockedTempMaxC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return lockedTempMaxF;
} else {
return null;
}
}
public Double getLockedTempMin() {
if (getTemperatureUnit() == CELSIUS) {
return lockedTempMinC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return lockedTempMinF;
} else {
return null;
}
}
public Boolean isCanCool() {
return canCool;
}
public Boolean isCanHeat() {
return canHeat;
}
public Boolean isUsingEmergencyHeat() {
return isUsingEmergencyHeat;
}
public Boolean isHasFan() {
return hasFan;
}
public Boolean isFanTimerActive() {
return fanTimerActive;
}
public Date getFanTimerTimeout() {
return fanTimerTimeout;
}
public Boolean isHasLeaf() {
return hasLeaf;
}
public Mode getPreviousHvacMode() {
return previousHvacMode;
}
public State getHvacState() {
return hvacState;
}
public Boolean isSunlightCorrectionEnabled() {
return sunlightCorrectionEnabled;
}
public Boolean isSunlightCorrectionActive() {
return sunlightCorrectionActive;
}
public Integer getFanTimerDuration() {
return fanTimerDuration;
}
public Integer getTimeToTarget() {
return parseTimeToTarget(timeToTarget);
}
/*
* Turns the time to target string into a real value.
*/
static Integer parseTimeToTarget(String timeToTarget) {
if (timeToTarget == null) {
return null;
} else if (timeToTarget.startsWith("~") || timeToTarget.startsWith("<") || timeToTarget.startsWith(">")) {
return Integer.valueOf(timeToTarget.substring(1));
}
return Integer.valueOf(timeToTarget);
}
public String getWhereName() {
return whereName;
}
public Double getAmbientTemperature() {
if (getTemperatureUnit() == CELSIUS) {
return ambientTemperatureC;
} else if (getTemperatureUnit() == FAHRENHEIT) {
return ambientTemperatureF;
} else {
return null;
}
}
public Integer getHumidity() {
return humidity;
}
public enum Mode {
@SerializedName("heat")
HEAT,
@SerializedName("cool")
COOL,
@SerializedName("heat-cool")
HEAT_COOL,
@SerializedName("eco")
ECO,
@SerializedName("off")
OFF
}
public enum State {
@SerializedName("heating")
HEATING,
@SerializedName("cooling")
COOLING,
@SerializedName("off")
OFF
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Thermostat other = (Thermostat) obj;
if (ambientTemperatureC == null) {
if (other.ambientTemperatureC != null) {
return false;
}
} else if (!ambientTemperatureC.equals(other.ambientTemperatureC)) {
return false;
}
if (ambientTemperatureF == null) {
if (other.ambientTemperatureF != null) {
return false;
}
} else if (!ambientTemperatureF.equals(other.ambientTemperatureF)) {
return false;
}
if (canCool == null) {
if (other.canCool != null) {
return false;
}
} else if (!canCool.equals(other.canCool)) {
return false;
}
if (canHeat == null) {
if (other.canHeat != null) {
return false;
}
} else if (!canHeat.equals(other.canHeat)) {
return false;
}
if (ecoTemperatureHighC == null) {
if (other.ecoTemperatureHighC != null) {
return false;
}
} else if (!ecoTemperatureHighC.equals(other.ecoTemperatureHighC)) {
return false;
}
if (ecoTemperatureHighF == null) {
if (other.ecoTemperatureHighF != null) {
return false;
}
} else if (!ecoTemperatureHighF.equals(other.ecoTemperatureHighF)) {
return false;
}
if (ecoTemperatureLowC == null) {
if (other.ecoTemperatureLowC != null) {
return false;
}
} else if (!ecoTemperatureLowC.equals(other.ecoTemperatureLowC)) {
return false;
}
if (ecoTemperatureLowF == null) {
if (other.ecoTemperatureLowF != null) {
return false;
}
} else if (!ecoTemperatureLowF.equals(other.ecoTemperatureLowF)) {
return false;
}
if (fanTimerActive == null) {
if (other.fanTimerActive != null) {
return false;
}
} else if (!fanTimerActive.equals(other.fanTimerActive)) {
return false;
}
if (fanTimerDuration == null) {
if (other.fanTimerDuration != null) {
return false;
}
} else if (!fanTimerDuration.equals(other.fanTimerDuration)) {
return false;
}
if (fanTimerTimeout == null) {
if (other.fanTimerTimeout != null) {
return false;
}
} else if (!fanTimerTimeout.equals(other.fanTimerTimeout)) {
return false;
}
if (hasFan == null) {
if (other.hasFan != null) {
return false;
}
} else if (!hasFan.equals(other.hasFan)) {
return false;
}
if (hasLeaf == null) {
if (other.hasLeaf != null) {
return false;
}
} else if (!hasLeaf.equals(other.hasLeaf)) {
return false;
}
if (humidity == null) {
if (other.humidity != null) {
return false;
}
} else if (!humidity.equals(other.humidity)) {
return false;
}
if (hvacMode != other.hvacMode) {
return false;
}
if (hvacState != other.hvacState) {
return false;
}
if (isLocked == null) {
if (other.isLocked != null) {
return false;
}
} else if (!isLocked.equals(other.isLocked)) {
return false;
}
if (isUsingEmergencyHeat == null) {
if (other.isUsingEmergencyHeat != null) {
return false;
}
} else if (!isUsingEmergencyHeat.equals(other.isUsingEmergencyHeat)) {
return false;
}
if (lockedTempMaxC == null) {
if (other.lockedTempMaxC != null) {
return false;
}
} else if (!lockedTempMaxC.equals(other.lockedTempMaxC)) {
return false;
}
if (lockedTempMaxF == null) {
if (other.lockedTempMaxF != null) {
return false;
}
} else if (!lockedTempMaxF.equals(other.lockedTempMaxF)) {
return false;
}
if (lockedTempMinC == null) {
if (other.lockedTempMinC != null) {
return false;
}
} else if (!lockedTempMinC.equals(other.lockedTempMinC)) {
return false;
}
if (lockedTempMinF == null) {
if (other.lockedTempMinF != null) {
return false;
}
} else if (!lockedTempMinF.equals(other.lockedTempMinF)) {
return false;
}
if (previousHvacMode != other.previousHvacMode) {
return false;
}
if (sunlightCorrectionActive == null) {
if (other.sunlightCorrectionActive != null) {
return false;
}
} else if (!sunlightCorrectionActive.equals(other.sunlightCorrectionActive)) {
return false;
}
if (sunlightCorrectionEnabled == null) {
if (other.sunlightCorrectionEnabled != null) {
return false;
}
} else if (!sunlightCorrectionEnabled.equals(other.sunlightCorrectionEnabled)) {
return false;
}
if (targetTemperatureC == null) {
if (other.targetTemperatureC != null) {
return false;
}
} else if (!targetTemperatureC.equals(other.targetTemperatureC)) {
return false;
}
if (targetTemperatureF == null) {
if (other.targetTemperatureF != null) {
return false;
}
} else if (!targetTemperatureF.equals(other.targetTemperatureF)) {
return false;
}
if (targetTemperatureHighC == null) {
if (other.targetTemperatureHighC != null) {
return false;
}
} else if (!targetTemperatureHighC.equals(other.targetTemperatureHighC)) {
return false;
}
if (targetTemperatureHighF == null) {
if (other.targetTemperatureHighF != null) {
return false;
}
} else if (!targetTemperatureHighF.equals(other.targetTemperatureHighF)) {
return false;
}
if (targetTemperatureLowC == null) {
if (other.targetTemperatureLowC != null) {
return false;
}
} else if (!targetTemperatureLowC.equals(other.targetTemperatureLowC)) {
return false;
}
if (targetTemperatureLowF == null) {
if (other.targetTemperatureLowF != null) {
return false;
}
} else if (!targetTemperatureLowF.equals(other.targetTemperatureLowF)) {
return false;
}
if (temperatureScale == null) {
if (other.temperatureScale != null) {
return false;
}
} else if (!temperatureScale.equals(other.temperatureScale)) {
return false;
}
if (timeToTarget == null) {
if (other.timeToTarget != null) {
return false;
}
} else if (!timeToTarget.equals(other.timeToTarget)) {
return false;
}
if (whereName == null) {
if (other.whereName != null) {
return false;
}
} else if (!whereName.equals(other.whereName)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((ambientTemperatureC == null) ? 0 : ambientTemperatureC.hashCode());
result = prime * result + ((ambientTemperatureF == null) ? 0 : ambientTemperatureF.hashCode());
result = prime * result + ((canCool == null) ? 0 : canCool.hashCode());
result = prime * result + ((canHeat == null) ? 0 : canHeat.hashCode());
result = prime * result + ((ecoTemperatureHighC == null) ? 0 : ecoTemperatureHighC.hashCode());
result = prime * result + ((ecoTemperatureHighF == null) ? 0 : ecoTemperatureHighF.hashCode());
result = prime * result + ((ecoTemperatureLowC == null) ? 0 : ecoTemperatureLowC.hashCode());
result = prime * result + ((ecoTemperatureLowF == null) ? 0 : ecoTemperatureLowF.hashCode());
result = prime * result + ((fanTimerActive == null) ? 0 : fanTimerActive.hashCode());
result = prime * result + ((fanTimerDuration == null) ? 0 : fanTimerDuration.hashCode());
result = prime * result + ((fanTimerTimeout == null) ? 0 : fanTimerTimeout.hashCode());
result = prime * result + ((hasFan == null) ? 0 : hasFan.hashCode());
result = prime * result + ((hasLeaf == null) ? 0 : hasLeaf.hashCode());
result = prime * result + ((humidity == null) ? 0 : humidity.hashCode());
result = prime * result + ((hvacMode == null) ? 0 : hvacMode.hashCode());
result = prime * result + ((hvacState == null) ? 0 : hvacState.hashCode());
result = prime * result + ((isLocked == null) ? 0 : isLocked.hashCode());
result = prime * result + ((isUsingEmergencyHeat == null) ? 0 : isUsingEmergencyHeat.hashCode());
result = prime * result + ((lockedTempMaxC == null) ? 0 : lockedTempMaxC.hashCode());
result = prime * result + ((lockedTempMaxF == null) ? 0 : lockedTempMaxF.hashCode());
result = prime * result + ((lockedTempMinC == null) ? 0 : lockedTempMinC.hashCode());
result = prime * result + ((lockedTempMinF == null) ? 0 : lockedTempMinF.hashCode());
result = prime * result + ((previousHvacMode == null) ? 0 : previousHvacMode.hashCode());
result = prime * result + ((sunlightCorrectionActive == null) ? 0 : sunlightCorrectionActive.hashCode());
result = prime * result + ((sunlightCorrectionEnabled == null) ? 0 : sunlightCorrectionEnabled.hashCode());
result = prime * result + ((targetTemperatureC == null) ? 0 : targetTemperatureC.hashCode());
result = prime * result + ((targetTemperatureF == null) ? 0 : targetTemperatureF.hashCode());
result = prime * result + ((targetTemperatureHighC == null) ? 0 : targetTemperatureHighC.hashCode());
result = prime * result + ((targetTemperatureHighF == null) ? 0 : targetTemperatureHighF.hashCode());
result = prime * result + ((targetTemperatureLowC == null) ? 0 : targetTemperatureLowC.hashCode());
result = prime * result + ((targetTemperatureLowF == null) ? 0 : targetTemperatureLowF.hashCode());
result = prime * result + ((temperatureScale == null) ? 0 : temperatureScale.hashCode());
result = prime * result + ((timeToTarget == null) ? 0 : timeToTarget.hashCode());
result = prime * result + ((whereName == null) ? 0 : whereName.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Thermostat [canCool=").append(canCool).append(", canHeat=").append(canHeat)
.append(", isUsingEmergencyHeat=").append(isUsingEmergencyHeat).append(", hasFan=").append(hasFan)
.append(", fanTimerActive=").append(fanTimerActive).append(", fanTimerTimeout=").append(fanTimerTimeout)
.append(", hasLeaf=").append(hasLeaf).append(", temperatureScale=").append(temperatureScale)
.append(", ambientTemperatureC=").append(ambientTemperatureC).append(", ambientTemperatureF=")
.append(ambientTemperatureF).append(", humidity=").append(humidity).append(", targetTemperatureC=")
.append(targetTemperatureC).append(", targetTemperatureF=").append(targetTemperatureF)
.append(", targetTemperatureHighC=").append(targetTemperatureHighC).append(", targetTemperatureHighF=")
.append(targetTemperatureHighF).append(", targetTemperatureLowC=").append(targetTemperatureLowC)
.append(", targetTemperatureLowF=").append(targetTemperatureLowF).append(", hvacMode=").append(hvacMode)
.append(", previousHvacMode=").append(previousHvacMode).append(", hvacState=").append(hvacState)
.append(", ecoTemperatureHighC=").append(ecoTemperatureHighC).append(", ecoTemperatureHighF=")
.append(ecoTemperatureHighF).append(", ecoTemperatureLowC=").append(ecoTemperatureLowC)
.append(", ecoTemperatureLowF=").append(ecoTemperatureLowF).append(", isLocked=").append(isLocked)
.append(", lockedTempMaxC=").append(lockedTempMaxC).append(", lockedTempMaxF=").append(lockedTempMaxF)
.append(", lockedTempMinC=").append(lockedTempMinC).append(", lockedTempMinF=").append(lockedTempMinF)
.append(", sunlightCorrectionEnabled=").append(sunlightCorrectionEnabled)
.append(", sunlightCorrectionActive=").append(sunlightCorrectionActive).append(", fanTimerDuration=")
.append(fanTimerDuration).append(", timeToTarget=").append(timeToTarget).append(", whereName=")
.append(whereName).append(", getId()=").append(getId()).append(", getName()=").append(getName())
.append(", getDeviceId()=").append(getDeviceId()).append(", getLastConnection()=")
.append(getLastConnection()).append(", isOnline()=").append(isOnline()).append(", getNameLong()=")
.append(getNameLong()).append(", getSoftwareVersion()=").append(getSoftwareVersion())
.append(", getStructureId()=").append(getStructureId()).append(", getWhereId()=").append(getWhereId())
.append("]");
return builder.toString();
}
}

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.nest.internal.data;
import java.util.Map;
/**
* Top level data for all the Nest stuff, this is the format the Nest data comes back from Nest in.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class TopLevelData {
private NestDevices devices;
private NestMetadata metadata;
private Map<String, Structure> structures;
public NestDevices getDevices() {
return devices;
}
public NestMetadata getMetadata() {
return metadata;
}
public Map<String, Structure> getStructures() {
return structures;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TopLevelData other = (TopLevelData) obj;
if (devices == null) {
if (other.devices != null) {
return false;
}
} else if (!devices.equals(other.devices)) {
return false;
}
if (metadata == null) {
if (other.metadata != null) {
return false;
}
} else if (!metadata.equals(other.metadata)) {
return false;
}
if (structures == null) {
if (other.structures != null) {
return false;
}
} else if (!structures.equals(other.structures)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((devices == null) ? 0 : devices.hashCode());
result = prime * result + ((metadata == null) ? 0 : metadata.hashCode());
result = prime * result + ((structures == null) ? 0 : structures.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TopLevelData [devices=").append(devices).append(", metadata=").append(metadata)
.append(", structures=").append(structures).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,79 @@
/**
* 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.nest.internal.data;
/**
* The top level data that is sent by Nest to a streaming REST client using SSE.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
* @author Wouter Born - Add equals and hashCode methods
*/
public class TopLevelStreamingData {
private String path;
private TopLevelData data;
public String getPath() {
return path;
}
public TopLevelData getData() {
return data;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((data == null) ? 0 : data.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TopLevelStreamingData other = (TopLevelStreamingData) obj;
if (data == null) {
if (other.data != null) {
return false;
}
} else if (!data.equals(other.data)) {
return false;
}
if (path == null) {
if (other.path != null) {
return false;
}
} else if (!path.equals(other.path)) {
return false;
}
return true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TopLevelStreamingData [path=").append(path).append(", data=").append(data).append("]");
return builder.toString();
}
}

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.nest.internal.data;
/**
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract Where object from Structure
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class Where {
private String whereId;
private String name;
public String getWhereId() {
return whereId;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Where other = (Where) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (whereId == null) {
if (other.whereId != null) {
return false;
}
} else if (!whereId.equals(other.whereId)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((whereId == null) ? 0 : whereId.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Where [whereId=").append(whereId).append(", name=").append(name).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,171 @@
/**
* 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.nest.internal.discovery;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.config.NestStructureConfiguration;
import org.openhab.binding.nest.internal.data.BaseNestDevice;
import org.openhab.binding.nest.internal.data.Camera;
import org.openhab.binding.nest.internal.data.SmokeDetector;
import org.openhab.binding.nest.internal.data.Structure;
import org.openhab.binding.nest.internal.data.Thermostat;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service connects to the Nest bridge and creates the correct discovery results for Nest devices
* as they are found through the API.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add representation properties
*/
@NonNullByDefault
public class NestDiscoveryService extends AbstractDiscoveryService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
.of(THING_TYPE_CAMERA, THING_TYPE_THERMOSTAT, THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE)
.collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(NestDiscoveryService.class);
private final DiscoveryDataListener<Camera> cameraDiscoveryDataListener = new DiscoveryDataListener<>(Camera.class,
THING_TYPE_CAMERA, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<SmokeDetector> smokeDetectorDiscoveryDataListener = new DiscoveryDataListener<>(
SmokeDetector.class, THING_TYPE_SMOKE_DETECTOR, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<Structure> structureDiscoveryDataListener = new DiscoveryDataListener<>(
Structure.class, THING_TYPE_STRUCTURE, this::addStructureDiscoveryResult);
private final DiscoveryDataListener<Thermostat> thermostatDiscoveryDataListener = new DiscoveryDataListener<>(
Thermostat.class, THING_TYPE_THERMOSTAT, this::addDeviceDiscoveryResult);
@SuppressWarnings("rawtypes")
private final List<DiscoveryDataListener> discoveryDataListeners = Stream.of(cameraDiscoveryDataListener,
smokeDetectorDiscoveryDataListener, structureDiscoveryDataListener, thermostatDiscoveryDataListener)
.collect(Collectors.toList());
private final NestBridgeHandler bridge;
private static class DiscoveryDataListener<T> implements NestThingDataListener<T> {
private Class<T> dataClass;
private ThingTypeUID thingTypeUID;
private BiConsumer<T, ThingTypeUID> onDiscovered;
private DiscoveryDataListener(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
this.dataClass = dataClass;
this.thingTypeUID = thingTypeUID;
this.onDiscovered = onDiscovered;
}
@Override
public void onNewData(T data) {
onDiscovered.accept(data, thingTypeUID);
}
@Override
public void onUpdatedData(T oldData, T data) {
}
@Override
public void onMissingData(String nestId) {
}
}
public NestDiscoveryService(NestBridgeHandler bridge) {
super(SUPPORTED_THING_TYPES, 60, true);
this.bridge = bridge;
}
@SuppressWarnings("unchecked")
public void activate() {
discoveryDataListeners.forEach(l -> bridge.addThingDataListener(l.dataClass, l));
addDiscoveryResultsFromLastUpdates();
}
@Override
@SuppressWarnings("unchecked")
public void deactivate() {
discoveryDataListeners.forEach(l -> bridge.removeThingDataListener(l.dataClass, l));
}
@Override
protected void startScan() {
addDiscoveryResultsFromLastUpdates();
}
@SuppressWarnings("unchecked")
private void addDiscoveryResultsFromLastUpdates() {
discoveryDataListeners
.forEach(l -> addDiscoveryResultsFromLastUpdates(l.dataClass, l.thingTypeUID, l.onDiscovered));
}
private <T> void addDiscoveryResultsFromLastUpdates(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
List<T> lastUpdates = bridge.getLastUpdates(dataClass);
lastUpdates.forEach(lastUpdate -> onDiscovered.accept(lastUpdate, thingTypeUID));
}
private void addDeviceDiscoveryResult(BaseNestDevice device, ThingTypeUID typeUID) {
ThingUID bridgeUID = bridge.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, device.getDeviceId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = new HashMap<>();
properties.put(NestDeviceConfiguration.DEVICE_ID, device.getDeviceId());
properties.put(PROPERTY_FIRMWARE_VERSION, device.getSoftwareVersion());
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(thingUID)
.withThingType(typeUID)
.withLabel(device.getNameLong())
.withBridge(bridgeUID)
.withProperties(properties)
.withRepresentationProperty(NestDeviceConfiguration.DEVICE_ID)
.build()
);
// @formatter:on
}
public void addStructureDiscoveryResult(Structure structure, ThingTypeUID typeUID) {
ThingUID bridgeUID = bridge.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, structure.getStructureId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = new HashMap<>();
properties.put(NestStructureConfiguration.STRUCTURE_ID, structure.getStructureId());
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(thingUID)
.withThingType(THING_TYPE_STRUCTURE)
.withLabel(structure.getName())
.withBridge(bridgeUID)
.withProperties(properties)
.withRepresentationProperty(NestStructureConfiguration.STRUCTURE_ID)
.build()
);
// @formatter:on
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.nest.internal.exceptions;
/**
* Will be thrown when the bridge was unable to resolve the Nest redirect URL.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling while sending data
*/
@SuppressWarnings("serial")
public class FailedResolvingNestUrlException extends Exception {
public FailedResolvingNestUrlException(String message) {
super(message);
}
public FailedResolvingNestUrlException(String message, Throwable cause) {
super(message, cause);
}
public FailedResolvingNestUrlException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.exceptions;
/**
* Will be thrown when the bridge was unable to retrieve data.
*
* @author Martin van Wingerden - Initial contribution
* @author Martin van Wingerden - Added more centralized handling of failure when retrieving data
*/
@SuppressWarnings("serial")
public class FailedRetrievingNestDataException extends Exception {
public FailedRetrievingNestDataException(String message) {
super(message);
}
public FailedRetrievingNestDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedRetrievingNestDataException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.nest.internal.exceptions;
/**
* Will be thrown when the bridge was unable to send data.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling while sending data
*/
@SuppressWarnings("serial")
public class FailedSendingNestDataException extends Exception {
public FailedSendingNestDataException(String message) {
super(message);
}
public FailedSendingNestDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedSendingNestDataException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.nest.internal.exceptions;
/**
* Will be thrown when there is no valid access token and it was not possible to refresh it
*
* @author Martin van Wingerden - Initial contribution
* @author Martin van Wingerden - Added more centralized handling of invalid access tokens
*/
@SuppressWarnings("serial")
public class InvalidAccessTokenException extends Exception {
public InvalidAccessTokenException(Exception cause) {
super(cause);
}
public InvalidAccessTokenException(String message, Throwable cause) {
super(message, cause);
}
public InvalidAccessTokenException(String message) {
super(message);
}
}

View File

@ -0,0 +1,204 @@
/**
* 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.nest.internal.handler;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.TimeZone;
import java.util.stream.Collectors;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.rest.NestUpdateRequest;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Deals with the structures on the Nest API, turning them into a thing in openHAB.
*
* @author David Bennett - Initial contribution
* @author Martin van Wingerden - Splitted of NestBaseHandler
* @author Wouter Born - Add generic update data type
*
* @param <T> the type of update data
*/
@NonNullByDefault
public abstract class NestBaseHandler<T> extends BaseThingHandler
implements NestThingDataListener<T>, NestIdentifiable {
private final Logger logger = LoggerFactory.getLogger(NestBaseHandler.class);
private @Nullable String deviceId;
private Class<T> dataClass;
NestBaseHandler(Thing thing, Class<T> dataClass) {
super(thing);
this.dataClass = dataClass;
}
@Override
public void initialize() {
logger.debug("Initializing handler for {}", getClass().getName());
NestBridgeHandler handler = getNestBridgeHandler();
if (handler != null) {
boolean success = handler.addThingDataListener(dataClass, getId(), this);
logger.debug("Adding {} with ID '{}' as device data listener, result: {}", getClass().getSimpleName(),
getId(), success);
} else {
logger.debug("Unable to add {} with ID '{}' as device data listener because bridge is null",
getClass().getSimpleName(), getId());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for refresh");
T lastUpdate = getLastUpdate();
if (lastUpdate != null) {
update(null, lastUpdate);
}
}
@Override
public void dispose() {
NestBridgeHandler handler = getNestBridgeHandler();
if (handler != null) {
handler.removeThingDataListener(dataClass, getId(), this);
}
}
protected @Nullable T getLastUpdate() {
NestBridgeHandler handler = getNestBridgeHandler();
if (handler != null) {
return handler.getLastUpdate(dataClass, getId());
}
return null;
}
protected void addUpdateRequest(String updatePath, String field, Object value) {
NestBridgeHandler handler = getNestBridgeHandler();
if (handler != null) {
// @formatter:off
handler.addUpdateRequest(new NestUpdateRequest.Builder()
.withBasePath(updatePath)
.withIdentifier(getId())
.withAdditionalValue(field, value)
.build());
// @formatter:on
}
}
@Override
public String getId() {
return getDeviceId();
}
protected String getDeviceId() {
String localDeviceId = deviceId;
if (localDeviceId == null) {
localDeviceId = getConfigAs(NestDeviceConfiguration.class).deviceId;
deviceId = localDeviceId;
}
return localDeviceId;
}
protected @Nullable NestBridgeHandler getNestBridgeHandler() {
Bridge bridge = getBridge();
return bridge != null ? (NestBridgeHandler) bridge.getHandler() : null;
}
protected abstract State getChannelState(ChannelUID channelUID, T data);
protected State getAsDateTimeTypeOrNull(@Nullable Date date) {
if (date == null) {
return UnDefType.NULL;
}
long offsetMillis = TimeZone.getDefault().getOffset(date.getTime());
Instant instant = date.toInstant().plusMillis(offsetMillis);
return new DateTimeType(ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()));
}
protected State getAsDecimalTypeOrNull(@Nullable Integer value) {
return value == null ? UnDefType.NULL : new DecimalType(value);
}
protected State getAsOnOffTypeOrNull(@Nullable Boolean value) {
return value == null ? UnDefType.NULL : value ? OnOffType.ON : OnOffType.OFF;
}
protected <U extends Quantity<U>> State getAsQuantityTypeOrNull(@Nullable Number value, Unit<U> unit) {
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
}
protected State getAsStringTypeOrNull(@Nullable Object value) {
return value == null ? UnDefType.NULL : new StringType(value.toString());
}
protected State getAsStringTypeListOrNull(@Nullable Collection<?> values) {
return values == null || values.isEmpty() ? UnDefType.NULL
: new StringType(values.stream().map(v -> v.toString()).collect(Collectors.joining(",")));
}
protected boolean isNotHandling(NestIdentifiable nestIdentifiable) {
return !(getId().equals(nestIdentifiable.getId()));
}
protected void updateLinkedChannels(T oldData, T data) {
getThing().getChannels().stream().map(c -> c.getUID()).filter(this::isLinked).forEach(channelUID -> {
State newState = getChannelState(channelUID, data);
if (oldData == null || !getChannelState(channelUID, oldData).equals(newState)) {
logger.debug("Updating {}", channelUID);
updateState(channelUID, newState);
}
});
}
@Override
public void onNewData(T data) {
update(null, data);
}
@Override
public void onUpdatedData(T oldData, T data) {
update(oldData, data);
}
@Override
public void onMissingData(String nestId) {
thing.setStatusInfo(
new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Missing from streaming updates"));
}
protected abstract void update(T oldData, T data);
}

View File

@ -0,0 +1,383 @@
/**
* 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.nest.internal.handler;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.data.ErrorData;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.binding.nest.internal.exceptions.FailedSendingNestDataException;
import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
import org.openhab.binding.nest.internal.listener.NestStreamingDataListener;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.rest.NestAuthorizer;
import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
import org.openhab.binding.nest.internal.rest.NestUpdateRequest;
import org.openhab.binding.nest.internal.update.NestCompositeUpdateHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This bridge handler connects to Nest and handles all the API requests. It pulls down the
* updated data, polls the system and does all the co-ordination with the other handlers
* to get the data updated to the correct things.
*
* @author David Bennett - Initial contribution
* @author Martin van Wingerden - Use listeners not only for discovery but for all data processing
* @author Wouter Born - Improve exception and URL redirect handling
*/
@NonNullByDefault
public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class);
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final List<NestUpdateRequest> nestUpdateRequests = new CopyOnWriteArrayList<>();
private final NestCompositeUpdateHandler updateHandler = new NestCompositeUpdateHandler(
this::getPresentThingsNestIds);
private @NonNullByDefault({}) NestAuthorizer authorizer;
private @NonNullByDefault({}) NestBridgeConfiguration config;
private @Nullable ScheduledFuture<?> initializeJob;
private @Nullable ScheduledFuture<?> transmitJob;
private @Nullable NestRedirectUrlSupplier redirectUrlSupplier;
private @Nullable NestStreamingRestClient streamingRestClient;
/**
* Creates the bridge handler to connect to Nest.
*
* @param bridge The bridge to connect to Nest with.
*/
public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) {
super(bridge);
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
}
/**
* Initialize the connection to Nest.
*/
@Override
public void initialize() {
logger.debug("Initializing Nest bridge handler");
config = getConfigAs(NestBridgeConfiguration.class);
authorizer = new NestAuthorizer(config);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query");
initializeJob = scheduler.schedule(() -> {
try {
logger.debug("Product ID {}", config.productId);
logger.debug("Product Secret {}", config.productSecret);
logger.debug("Pincode {}", config.pincode);
logger.debug("Access Token {}", getExistingOrNewAccessToken());
redirectUrlSupplier = createRedirectUrlSupplier();
restartStreamingUpdates();
} catch (InvalidAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
}
}, 0, TimeUnit.SECONDS);
logger.debug("Finished initializing Nest bridge handler");
}
/**
* Clean up the handler.
*/
@Override
public void dispose() {
logger.debug("Nest bridge disposed");
stopStreamingUpdates();
ScheduledFuture<?> localInitializeJob = initializeJob;
if (localInitializeJob != null && !localInitializeJob.isCancelled()) {
localInitializeJob.cancel(true);
initializeJob = null;
}
ScheduledFuture<?> localTransmitJob = transmitJob;
if (localTransmitJob != null && !localTransmitJob.isCancelled()) {
localTransmitJob.cancel(true);
transmitJob = null;
}
this.authorizer = null;
this.redirectUrlSupplier = null;
this.streamingRestClient = null;
}
public <T> boolean addThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
return updateHandler.addListener(dataClass, listener);
}
public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
return updateHandler.addListener(dataClass, nestId, listener);
}
/**
* Adds the update request into the queue for doing something with, send immediately if the queue is empty.
*/
public void addUpdateRequest(NestUpdateRequest request) {
nestUpdateRequests.add(request);
scheduleTransmitJobForPendingRequests();
}
protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
return new NestRedirectUrlSupplier(getHttpHeaders());
}
private String getExistingOrNewAccessToken() throws InvalidAccessTokenException {
String accessToken = config.accessToken;
if (accessToken == null || accessToken.isEmpty()) {
accessToken = authorizer.getNewAccessToken();
config.accessToken = accessToken;
config.pincode = "";
// Update and save the access token in the bridge configuration
Configuration configuration = editConfiguration();
configuration.put(NestBridgeConfiguration.ACCESS_TOKEN, config.accessToken);
configuration.put(NestBridgeConfiguration.PINCODE, config.pincode);
updateConfiguration(configuration);
logger.debug("Retrieved new access token: {}", config.accessToken);
return accessToken;
} else {
logger.debug("Re-using access token from configuration: {}", accessToken);
return accessToken;
}
}
protected Properties getHttpHeaders() throws InvalidAccessTokenException {
Properties httpHeaders = new Properties();
httpHeaders.put("Authorization", "Bearer " + getExistingOrNewAccessToken());
httpHeaders.put("Content-Type", JSON_CONTENT_TYPE);
return httpHeaders;
}
public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
return updateHandler.getLastUpdate(dataClass, nestId);
}
public <T> List<T> getLastUpdates(Class<T> dataClass) {
return updateHandler.getLastUpdates(dataClass);
}
private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException {
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier == null) {
localRedirectUrlSupplier = createRedirectUrlSupplier();
redirectUrlSupplier = localRedirectUrlSupplier;
}
return localRedirectUrlSupplier;
}
private Set<String> getPresentThingsNestIds() {
Set<String> nestIds = new HashSet<>();
for (Thing thing : getThing().getThings()) {
ThingHandler handler = thing.getHandler();
if (handler != null && thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.GONE) {
nestIds.add(((NestIdentifiable) handler).getId());
}
}
return nestIds;
}
/**
* Handles an incoming command update
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
logger.debug("Refresh command received");
updateHandler.resendLastUpdates();
}
}
private void jsonToPutUrl(NestUpdateRequest request)
throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException {
try {
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier == null) {
throw new FailedResolvingNestUrlException("redirectUrlSupplier is null");
}
String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath();
logger.debug("Putting data to: {}", url);
String jsonContent = NestUtils.toJson(request.getValues());
logger.debug("PUT content: {}", jsonContent);
ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8));
String jsonResponse = HttpUtil.executeUrl("PUT", url, getHttpHeaders(), inputStream, JSON_CONTENT_TYPE,
REQUEST_TIMEOUT);
logger.debug("PUT response: {}", jsonResponse);
ErrorData error = NestUtils.fromJson(jsonResponse, ErrorData.class);
if (error.getError() != null && !error.getError().isBlank()) {
logger.debug("Nest API error: {}", error);
logger.warn("Nest API error: {}", error.getMessage());
}
} catch (IOException e) {
throw new FailedSendingNestDataException("Failed to send data", e);
}
}
@Override
public void onAuthorizationRevoked(String token) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Authorization token revoked: " + token);
}
@Override
public void onConnected() {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Streaming data connection established");
scheduleTransmitJobForPendingRequests();
}
@Override
public void onDisconnected() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Streaming data disconnected");
}
@Override
public void onError(String message) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
}
@Override
public void onNewTopLevelData(TopLevelData data) {
updateHandler.handleUpdate(data);
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data");
}
public <T> boolean removeThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
return updateHandler.removeListener(dataClass, listener);
}
public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
return updateHandler.removeListener(dataClass, nestId, listener);
}
private void restartStreamingUpdates() {
synchronized (this) {
stopStreamingUpdates();
startStreamingUpdates();
}
}
private void scheduleTransmitJobForPendingRequests() {
ScheduledFuture<?> localTransmitJob = transmitJob;
if (!nestUpdateRequests.isEmpty() && (localTransmitJob == null || localTransmitJob.isDone())) {
transmitJob = scheduler.schedule(this::transmitQueue, 0, SECONDS);
}
}
private void startStreamingUpdates() {
synchronized (this) {
try {
NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient(
getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory,
getOrCreateRedirectUrlSupplier(), scheduler);
localStreamingRestClient.addStreamingDataListener(this);
localStreamingRestClient.start();
streamingRestClient = localStreamingRestClient;
} catch (InvalidAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
}
}
}
private void stopStreamingUpdates() {
NestStreamingRestClient localStreamingRestClient = streamingRestClient;
if (localStreamingRestClient != null) {
synchronized (this) {
localStreamingRestClient.stop();
localStreamingRestClient.removeStreamingDataListener(this);
streamingRestClient = null;
}
}
}
private void transmitQueue() {
if (getThing().getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Not transmitting events because bridge is OFFLINE");
return;
}
try {
while (!nestUpdateRequests.isEmpty()) {
// nestUpdateRequests is a CopyOnWriteArrayList so its iterator does not support remove operations
NestUpdateRequest request = nestUpdateRequests.get(0);
jsonToPutUrl(request);
nestUpdateRequests.remove(request);
}
} catch (InvalidAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
} catch (FailedResolvingNestUrlException e) {
logger.debug("Unable to resolve redirect URL", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
} catch (FailedSendingNestDataException e) {
logger.debug("Error sending data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier != null) {
localRedirectUrlSupplier.resetCache();
}
}
}
}

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.Camera;
import org.openhab.binding.nest.internal.data.CameraEvent;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles all the updates to the camera as well as handling the commands that send
* updates to Nest.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestCameraHandler extends NestBaseHandler<Camera> {
private final Logger logger = LoggerFactory.getLogger(NestCameraHandler.class);
public NestCameraHandler(Thing thing) {
super(thing, Camera.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Camera camera) {
if (channelUID.getId().startsWith(CHANNEL_GROUP_CAMERA_PREFIX)) {
return getCameraChannelState(channelUID, camera);
} else if (channelUID.getId().startsWith(CHANNEL_GROUP_LAST_EVENT_PREFIX)) {
return getLastEventChannelState(channelUID, camera);
} else {
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
protected State getCameraChannelState(ChannelUID channelUID, Camera camera) {
switch (channelUID.getId()) {
case CHANNEL_CAMERA_APP_URL:
return getAsStringTypeOrNull(camera.getAppUrl());
case CHANNEL_CAMERA_AUDIO_INPUT_ENABLED:
return getAsOnOffTypeOrNull(camera.isAudioInputEnabled());
case CHANNEL_CAMERA_LAST_ONLINE_CHANGE:
return getAsDateTimeTypeOrNull(camera.getLastIsOnlineChange());
case CHANNEL_CAMERA_PUBLIC_SHARE_ENABLED:
return getAsOnOffTypeOrNull(camera.isPublicShareEnabled());
case CHANNEL_CAMERA_PUBLIC_SHARE_URL:
return getAsStringTypeOrNull(camera.getPublicShareUrl());
case CHANNEL_CAMERA_SNAPSHOT_URL:
return getAsStringTypeOrNull(camera.getSnapshotUrl());
case CHANNEL_CAMERA_STREAMING:
return getAsOnOffTypeOrNull(camera.isStreaming());
case CHANNEL_CAMERA_VIDEO_HISTORY_ENABLED:
return getAsOnOffTypeOrNull(camera.isVideoHistoryEnabled());
case CHANNEL_CAMERA_WEB_URL:
return getAsStringTypeOrNull(camera.getWebUrl());
default:
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
protected State getLastEventChannelState(ChannelUID channelUID, Camera camera) {
CameraEvent lastEvent = camera.getLastEvent();
if (lastEvent == null) {
return UnDefType.NULL;
}
switch (channelUID.getId()) {
case CHANNEL_LAST_EVENT_ACTIVITY_ZONES:
return getAsStringTypeListOrNull(lastEvent.getActivityZones());
case CHANNEL_LAST_EVENT_ANIMATED_IMAGE_URL:
return getAsStringTypeOrNull(lastEvent.getAnimatedImageUrl());
case CHANNEL_LAST_EVENT_APP_URL:
return getAsStringTypeOrNull(lastEvent.getAppUrl());
case CHANNEL_LAST_EVENT_END_TIME:
return getAsDateTimeTypeOrNull(lastEvent.getEndTime());
case CHANNEL_LAST_EVENT_HAS_MOTION:
return getAsOnOffTypeOrNull(lastEvent.isHasMotion());
case CHANNEL_LAST_EVENT_HAS_PERSON:
return getAsOnOffTypeOrNull(lastEvent.isHasPerson());
case CHANNEL_LAST_EVENT_HAS_SOUND:
return getAsOnOffTypeOrNull(lastEvent.isHasSound());
case CHANNEL_LAST_EVENT_IMAGE_URL:
return getAsStringTypeOrNull(lastEvent.getImageUrl());
case CHANNEL_LAST_EVENT_START_TIME:
return getAsDateTimeTypeOrNull(lastEvent.getStartTime());
case CHANNEL_LAST_EVENT_URLS_EXPIRE_TIME:
return getAsDateTimeTypeOrNull(lastEvent.getUrlsExpireTime());
case CHANNEL_LAST_EVENT_WEB_URL:
return getAsStringTypeOrNull(lastEvent.getWebUrl());
default:
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Camera lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
} else if (CHANNEL_CAMERA_STREAMING.equals(channelUID.getId())) {
// Change the mode.
if (command instanceof OnOffType) {
// Set the mode to be the cmd value.
addUpdateRequest("is_streaming", command == OnOffType.ON);
}
}
}
private void addUpdateRequest(String field, Object value) {
addUpdateRequest(NEST_CAMERA_UPDATE_PATH, field, value);
}
@Override
protected void update(Camera oldCamera, Camera camera) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldCamera, camera);
updateProperty(PROPERTY_FIRMWARE_VERSION, camera.getSoftwareVersion());
ThingStatus newStatus = camera.isOnline() == null ? ThingStatus.UNKNOWN
: camera.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
if (newStatus != thing.getStatus()) {
updateStatus(newStatus);
}
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.nest.internal.handler;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.nest.internal.NestBindingConstants;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Supplies resolved redirect URLs of {@link NestBindingConstants#NEST_URL} so they can be used with HTTP clients that
* do not pass Authorization headers after redirects like the Jetty client used by {@link HttpUtil}.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Extract resolving redirect URL from NestBridgeHandler into NestRedirectUrlSupplier
*/
@NonNullByDefault
public class NestRedirectUrlSupplier {
private final Logger logger = LoggerFactory.getLogger(NestRedirectUrlSupplier.class);
protected String cachedUrl = "";
protected Properties httpHeaders;
public NestRedirectUrlSupplier(Properties httpHeaders) {
this.httpHeaders = httpHeaders;
}
public String getRedirectUrl() throws FailedResolvingNestUrlException {
if (cachedUrl.isEmpty()) {
cachedUrl = resolveRedirectUrl();
}
return cachedUrl;
}
public void resetCache() {
cachedUrl = "";
}
/**
* Resolves the redirect URL for calls using the {@link NestBindingConstants#NEST_URL}.
*
* The Jetty client used by {@link HttpUtil} will not pass the Authorization header after a redirect resulting in
* "401 Unauthorized error" issues.
*
* Note that this workaround currently does not use any configured proxy like {@link HttpUtil} does.
*
* @see https://developers.nest.com/documentation/cloud/how-to-handle-redirects
*/
private String resolveRedirectUrl() throws FailedResolvingNestUrlException {
HttpClient httpClient = new HttpClient(new SslContextFactory());
httpClient.setFollowRedirects(false);
Request request = httpClient.newRequest(NestBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30,
TimeUnit.SECONDS);
for (String httpHeaderKey : httpHeaders.stringPropertyNames()) {
request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey));
}
ContentResponse response;
try {
httpClient.start();
response = request.send();
httpClient.stop();
} catch (Exception e) {
throw new FailedResolvingNestUrlException("Failed to resolve redirect URL: " + e.getMessage(), e);
}
int status = response.getStatus();
String redirectUrl = response.getHeaders().get(HttpHeader.LOCATION);
if (status != HttpStatus.TEMPORARY_REDIRECT_307) {
logger.debug("Redirect status: {}", status);
logger.debug("Redirect response: {}", response.getContentAsString());
throw new FailedResolvingNestUrlException("Failed to get redirect URL, expected status "
+ HttpStatus.TEMPORARY_REDIRECT_307 + " but was " + status);
} else if (redirectUrl == null || redirectUrl.isEmpty()) {
throw new FailedResolvingNestUrlException("Redirect URL is empty");
}
redirectUrl = redirectUrl.endsWith("/") ? redirectUrl.substring(0, redirectUrl.length() - 1) : redirectUrl;
logger.debug("Redirect URL: {}", redirectUrl);
return redirectUrl;
}
}

View File

@ -0,0 +1,95 @@
/**
* 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.nest.internal.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.SmokeDetector;
import org.openhab.binding.nest.internal.data.SmokeDetector.BatteryHealth;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The smoke detector handler, it handles the data from Nest for the smoke detector.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestSmokeDetectorHandler extends NestBaseHandler<SmokeDetector> {
private final Logger logger = LoggerFactory.getLogger(NestSmokeDetectorHandler.class);
public NestSmokeDetectorHandler(Thing thing) {
super(thing, SmokeDetector.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, SmokeDetector smokeDetector) {
switch (channelUID.getId()) {
case CHANNEL_CO_ALARM_STATE:
return getAsStringTypeOrNull(smokeDetector.getCoAlarmState());
case CHANNEL_LAST_CONNECTION:
return getAsDateTimeTypeOrNull(smokeDetector.getLastConnection());
case CHANNEL_LAST_MANUAL_TEST_TIME:
return getAsDateTimeTypeOrNull(smokeDetector.getLastManualTestTime());
case CHANNEL_LOW_BATTERY:
return getAsOnOffTypeOrNull(smokeDetector.getBatteryHealth() == null ? null
: smokeDetector.getBatteryHealth() == BatteryHealth.REPLACE);
case CHANNEL_MANUAL_TEST_ACTIVE:
return getAsOnOffTypeOrNull(smokeDetector.isManualTestActive());
case CHANNEL_SMOKE_ALARM_STATE:
return getAsStringTypeOrNull(smokeDetector.getSmokeAlarmState());
case CHANNEL_UI_COLOR_STATE:
return getAsStringTypeOrNull(smokeDetector.getUiColorState());
default:
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
/**
* Handles any incoming command requests.
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
SmokeDetector lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
}
}
@Override
protected void update(SmokeDetector oldSmokeDetector, SmokeDetector smokeDetector) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldSmokeDetector, smokeDetector);
updateProperty(PROPERTY_FIRMWARE_VERSION, smokeDetector.getSoftwareVersion());
ThingStatus newStatus = smokeDetector.isOnline() == null ? ThingStatus.UNKNOWN
: smokeDetector.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
if (newStatus != thing.getStatus()) {
updateStatus(newStatus);
}
}
}

View File

@ -0,0 +1,128 @@
/**
* 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.nest.internal.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.config.NestStructureConfiguration;
import org.openhab.binding.nest.internal.data.Structure;
import org.openhab.binding.nest.internal.data.Structure.HomeAwayState;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Deals with the structures on the Nest API, turning them into a thing in openHAB.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestStructureHandler extends NestBaseHandler<Structure> {
private final Logger logger = LoggerFactory.getLogger(NestStructureHandler.class);
private @Nullable String structureId;
public NestStructureHandler(Thing thing) {
super(thing, Structure.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Structure structure) {
switch (channelUID.getId()) {
case CHANNEL_AWAY:
return getAsStringTypeOrNull(structure.getAway());
case CHANNEL_CO_ALARM_STATE:
return getAsStringTypeOrNull(structure.getCoAlarmState());
case CHANNEL_COUNTRY_CODE:
return getAsStringTypeOrNull(structure.getCountryCode());
case CHANNEL_ETA_BEGIN:
return getAsDateTimeTypeOrNull(structure.getEtaBegin());
case CHANNEL_PEAK_PERIOD_END_TIME:
return getAsDateTimeTypeOrNull(structure.getPeakPeriodEndTime());
case CHANNEL_PEAK_PERIOD_START_TIME:
return getAsDateTimeTypeOrNull(structure.getPeakPeriodStartTime());
case CHANNEL_POSTAL_CODE:
return getAsStringTypeOrNull(structure.getPostalCode());
case CHANNEL_RUSH_HOUR_REWARDS_ENROLLMENT:
return getAsOnOffTypeOrNull(structure.isRhrEnrollment());
case CHANNEL_SECURITY_STATE:
return getAsStringTypeOrNull(structure.getWwnSecurityState());
case CHANNEL_SMOKE_ALARM_STATE:
return getAsStringTypeOrNull(structure.getSmokeAlarmState());
case CHANNEL_TIME_ZONE:
return getAsStringTypeOrNull(structure.getTimeZone());
default:
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
@Override
public String getId() {
return getStructureId();
}
private String getStructureId() {
String localStructureId = structureId;
if (localStructureId == null) {
localStructureId = getConfigAs(NestStructureConfiguration.class).structureId;
structureId = localStructureId;
}
return localStructureId;
}
/**
* Handles updating the details on this structure by sending the request all the way
* to Nest.
*
* @param channelUID the channel to update
* @param command the command to apply
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Structure lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
} else if (CHANNEL_AWAY.equals(channelUID.getId())) {
// Change the home/away state.
if (command instanceof StringType) {
StringType cmd = (StringType) command;
// Set the mode to be the cmd value.
addUpdateRequest(NEST_STRUCTURE_UPDATE_PATH, "away", HomeAwayState.valueOf(cmd.toString()));
}
}
}
@Override
protected void update(Structure oldStructure, Structure structure) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldStructure, structure);
if (ThingStatus.ONLINE != thing.getStatus()) {
updateStatus(ThingStatus.ONLINE);
}
}
}

View File

@ -0,0 +1,219 @@
/**
* 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.nest.internal.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
import java.math.BigDecimal;
import java.math.RoundingMode;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.data.Thermostat;
import org.openhab.binding.nest.internal.data.Thermostat.Mode;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NestThermostatHandler} is responsible for handling commands, which are
* sent to one of the channels for the thermostat.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestThermostatHandler extends NestBaseHandler<Thermostat> {
private final Logger logger = LoggerFactory.getLogger(NestThermostatHandler.class);
public NestThermostatHandler(Thing thing) {
super(thing, Thermostat.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Thermostat thermostat) {
switch (channelUID.getId()) {
case CHANNEL_CAN_COOL:
return getAsOnOffTypeOrNull(thermostat.isCanCool());
case CHANNEL_CAN_HEAT:
return getAsOnOffTypeOrNull(thermostat.isCanHeat());
case CHANNEL_ECO_MAX_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getEcoTemperatureHigh(), thermostat.getTemperatureUnit());
case CHANNEL_ECO_MIN_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getEcoTemperatureLow(), thermostat.getTemperatureUnit());
case CHANNEL_FAN_TIMER_ACTIVE:
return getAsOnOffTypeOrNull(thermostat.isFanTimerActive());
case CHANNEL_FAN_TIMER_DURATION:
return getAsQuantityTypeOrNull(thermostat.getFanTimerDuration(), SmartHomeUnits.MINUTE);
case CHANNEL_FAN_TIMER_TIMEOUT:
return getAsDateTimeTypeOrNull(thermostat.getFanTimerTimeout());
case CHANNEL_HAS_FAN:
return getAsOnOffTypeOrNull(thermostat.isHasFan());
case CHANNEL_HAS_LEAF:
return getAsOnOffTypeOrNull(thermostat.isHasLeaf());
case CHANNEL_HUMIDITY:
return getAsQuantityTypeOrNull(thermostat.getHumidity(), SmartHomeUnits.PERCENT);
case CHANNEL_LAST_CONNECTION:
return getAsDateTimeTypeOrNull(thermostat.getLastConnection());
case CHANNEL_LOCKED:
return getAsOnOffTypeOrNull(thermostat.isLocked());
case CHANNEL_LOCKED_MAX_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getLockedTempMax(), thermostat.getTemperatureUnit());
case CHANNEL_LOCKED_MIN_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getLockedTempMin(), thermostat.getTemperatureUnit());
case CHANNEL_MAX_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getTargetTemperatureHigh(), thermostat.getTemperatureUnit());
case CHANNEL_MIN_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getTargetTemperatureLow(), thermostat.getTemperatureUnit());
case CHANNEL_MODE:
return getAsStringTypeOrNull(thermostat.getMode());
case CHANNEL_PREVIOUS_MODE:
Mode previousMode = thermostat.getPreviousHvacMode() != null ? thermostat.getPreviousHvacMode()
: thermostat.getMode();
return getAsStringTypeOrNull(previousMode);
case CHANNEL_STATE:
return getAsStringTypeOrNull(thermostat.getHvacState());
case CHANNEL_SET_POINT:
return getAsQuantityTypeOrNull(thermostat.getTargetTemperature(), thermostat.getTemperatureUnit());
case CHANNEL_SUNLIGHT_CORRECTION_ACTIVE:
return getAsOnOffTypeOrNull(thermostat.isSunlightCorrectionActive());
case CHANNEL_SUNLIGHT_CORRECTION_ENABLED:
return getAsOnOffTypeOrNull(thermostat.isSunlightCorrectionEnabled());
case CHANNEL_TEMPERATURE:
return getAsQuantityTypeOrNull(thermostat.getAmbientTemperature(), thermostat.getTemperatureUnit());
case CHANNEL_TIME_TO_TARGET:
return getAsQuantityTypeOrNull(thermostat.getTimeToTarget(), SmartHomeUnits.MINUTE);
case CHANNEL_USING_EMERGENCY_HEAT:
return getAsOnOffTypeOrNull(thermostat.isUsingEmergencyHeat());
default:
logger.error("Unsupported channelId '{}'", channelUID.getId());
return UnDefType.UNDEF;
}
}
/**
* Handle the command to do things to the thermostat, this will change the
* value of a channel by sending the request to Nest.
*/
@Override
@SuppressWarnings("unchecked")
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Thermostat lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
} else if (CHANNEL_FAN_TIMER_ACTIVE.equals(channelUID.getId())) {
if (command instanceof OnOffType) {
// Update fan timer active to the command value
addUpdateRequest("fan_timer_active", command == OnOffType.ON);
}
} else if (CHANNEL_FAN_TIMER_DURATION.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
// Update fan timer duration to the command value
QuantityType<Time> minuteQuantity = ((QuantityType<Time>) command).toUnit(SmartHomeUnits.MINUTE);
if (minuteQuantity != null) {
addUpdateRequest("fan_timer_duration", minuteQuantity.intValue());
}
}
} else if (CHANNEL_MAX_SET_POINT.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
// Update maximum set point to the command value
addTemperatureUpdateRequest("target_temperature_high_c", "target_temperature_high_f",
(QuantityType<Temperature>) command);
}
} else if (CHANNEL_MIN_SET_POINT.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
// Update minimum set point to the command value
addTemperatureUpdateRequest("target_temperature_low_c", "target_temperature_low_f",
(QuantityType<Temperature>) command);
}
} else if (CHANNEL_MODE.equals(channelUID.getId())) {
if (command instanceof StringType) {
// Update the HVAC mode to the command value
addUpdateRequest("hvac_mode", Mode.valueOf(((StringType) command).toString()));
}
} else if (CHANNEL_SET_POINT.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
// Update set point to the command value
addTemperatureUpdateRequest("target_temperature_c", "target_temperature_f",
(QuantityType<Temperature>) command);
}
}
}
private void addUpdateRequest(String field, Object value) {
addUpdateRequest(NEST_THERMOSTAT_UPDATE_PATH, field, value);
}
private void addTemperatureUpdateRequest(String celsiusField, String fahrenheitField,
QuantityType<Temperature> quantity) {
Unit<Temperature> unit = getTemperatureUnit(quantity.getUnit());
BigDecimal value = quantityToRoundedTemperature(quantity, unit);
if (value != null) {
addUpdateRequest(NEST_THERMOSTAT_UPDATE_PATH, unit == CELSIUS ? celsiusField : fahrenheitField, value);
}
}
private Unit<Temperature> getTemperatureUnit(Unit<Temperature> fallbackUnit) {
Thermostat lastUpdate = getLastUpdate();
if (lastUpdate != null && lastUpdate.getTemperatureUnit() != null) {
return lastUpdate.getTemperatureUnit();
}
return fallbackUnit;
}
private @Nullable BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity,
Unit<Temperature> unit) throws IllegalArgumentException {
QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
if (temparatureQuantity == null) {
return null;
}
BigDecimal value = temparatureQuantity.toBigDecimal();
BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
return divisor.multiply(increment);
}
@Override
protected void update(Thermostat oldThermostat, Thermostat thermostat) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldThermostat, thermostat);
updateProperty(PROPERTY_FIRMWARE_VERSION, thermostat.getSoftwareVersion());
ThingStatus newStatus = thermostat.isOnline() == null ? ThingStatus.UNKNOWN
: thermostat.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
if (newStatus != thing.getStatus()) {
updateStatus(newStatus);
}
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
/**
* Interface for listeners of events generated by the {@link NestStreamingRestClient}.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public interface NestStreamingDataListener {
/**
* Authorization has been revoked for a token.
*/
void onAuthorizationRevoked(String token);
/**
* The client successfully established a connection.
*/
void onConnected();
/**
* The client was disconnected.
*/
void onDisconnected();
/**
* An error message was published.
*/
void onError(String message);
/**
* Initial {@link TopLevelData} or an update is sent.
*/
void onNewTopLevelData(TopLevelData data);
}

View File

@ -0,0 +1,46 @@
/**
* 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.nest.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Used to track incoming data for Nest things.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface NestThingDataListener<T> {
/**
* An initial value for the data was received or the value is send again due to a refresh.
*
* @param data the data
*/
void onNewData(T data);
/**
* Existing data was updated to a new value.
*
* @param oldData the previous value
* @param data the current value
*/
void onUpdatedData(T oldData, T data);
/**
* A Nest thing which previously had data is missing. E.g. it was removed from the account.
*
* @param nestId identifies the Nest thing
*/
void onMissingData(String nestId);
}

View File

@ -0,0 +1,89 @@
/**
* 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.nest.internal.rest;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.NestBindingConstants;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.data.AccessTokenData;
import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Retrieves the Nest access token using the OAuth 2.0 protocol using pin-based authorization.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Improve exception handling
*/
@NonNullByDefault
public class NestAuthorizer {
private final Logger logger = LoggerFactory.getLogger(NestAuthorizer.class);
private final NestBridgeConfiguration config;
/**
* Create the helper class for the Nest access token. Also creates the folder
* to put the access token data in if it does not already exist.
*
* @param config The configuration to use for the token
*/
public NestAuthorizer(NestBridgeConfiguration config) {
this.config = config;
}
/**
* Get the current access token, refreshing if needed.
*
* @throws InvalidAccessTokenException thrown when the access token is invalid and could not be refreshed
*/
public String getNewAccessToken() throws InvalidAccessTokenException {
try {
String pincode = config.pincode;
if (pincode == null || pincode.isBlank()) {
throw new InvalidAccessTokenException("Pincode is empty");
}
// @formatter:off
StringBuilder urlBuilder = new StringBuilder(NestBindingConstants.NEST_ACCESS_TOKEN_URL)
.append("?client_id=")
.append(config.productId)
.append("&client_secret=")
.append(config.productSecret)
.append("&code=")
.append(pincode)
.append("&grant_type=authorization_code");
// @formatter:on
logger.debug("Requesting access token from URL: {}", urlBuilder);
String responseContentAsString = HttpUtil.executeUrl("POST", urlBuilder.toString(), null, null,
"application/x-www-form-urlencoded", 10_000);
AccessTokenData data = NestUtils.fromJson(responseContentAsString, AccessTokenData.class);
logger.debug("Received: {}", data);
String accessToken = data.getAccessToken();
if (accessToken == null || accessToken.isBlank()) {
throw new InvalidAccessTokenException("Pincode to obtain access token is already used or invalid)");
}
return accessToken;
} catch (IOException e) {
throw new InvalidAccessTokenException("Access token request failed", e);
}
}
}

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.nest.internal.rest;
import java.io.IOException;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Inserts Authorization and Cache-Control headers for requests on the streaming REST API.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public class NestStreamingRequestFilter implements ClientRequestFilter {
private final String accessToken;
public NestStreamingRequestFilter(String accessToken) {
this.accessToken = accessToken;
}
@Override
public void filter(@Nullable ClientRequestContext requestContext) throws IOException {
if (requestContext != null) {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
}
}
}

View File

@ -0,0 +1,232 @@
/**
* 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.nest.internal.rest;
import static org.openhab.binding.nest.internal.NestBindingConstants.KEEP_ALIVE_MILLIS;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.sse.InboundSseEvent;
import javax.ws.rs.sse.SseEventSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.data.TopLevelStreamingData;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier;
import org.openhab.binding.nest.internal.listener.NestStreamingDataListener;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A client that generates events based on Nest streaming REST API Server-Sent Events (SSE).
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public class NestStreamingRestClient {
// Assume connection timeout when 2 keep alive message should have been received
private static final long CONNECTION_TIMEOUT_MILLIS = 2 * KEEP_ALIVE_MILLIS + KEEP_ALIVE_MILLIS / 2;
public static final String AUTH_REVOKED = "auth_revoked";
public static final String ERROR = "error";
public static final String KEEP_ALIVE = "keep-alive";
public static final String OPEN = "open";
public static final String PUT = "put";
private final Logger logger = LoggerFactory.getLogger(NestStreamingRestClient.class);
private final String accessToken;
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final NestRedirectUrlSupplier redirectUrlSupplier;
private final ScheduledExecutorService scheduler;
private final Object startStopLock = new Object();
private final List<NestStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
private @Nullable ScheduledFuture<?> checkConnectionJob;
private boolean connected;
private @Nullable SseEventSource eventSource;
private long lastEventTimestamp;
private @Nullable TopLevelData lastReceivedTopLevelData;
public NestStreamingRestClient(String accessToken, ClientBuilder clientBuilder,
SseEventSourceFactory eventSourceFactory, NestRedirectUrlSupplier redirectUrlSupplier,
ScheduledExecutorService scheduler) {
this.accessToken = accessToken;
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.redirectUrlSupplier = redirectUrlSupplier;
this.scheduler = scheduler;
}
private SseEventSource createEventSource() throws FailedResolvingNestUrlException {
Client client = clientBuilder.register(new NestStreamingRequestFilter(accessToken)).build();
SseEventSource eventSource = eventSourceFactory.newSource(client.target(redirectUrlSupplier.getRedirectUrl()));
eventSource.register(this::onEvent, this::onError);
return eventSource;
}
private void checkConnection() {
long millisSinceLastEvent = System.currentTimeMillis() - lastEventTimestamp;
if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
logger.debug("Check: Disconnected from streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
synchronized (startStopLock) {
stopCheckConnectionJob(false);
if (connected) {
connected = false;
listeners.forEach(listener -> listener.onDisconnected());
}
redirectUrlSupplier.resetCache();
reopenEventSource();
startCheckConnectionJob();
}
} else {
logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
}
}
/**
* Closes the existing EventSource and opens a new EventSource as workaround when the EventSource fails to reconnect
* itself.
*/
private void reopenEventSource() {
try {
logger.debug("Reopening EventSource");
closeEventSource(10, TimeUnit.SECONDS);
logger.debug("Opening new EventSource");
SseEventSource localEventSource = createEventSource();
localEventSource.open();
eventSource = localEventSource;
} catch (FailedResolvingNestUrlException e) {
logger.debug("Failed to resolve Nest redirect URL while opening new EventSource");
}
}
public void start() {
synchronized (startStopLock) {
logger.debug("Opening EventSource and starting checkConnection job");
reopenEventSource();
startCheckConnectionJob();
logger.debug("Started");
}
}
public void stop() {
synchronized (startStopLock) {
logger.debug("Closing EventSource and stopping checkConnection job");
stopCheckConnectionJob(true);
closeEventSource(0, TimeUnit.SECONDS);
logger.debug("Stopped");
}
}
private void closeEventSource(long timeout, TimeUnit timeoutUnit) {
SseEventSource localEventSource = eventSource;
if (localEventSource != null) {
if (!localEventSource.isOpen()) {
logger.debug("Existing EventSource is already closed");
} else if (localEventSource.close(timeout, timeoutUnit)) {
logger.debug("Succesfully closed existing EventSource");
} else {
logger.debug("Failed to close existing EventSource");
}
eventSource = null;
}
}
private void startCheckConnectionJob() {
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
checkConnectionJob = scheduler.scheduleWithFixedDelay(this::checkConnection, CONNECTION_TIMEOUT_MILLIS,
KEEP_ALIVE_MILLIS, TimeUnit.MILLISECONDS);
}
}
private void stopCheckConnectionJob(boolean mayInterruptIfRunning) {
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
if (localCheckConnectionJob != null && !localCheckConnectionJob.isCancelled()) {
localCheckConnectionJob.cancel(mayInterruptIfRunning);
checkConnectionJob = null;
}
}
public boolean addStreamingDataListener(NestStreamingDataListener listener) {
return listeners.add(listener);
}
public boolean removeStreamingDataListener(NestStreamingDataListener listener) {
return listeners.remove(listener);
}
public @Nullable TopLevelData getLastReceivedTopLevelData() {
return lastReceivedTopLevelData;
}
private void onEvent(InboundSseEvent inboundEvent) {
try {
lastEventTimestamp = System.currentTimeMillis();
String name = inboundEvent.getName();
String data = inboundEvent.readData();
logger.debug("Received '{}' event, data: {}", name, data);
if (!connected) {
logger.debug("Connected to streaming events");
connected = true;
listeners.forEach(listener -> listener.onConnected());
}
if (AUTH_REVOKED.equals(name)) {
logger.debug("API authorization has been revoked for access token: {}", data);
listeners.forEach(listener -> listener.onAuthorizationRevoked(data));
} else if (ERROR.equals(name)) {
logger.warn("Error occurred: {}", data);
listeners.forEach(listener -> listener.onError(data));
} else if (KEEP_ALIVE.equals(name)) {
logger.debug("Received message to keep connection alive");
} else if (OPEN.equals(name)) {
logger.debug("Event stream opened");
} else if (PUT.equals(name)) {
logger.debug("Data has changed (or initial data sent)");
TopLevelData topLevelData = NestUtils.fromJson(data, TopLevelStreamingData.class).getData();
lastReceivedTopLevelData = topLevelData;
listeners.forEach(listener -> listener.onNewTopLevelData(topLevelData));
} else {
logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
}
} catch (Exception e) {
// catch exceptions here otherwise they will be swallowed by the implementation
logger.warn("An exception occurred while processing the inbound event", e);
}
}
private void onError(Throwable error) {
logger.debug("Error occurred while receiving events", error);
}
}

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.nest.internal.rest;
import java.util.HashMap;
import java.util.Map;
/**
* Contains the data needed to do an update request back to Nest.
*
* @author David Bennett - Initial contribution
*/
public class NestUpdateRequest {
private final String updatePath;
private final Map<String, Object> values;
private NestUpdateRequest(Builder builder) {
this.updatePath = builder.basePath + builder.identifier;
this.values = builder.values;
}
public String getUpdatePath() {
return updatePath;
}
public Map<String, Object> getValues() {
return values;
}
public static class Builder {
private String basePath;
private String identifier;
private Map<String, Object> values = new HashMap<>();
public Builder withBasePath(String basePath) {
this.basePath = basePath;
return this;
}
public Builder withIdentifier(String identifier) {
this.identifier = identifier;
return this;
}
public Builder withAdditionalValue(String field, Object value) {
values.put(field, value);
return this;
}
public NestUpdateRequest build() {
return new NestUpdateRequest(this);
}
}
}

View File

@ -0,0 +1,129 @@
/**
* 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.nest.internal.update;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
/**
* Handles all Nest data updates through delegation to the {@link NestUpdateHandler} for the respective data type.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class NestCompositeUpdateHandler {
private final Supplier<Set<String>> presentNestIdsSupplier;
private final Map<Class<?>, @Nullable NestUpdateHandler<?>> updateHandlersMap = new ConcurrentHashMap<>();
public NestCompositeUpdateHandler(Supplier<Set<String>> presentNestIdsSupplier) {
this.presentNestIdsSupplier = presentNestIdsSupplier;
}
public <T> boolean addListener(Class<T> dataClass, NestThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).addListener(listener);
}
public <T> boolean addListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).addListener(nestId, listener);
}
private Set<String> findMissingNestIds(Set<NestIdentifiable> updates) {
Set<String> nestIds = updates.stream().map(u -> u.getId()).collect(Collectors.toSet());
Set<String> missingNestIds = presentNestIdsSupplier.get();
missingNestIds.removeAll(nestIds);
return missingNestIds;
}
public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
return getOrCreateUpdateHandler(dataClass).getLastUpdate(nestId);
}
public <T> List<T> getLastUpdates(Class<T> dataClass) {
return getOrCreateUpdateHandler(dataClass).getLastUpdates();
}
private Set<NestIdentifiable> getNestUpdates(TopLevelData data) {
Set<NestIdentifiable> updates = new HashSet<>();
if (data.getDevices() != null) {
if (data.getDevices().getCameras() != null) {
updates.addAll(data.getDevices().getCameras().values());
}
if (data.getDevices().getSmokeCoAlarms() != null) {
updates.addAll(data.getDevices().getSmokeCoAlarms().values());
}
if (data.getDevices().getThermostats() != null) {
updates.addAll(data.getDevices().getThermostats().values());
}
}
if (data.getStructures() != null) {
updates.addAll(data.getStructures().values());
}
return updates;
}
@SuppressWarnings("unchecked")
private <T> NestUpdateHandler<T> getOrCreateUpdateHandler(Class<T> dataClass) {
NestUpdateHandler<T> handler = (NestUpdateHandler<T>) updateHandlersMap.get(dataClass);
if (handler == null) {
handler = new NestUpdateHandler<>();
updateHandlersMap.put(dataClass, handler);
}
return handler;
}
@SuppressWarnings("unchecked")
public void handleUpdate(TopLevelData data) {
Set<NestIdentifiable> updates = getNestUpdates(data);
updates.forEach(update -> {
Class<NestIdentifiable> updateClass = (Class<NestIdentifiable>) update.getClass();
getOrCreateUpdateHandler(updateClass).handleUpdate(updateClass, update.getId(), update);
});
Set<String> missingNestIds = findMissingNestIds(updates);
if (!missingNestIds.isEmpty()) {
updateHandlersMap.values().forEach(handler -> {
if (handler != null) {
handler.handleMissingNestIds(missingNestIds);
}
});
}
}
public <T> boolean removeListener(Class<T> dataClass, NestThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).removeListener(listener);
}
public <T> boolean removeListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).removeListener(nestId, listener);
}
public void resendLastUpdates() {
updateHandlersMap.values().forEach(handler -> {
if (handler != null) {
handler.resendLastUpdates();
}
});
}
}

View File

@ -0,0 +1,114 @@
/**
* 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.nest.internal.update;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
/**
* Handles the updates of one type of data by notifying listeners of changes and storing the update value.
*
* @author Wouter Born - Initial contribution
*
* @param <T> the type of update data
*/
@NonNullByDefault
public class NestUpdateHandler<T> {
/**
* The ID used for listeners that subscribe to any Nest update.
*/
private static final String ANY_ID = "*";
private final Map<String, @Nullable T> lastUpdates = new ConcurrentHashMap<>();
private final Map<String, @Nullable Set<NestThingDataListener<T>>> listenersMap = new ConcurrentHashMap<>();
public boolean addListener(NestThingDataListener<T> listener) {
return addListener(ANY_ID, listener);
}
public boolean addListener(String nestId, NestThingDataListener<T> listener) {
return getOrCreateListeners(nestId).add(listener);
}
public @Nullable T getLastUpdate(String nestId) {
return lastUpdates.get(nestId);
}
public List<T> getLastUpdates() {
return new ArrayList<>(lastUpdates.values());
}
private Set<NestThingDataListener<T>> getListeners(String nestId) {
Set<NestThingDataListener<T>> listeners = new HashSet<>();
if (listenersMap.get(nestId) != null) {
listeners.addAll(listenersMap.get(nestId));
}
if (listenersMap.get(ANY_ID) != null) {
listeners.addAll(listenersMap.get(ANY_ID));
}
return listeners;
}
private Set<NestThingDataListener<T>> getOrCreateListeners(String nestId) {
Set<NestThingDataListener<T>> listeners = listenersMap.get(nestId);
if (listeners == null) {
listeners = new CopyOnWriteArraySet<>();
listenersMap.put(nestId, listeners);
}
return listeners;
}
public void handleMissingNestIds(Set<String> nestIds) {
nestIds.forEach(nestId -> {
lastUpdates.remove(nestId);
getListeners(nestId).forEach(l -> l.onMissingData(nestId));
});
}
public void handleUpdate(Class<T> dataClass, String nestId, T update) {
T lastUpdate = getLastUpdate(nestId);
lastUpdates.put(nestId, update);
notifyListeners(nestId, lastUpdate, update);
}
private void notifyListeners(String nestId, @Nullable T lastUpdate, T update) {
Set<NestThingDataListener<T>> listeners = getListeners(nestId);
if (lastUpdate == null) {
listeners.forEach(l -> l.onNewData(update));
} else if (!lastUpdate.equals(update)) {
listeners.forEach(l -> l.onUpdatedData(lastUpdate, update));
}
}
public boolean removeListener(NestThingDataListener<T> listener) {
return removeListener(ANY_ID, listener);
}
public boolean removeListener(String nestId, NestThingDataListener<T> listener) {
return getOrCreateListeners(nestId).remove(listener);
}
public void resendLastUpdates() {
lastUpdates.forEach((nestId, update) -> notifyListeners(nestId, null, update));
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nest" 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>Nest Binding</name>
<description>Nest connects to the Nest cloud and allows control of the various Nest devices.</description>
</binding:binding>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:nest:account">
<parameter-group name="oauth">
<label>Nest API OAuth</label>
<description>The OAuth parameters used when communicating with the Nest API</description>
</parameter-group>
<parameter-group name="binding">
<label>Binding Settings</label>
<description>Local settings</description>
</parameter-group>
<parameter name="productId" type="text" groupName="oauth">
<label>Product ID</label>
<description>The product ID from the Nest product page</description>
<required>true</required>
</parameter>
<parameter name="productSecret" type="text" groupName="oauth">
<label>Product Secret</label>
<description>The product secret from the Nest product page</description>
<required>true</required>
</parameter>
<parameter name="pincode" type="text" groupName="oauth">
<label>Pincode</label>
<description>The single use pincode for obtaining an OAuth access token.
Get the pincode by accepting to the terms
shown at the product authorization URL.
This value is automatically reset when the access token has been obtained</description>
</parameter>
<parameter name="accessToken" type="text" groupName="oauth">
<label>Access Token</label>
<description>The access token used for authenticating to the Nest API.
It is automatically obtained from Nest when the
value is empty and
a valid pincode parameter is entered</description>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:nest:device">
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
</parameter>
</config-description>
<config-description uri="thing-type:nest:structure">
<parameter name="structureId" type="text" required="true">
<label>Structure ID</label>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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">
<bridge-type id="account">
<label>Nest Account</label>
<description>An account for using the Nest REST API</description>
<config-description-ref uri="thing-type:nest:account"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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="camera">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Nest Cam</label>
<description>A Nest Cam registered with your account</description>
<channel-groups>
<channel-group id="camera" typeId="Camera"/>
<channel-group id="last_event" typeId="CameraEvent">
<label>Last Event</label>
<description>Information about the last camera event (requires Nest Aware subscription)</description>
</channel-group>
</channel-groups>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,520 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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">
<!-- Common -->
<channel-type id="LastConnection" advanced="true">
<item-type>DateTime</item-type>
<label>Last Connection</label>
<description>Timestamp of the last successful interaction with Nest</description>
<state readOnly="true"/>
</channel-type>
<!-- Structure -->
<channel-type id="Away">
<item-type>String</item-type>
<label>Away</label>
<description>Away state of the structure</description>
<state>
<options>
<option value="AWAY">Away</option>
<option value="HOME">Home</option>
</options>
</state>
</channel-type>
<channel-type id="CountryCode" advanced="true">
<item-type>String</item-type>
<label>Country Code</label>
<description>Country code of the structure</description>
</channel-type>
<channel-type id="PostalCode" advanced="true">
<item-type>String</item-type>
<label>Postal Code</label>
<description>Postal code of the structure</description>
</channel-type>
<channel-type id="TimeZone">
<item-type>String</item-type>
<label>Time Zone</label>
<description>The time zone for the structure</description>
</channel-type>
<channel-type id="PeakPeriodStartTime" advanced="true">
<item-type>DateTime</item-type>
<label>Peak Period Start Time</label>
<description>Peak period start for the Rush Hour Rewards program</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="PeakPeriodEndTime" advanced="true">
<item-type>DateTime</item-type>
<label>Peak Period End Time</label>
<description>Peak period end for the Rush Hour Rewards program</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="EtaBegin" advanced="true">
<item-type>DateTime</item-type>
<label>ETA</label>
<description>
Estimated time of arrival at home, will setup the heat to turn on and be warm
by the time you arrive
</description>
</channel-type>
<channel-type id="RushHourRewardsEnrollment">
<item-type>Switch</item-type>
<label>Rush Hour Rewards</label>
<description>If rush hour rewards system is enabled or not</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SecurityState">
<item-type>String</item-type>
<label>Security State</label>
<description>Security state of the structure</description>
<state readOnly="true">
<options>
<option value="OK">ok</option>
<option value="DETER">deter</option>
</options>
</state>
</channel-type>
<!-- Camera -->
<channel-group-type id="Camera">
<label>Camera</label>
<description>Information about the camera</description>
<channels>
<channel id="streaming" typeId="Streaming"/>
<channel id="audio_input_enabled" typeId="AudioInputEnabled"/>
<channel id="public_share_enabled" typeId="PublicShareEnabled"/>
<channel id="video_history_enabled" typeId="VideoHistoryEnabled"/>
<channel id="app_url" typeId="AppUrl"/>
<channel id="snapshot_url" typeId="SnapshotUrl"/>
<channel id="public_share_url" typeId="PublicShareUrl"/>
<channel id="web_url" typeId="WebUrl"/>
<channel id="last_online_change" typeId="LastOnlineChange"/>
</channels>
</channel-group-type>
<channel-type id="AudioInputEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Audio Input Enabled</label>
<description>If the audio input is enabled for this camera</description>
</channel-type>
<channel-type id="VideoHistoryEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Video History Enabled</label>
<description>If the video history is enabled for this camera</description>
</channel-type>
<channel-type id="PublicShareEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Public Share Enabled</label>
<description>If the public sharing of this camera is enabled</description>
</channel-type>
<channel-type id="Streaming">
<item-type>Switch</item-type>
<label>Streaming</label>
<description>If the camera is currently streaming</description>
</channel-type>
<channel-type id="WebUrl">
<item-type>String</item-type>
<label>Web URL</label>
<description>The web URL for the camera, allows you to see the camera in a web page</description>
</channel-type>
<channel-type id="PublicShareUrl">
<item-type>String</item-type>
<label>Public Share URL</label>
<description>The publicly available URL for the camera</description>
</channel-type>
<channel-type id="SnapshotUrl" advanced="true">
<item-type>String</item-type>
<label>Snapshot URL</label>
<description>The URL showing a snapshot of the camera</description>
</channel-type>
<channel-type id="AppUrl" advanced="true">
<item-type>String</item-type>
<label>App URL</label>
<description>The app URL for the camera, allows you to see the camera in an app</description>
</channel-type>
<channel-type id="LastOnlineChange" advanced="true">
<item-type>DateTime</item-type>
<label>Last Online Change</label>
<description>Timestamp of the last online status change</description>
<state readOnly="true"/>
</channel-type>
<channel-group-type id="CameraEvent">
<label>Camera Event</label>
<description>Information about the camera event</description>
<channels>
<channel id="has_motion" typeId="CameraEventHasMotion"/>
<channel id="has_sound" typeId="CameraEventHasSound"/>
<channel id="has_person" typeId="CameraEventHasPerson"/>
<channel id="start_time" typeId="CameraEventStartTime"/>
<channel id="end_time" typeId="CameraEventEndTime"/>
<channel id="urls_expire_time" typeId="CameraEventUrlsExpireTime"/>
<channel id="animated_image_url" typeId="CameraEventAnimatedImageUrl"/>
<channel id="app_url" typeId="CameraEventAppUrl"/>
<channel id="image_url" typeId="CameraEventImageUrl"/>
<channel id="web_url" typeId="CameraEventWebUrl"/>
<channel id="activity_zones" typeId="CameraEventActivityZones"/>
</channels>
</channel-group-type>
<channel-type id="CameraEventHasSound" advanced="true">
<item-type>Switch</item-type>
<label>Has Sound</label>
<description>If sound was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventHasMotion" advanced="true">
<item-type>Switch</item-type>
<label>Has Motion</label>
<description>If motion was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventHasPerson" advanced="true">
<item-type>Switch</item-type>
<label>Has Person</label>
<description>If a person was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventStartTime" advanced="true">
<item-type>DateTime</item-type>
<label>Start Time</label>
<description>Timestamp when the camera event started</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventEndTime" advanced="true">
<item-type>DateTime</item-type>
<label>End Time</label>
<description>Timestamp when the camera event ended</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventUrlsExpireTime" advanced="true">
<item-type>DateTime</item-type>
<label>URLs Expire Time</label>
<description>Timestamp when the camera event URLs expire</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventWebUrl" advanced="true">
<item-type>String</item-type>
<label>Web URL</label>
<description>The web URL for the camera event, allows you to see the camera event in a web page</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventAppUrl" advanced="true">
<item-type>String</item-type>
<label>App URL</label>
<description>The app URL for the camera event, allows you to see the camera event in an app</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventImageUrl" advanced="true">
<item-type>String</item-type>
<label>Image URL</label>
<description>The URL showing an image for the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventAnimatedImageUrl" advanced="true">
<item-type>String</item-type>
<label>Animated Image URL</label>
<description>The URL showing an animated image for the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventActivityZones" advanced="true">
<item-type>String</item-type>
<label>Activity Zones</label>
<description>Identifiers for activity zones that detected the event (comma separated)</description>
<state readOnly="true"/>
</channel-type>
<!-- Smoke detector -->
<channel-type id="UiColorState" advanced="true">
<item-type>String</item-type>
<label>UI Color State</label>
<description>Current color state of the protect</description>
<state readOnly="true">
<options>
<option value="GRAY">gray</option>
<option value="GREEN">green</option>
<option value="YELLOW">yellow</option>
<option value="RED">red</option>
</options>
</state>
</channel-type>
<channel-type id="CoAlarmState">
<item-type>String</item-type>
<label>CO Alarm State</label>
<description>Carbon monoxide alarm state</description>
<state readOnly="true">
<options>
<option value="OK">ok</option>
<option value="EMERGENCY">emergency</option>
<option value="WARNING">warning</option>
</options>
</state>
</channel-type>
<channel-type id="SmokeAlarmState">
<item-type>String</item-type>
<label>Smoke Alarm State</label>
<description>Smoke alarm state</description>
<state readOnly="true">
<options>
<option value="OK">ok</option>
<option value="EMERGENCY">emergency</option>
<option value="WARNING">warning</option>
</options>
</state>
</channel-type>
<channel-type id="ManualTestActive" advanced="true">
<item-type>Switch</item-type>
<label>Manual Test Active</label>
<description>If the manual test is currently active</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="LastManualTestTime" advanced="true">
<item-type>DateTime</item-type>
<label>Last Manual Test Time</label>
<description>Timestamp of the last successful manual test</description>
<state readOnly="true"/>
</channel-type>
<!-- Thermostat -->
<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="SetPoint">
<item-type>Number:Temperature</item-type>
<label>Set Point</label>
<description>The set point temperature</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="MaxSetPoint">
<item-type>Number:Temperature</item-type>
<label>Max Set Point</label>
<description>The max set point temperature</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="MinSetPoint">
<item-type>Number:Temperature</item-type>
<label>Min Set Point</label>
<description>The min set point temperature</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="EcoMaxSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Eco Max Set Point</label>
<description>The eco range max set point temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="EcoMinSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Eco Min Set Point</label>
<description>The eco range min set point temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="LockedMaxSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Locked Max Set Point</label>
<description>The locked range max set point temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="LockedMinSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Locked Min Set Point</label>
<description>The locked range min set point temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="Locked" advanced="true">
<item-type>Switch</item-type>
<label>Locked</label>
<description>If the thermostat has the temperature locked to only be within a set range</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="Mode">
<item-type>String</item-type>
<label>Mode</label>
<description>Current mode of the Nest thermostat</description>
<state>
<options>
<option value="OFF">off</option>
<option value="ECO">eco</option>
<option value="HEAT">heating</option>
<option value="COOL">cooling</option>
<option value="HEAT_COOL">heat/cool</option>
</options>
</state>
</channel-type>
<channel-type id="PreviousMode" advanced="true">
<item-type>String</item-type>
<label>Previous Mode</label>
<description>The previous mode of the Nest thermostat</description>
<state readOnly="true">
<options>
<option value="OFF">off</option>
<option value="ECO">eco</option>
<option value="HEAT">heating</option>
<option value="COOL">cooling</option>
<option value="HEAT_COOL">heat/cool</option>
</options>
</state>
</channel-type>
<channel-type id="State" advanced="true">
<item-type>String</item-type>
<label>State</label>
<description>The active state of the Nest thermostat</description>
<state readOnly="true">
<options>
<option value="OFF">off</option>
<option value="HEATING">heating</option>
<option value="COOLING">cooling</option>
</options>
</state>
</channel-type>
<channel-type id="Humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Indicates the current relative humidity</description>
<category>Humidity</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="TimeToTarget">
<item-type>Number:Time</item-type>
<label>Time to Target</label>
<description>Time left to the target temperature approximately</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="CanHeat" advanced="true">
<item-type>Switch</item-type>
<label>Can Heat</label>
<description>If the thermostat can actually turn on heating</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CanCool" advanced="true">
<item-type>Switch</item-type>
<label>Can Cool</label>
<description>If the thermostat can actually turn on cooling</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="FanTimerActive" advanced="true">
<item-type>Switch</item-type>
<label>Fan Timer Active</label>
<description>If the fan timer is engaged</description>
<state/>
</channel-type>
<channel-type id="FanTimerDuration" advanced="true">
<item-type>Number:Time</item-type>
<label>Fan Timer Duration</label>
<description>Length of time that the fan is set to run</description>
<state>
<options>
<option value="15">15 min</option>
<option value="30">30 min</option>
<option value="45">45 min</option>
<option value="60">1 h</option>
<option value="120">2 h</option>
<option value="240">4 h</option>
<option value="480">8 h</option>
<option value="960">16 h</option>
</options>
</state>
</channel-type>
<channel-type id="FanTimerTimeout" advanced="true">
<item-type>DateTime</item-type>
<label>Fan Timer Timeout</label>
<description>Timestamp when the fan stops running</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="HasFan" advanced="true">
<item-type>Switch</item-type>
<label>Has Fan</label>
<description>If the thermostat can control the fan</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="HasLeaf" advanced="true">
<item-type>Switch</item-type>
<label>Has Leaf</label>
<description>If the thermostat is currently in a leaf mode</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SunlightCorrectionEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Sunlight Correction Enabled</label>
<description>If sunlight correction is enabled</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SunlightCorrectionActive" advanced="true">
<item-type>Switch</item-type>
<label>Sunlight Correction Active</label>
<description>If sunlight correction is active</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="UsingEmergencyHeat" advanced="true">
<item-type>Switch</item-type>
<label>Using Emergency Heat</label>
<description>If the system is currently using emergency heat</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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="smoke_detector">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Nest Protect</label>
<description>The smoke detector/Nest Protect for the account</description>
<channels>
<channel id="ui_color_state" typeId="UiColorState"/>
<channel id="low_battery" typeId="system.low-battery"/>
<channel id="co_alarm_state" typeId="CoAlarmState"/>
<channel id="smoke_alarm_state" typeId="SmokeAlarmState"/>
<channel id="manual_test_active" typeId="ManualTestActive"/>
<channel id="last_manual_test_time" typeId="LastManualTestTime"/>
<channel id="last_connection" typeId="LastConnection"/>
</channels>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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="structure">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Nest Structure</label>
<description>The Nest structure defines the house the account has setup on Nest.
You will only have more than one
structure if you have more than one house</description>
<channels>
<channel id="country_code" typeId="CountryCode"/>
<channel id="postal_code" typeId="PostalCode"/>
<channel id="time_zone" typeId="TimeZone"/>
<channel id="peak_period_start_time" typeId="PeakPeriodStartTime"/>
<channel id="peak_period_end_time" typeId="PeakPeriodEndTime"/>
<channel id="rush_hour_rewards_enrollment" typeId="RushHourRewardsEnrollment"/>
<channel id="eta_begin" typeId="EtaBegin"/>
<channel id="co_alarm_state" typeId="CoAlarmState"/>
<channel id="smoke_alarm_state" typeId="SmokeAlarmState"/>
<channel id="security_state" typeId="SecurityState"/>
<channel id="away" typeId="Away"/>
</channels>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>structureId</representation-property>
<config-description-ref uri="thing-type:nest:structure"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
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="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Nest Thermostat</label>
<description>A Thermostat to control the various aspects of the house's HVAC system</description>
<channels>
<channel id="temperature" typeId="Temperature"/>
<channel id="humidity" typeId="Humidity"/>
<channel id="mode" typeId="Mode"/>
<channel id="previous_mode" typeId="PreviousMode"/>
<channel id="state" typeId="State"/>
<channel id="set_point" typeId="SetPoint"/>
<channel id="max_set_point" typeId="MaxSetPoint"/>
<channel id="min_set_point" typeId="MinSetPoint"/>
<channel id="can_heat" typeId="CanHeat"/>
<channel id="can_cool" typeId="CanCool"/>
<channel id="fan_timer_active" typeId="FanTimerActive"/>
<channel id="fan_timer_duration" typeId="FanTimerDuration"/>
<channel id="fan_timer_timeout" typeId="FanTimerTimeout"/>
<channel id="has_fan" typeId="HasFan"/>
<channel id="has_leaf" typeId="HasLeaf"/>
<channel id="sunlight_correction_enabled" typeId="SunlightCorrectionEnabled"/>
<channel id="sunlight_correction_active" typeId="SunlightCorrectionActive"/>
<channel id="using_emergency_heat" typeId="UsingEmergencyHeat"/>
<channel id="eco_max_set_point" typeId="EcoMaxSetPoint"/>
<channel id="eco_min_set_point" typeId="EcoMinSetPoint"/>
<channel id="locked" typeId="Locked"/>
<channel id="locked_max_set_point" typeId="LockedMaxSetPoint"/>
<channel id="locked_min_set_point" typeId="LockedMinSetPoint"/>
<channel id="time_to_target" typeId="TimeToTarget"/>
<channel id="last_connection" typeId="LastConnection"/>
</channels>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
</thing-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="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.persistence.dynamodb</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,174 @@
# Amazon DynamoDB Persistence
This service allows you to persist state updates using the [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) database.
Query functionality is also fully supported.
Features:
* Writing/reading information to relational database systems
* Configurable database table names
* Automatic table creation
## Disclaimer
This service is provided "AS IS", and the user takes full responsibility of any charges or damage to Amazon data.
## Table of Contents
<!-- Using MarkdownTOC plugin for Sublime Text to update the table of contents (TOC) -->
<!-- MarkdownTOC depth=3 autolink=true bracket=round -->
- [Prerequisites](#prerequisites)
- [Setting Up an Amazon Account](#setting-up-an-amazon-account)
- [Configuration](#configuration)
- [Basic configuration](#basic-configuration)
- [Configuration Using Credentials File](#configuration-using-credentials-file)
- [Advanced Configuration](#advanced-configuration)
- [Details](#details)
- [Tables Creation](#tables-creation)
- [Caveats](#caveats)
- [Developer Notes](#developer-notes)
- [Updating Amazon SDK](#updating-amazon-sdk)
<!-- /MarkdownTOC -->
## Prerequisites
You must first set up an Amazon account as described below.
Users are recommended to familiarize themselves with AWS pricing before using this service.
Please note that there might be charges from Amazon when using this service to query/store data to DynamoDB.
See [Amazon DynamoDB pricing pages](https://aws.amazon.com/dynamodb/pricing/) for more details.
Please also note possible [Free Tier](https://aws.amazon.com/free/) benefits.
### Setting Up an Amazon Account
* [Sign up](https://aws.amazon.com/) for Amazon AWS.
* Select the AWS region in the [AWS console](https://console.aws.amazon.com/) using [these instructions](https://docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/getting-started.html#select-region). Note the region identifier in the URL (e.g. `https://eu-west-1.console.aws.amazon.com/console/home?region=eu-west-1` means that region id is `eu-west-1`).
* **Create user for openHAB with IAM**
* Open Services -> IAM -> Users -> Create new Users. Enter `openhab` to _User names_, keep _Generate an access key for each user_ checked, and finally click _Create_.
* _Show User Security Credentials_ and record the keys displayed
* **Configure user policy to have access for dynamodb**
* Open Services -> IAM -> Policies
* Check _AmazonDynamoDBFullAccess_ and click _Policy actions_ -> _Attach_
* Check the user created in step 2 and click _Attach policy_
## Configuration
This service can be configured in the file `services/dynamodb.cfg`.
### Basic configuration
| Property | Default | Required | Description |
| --------- | ------- | :------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| accessKey | | Yes | access key as shown in [Setting up Amazon account](#setting-up-an-amazon-account). |
| secretKey | | Yes | secret key as shown in [Setting up Amazon account](#setting-up-an-amazon-account). |
| region | | Yes | AWS region ID as described in [Setting up Amazon account](#setting-up-an-amazon-account). The region needs to match the region that was used to create the user. |
### Configuration Using Credentials File
Alternatively, instead of specifying `accessKey` and `secretKey`, one can configure a configuration profile file.
| Property | Default | Required | Description |
| ------------------ | ------- | :------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| profilesConfigFile | | Yes | path to the credentials file. For example, `/etc/openhab2/aws_creds`. Please note that the user that runs openHAB must have approriate read rights to the credential file. For more details on the Amazon credential file format, see [Amazon documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html). |
| profile | | Yes | name of the profile to use |
| region | | Yes | AWS region ID as described in Step 2 in [Setting up Amazon account](#setting-up-an-amazon-account). The region needs to match the region that was used to create the user. |
Example of service configuration file (`services/dynamodb.cfg`):
```ini
profilesConfigFile=/etc/openhab2/aws_creds
profile=fooprofile
region=eu-west-1
```
Example of credentials file (`/etc/openhab2/aws_creds`):
````ini
[fooprofile]
aws_access_key_id=testAccessKey
aws_secret_access_key=testSecretKey
````
### Advanced Configuration
In addition to the configuration properties above, the following are also available:
| Property | Default | Required | Description |
| -------------------------- | ---------- | :------: | -------------------------------------------------------------------------------------------------- |
| readCapacityUnits | 1 | No | read capacity for the created tables |
| writeCapacityUnits | 1 | No | write capacity for the created tables |
| tablePrefix | `openhab-` | No | table prefix used in the name of created tables |
| bufferCommitIntervalMillis | 1000 | No | Interval to commit (write) buffered data. In milliseconds. |
| bufferSize | 1000 | No | Internal buffer size in datapoints which is used to batch writes to DynamoDB every `bufferCommitIntervalMillis`. |
Typically you should not need to modify parameters related to buffering.
Refer to Amazon documentation on [provisioned throughput](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ProvisionedThroughput.html) for details on read/write capacity.
All item- and event-related configuration is done in the file `persistence/dynamodb.persist`.
## Details
### Tables Creation
When an item is persisted via this service, a table is created (if necessary).
Currently, the service will create at most two tables for different item types.
The tables will be named `<tablePrefix><item-type>`, where the `<item-type>` is either `bigdecimal` (numeric items) or `string` (string and complex items).
Each table will have three columns: `itemname` (item name), `timeutc` (in ISO 8601 format with millisecond accuracy), and `itemstate` (either a number or string representing item state).
## Buffering
By default, the service is asynchronous which means that data is not written immediately to DynamoDB but instead buffered in-memory.
The size of the buffer, in terms of datapoints, can be configured with `bufferSize`.
Every `bufferCommitIntervalMillis` the whole buffer of data is flushed to DynamoDB.
It is recommended to have the buffering enabled since the synchronous behaviour (writing data immediately) might have adverse impact to the whole system when there is many items persisted at the same time.
The buffering can be disabled by setting `bufferSize` to zero.
The defaults should be suitable in many use cases.
### Caveats
When the tables are created, the read/write capacity is configured according to configuration.
However, the service does not modify the capacity of existing tables.
As a workaround, you can modify the read/write capacity of existing tables using the [Amazon console](https://aws.amazon.com/console/).
## Developer Notes
### Updating Amazon SDK
1. Clean `lib/*`
2. Update SDK version in `scripts/fetch_sdk_pom.xml`. You can use the [maven online repository browser](https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-dynamodb) to find the latest version available online.
3. `scripts/fetch_sdk.sh`
4. Copy `scripts/target/site/dependencies.html` and `scripts/target/dependency/*.jar` to `lib/`
5. Generate `build.properties` entries
`ls lib/*.jar | python -c "import sys; print(' ' + ',\\\\\\n '.join(map(str.strip, sys.stdin.readlines())))"`
6. Generate `META-INF/MANIFEST.MF` `Bundle-ClassPath` entries
`ls lib/*.jar | python -c "import sys; print(' ' + ',\\n '.join(map(str.strip, sys.stdin.readlines())))"`
7. Generate `.classpath` entries
`ls lib/*.jar | python -c "import sys;pre='<classpathentry exported=\"true\" kind=\"lib\" path=\"';post='\"/>'; print('\\t' + pre + (post + '\\n\\t' + pre).join(map(str.strip, sys.stdin.readlines())) + post)"`
After these changes, it's good practice to run integration tests (against live AWS DynamoDB) in `org.openhab.persistence.dynamodb.test` bundle.
See README.md in the test bundle for more information how to execute the tests.
### Running integration tests
To run integration tests, one needs to provide AWS credentials.
Eclipse instructions
1. Run all tests (in package org.openhab.persistence.dynamodb.internal) as JUnit Tests
2. Configure the run configuration, and open Arguments sheet
3. In VM arguments, provide the credentials for AWS
````
-DDYNAMODBTEST_REGION=REGION-ID
-DDYNAMODBTEST_ACCESS=ACCESS-KEY
-DDYNAMODBTEST_SECRET=SECRET
````
The tests will create tables with prefix `dynamodb-integration-tests-`.
Note that when tests are begun, all data is removed from that table!

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.persistence.dynamodb</artifactId>
<name>openHAB Add-ons :: Bundles :: Persistence Service :: DynamoDB</name>
<properties>
<bnd.importpackage>!com.amazonaws.*,!org.joda.convert.*,!com.sun.org.apache.xpath.*,!kotlin,!org.apache.log.*,!org.bouncycastle.*,!org.apache.avalon.*</bnd.importpackage>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-core -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-core</artifactId>
<version>1.11.213</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>1.11.213</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-kms -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-kms</artifactId>
<version>1.11.213</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.213</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/jmespath-java -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>jmespath-java</artifactId>
<version>1.11.213</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.ion/ion-java -->
<dependency>
<groupId>software.amazon.ion</groupId>
<artifactId>ion-java</artifactId>
<version>1.0.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.8.1</version>
</dependency>
<!-- The following dependencies are required for test resolution -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.6.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-cbor -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
<version>2.6.7</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
mvn -f $DIR/fetch_sdk_pom.xml clean process-sources project-info-reports:dependencies
echo "Check $DIR/target/site/dependencies.html and $DIR/target/dependency"

View File

@ -0,0 +1,37 @@
<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>
<groupId>groupId</groupId>
<artifactId>artifactId</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>1.11.213</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>process-sources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${targetdirectory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.persistence.dynamodb-${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-persistence-dynamodb" description="DynamoDB Persistence" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.persistence.dynamodb/${project.version}</bundle>
<configfile finalname="${openhab.conf}/services/dynamodb.cfg" override="false">mvn:${project.groupId}/openhab-addons-external3/${project.version}/cfg/dynamodb</configfile>
</feature>
</features>

View File

@ -0,0 +1,129 @@
/**
* 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.persistence.dynamodb.internal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class for buffered persistence services
*
* @param <T> Type of the state as accepted by the AWS SDK.
*
* @author Sami Salonen - Initial contribution
* @author Kai Kreuzer - Migration to 3.x
*
*/
@NonNullByDefault
public abstract class AbstractBufferedPersistenceService<T> implements PersistenceService {
private static final long BUFFER_OFFER_TIMEOUT_MILLIS = 500;
private final Logger logger = LoggerFactory.getLogger(AbstractBufferedPersistenceService.class);
protected @Nullable BlockingQueue<T> buffer;
private boolean writeImmediately;
protected void resetWithBufferSize(int bufferSize) {
int capacity = Math.max(1, bufferSize);
buffer = new ArrayBlockingQueue<>(capacity, true);
writeImmediately = bufferSize == 0;
}
protected abstract T persistenceItemFromState(String name, State state, ZonedDateTime time);
protected abstract boolean isReadyToStore();
protected abstract void flushBufferedData();
@Override
public void store(Item item) {
store(item, null);
}
@Override
public void store(Item item, @Nullable String alias) {
long storeStart = System.currentTimeMillis();
String uuid = UUID.randomUUID().toString();
if (item.getState() instanceof UnDefType) {
logger.debug("Undefined item state received. Not storing item {}.", item.getName());
return;
}
if (!isReadyToStore()) {
return;
}
if (buffer == null) {
throw new IllegalStateException("Buffer not initialized with resetWithBufferSize. Bug?");
}
ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochMilli(storeStart), ZoneId.systemDefault());
String realName = item.getName();
String name = (alias != null) ? alias : realName;
State state = item.getState();
T persistenceItem = persistenceItemFromState(name, state, time);
logger.trace("store() called with item {}, which was converted to {} [{}]", item, persistenceItem, uuid);
if (writeImmediately) {
logger.debug("Writing immediately item {} [{}]", realName, uuid);
// We want to write everything immediately
// Synchronous behavior to ensure buffer does not get full.
synchronized (this) {
boolean buffered = addToBuffer(persistenceItem);
assert buffered;
flushBufferedData();
}
} else {
long bufferStart = System.currentTimeMillis();
boolean buffered = addToBuffer(persistenceItem);
if (buffered) {
logger.debug("Buffered item {} in {} ms. Total time for store(): {} [{}]", realName,
System.currentTimeMillis() - bufferStart, System.currentTimeMillis() - storeStart, uuid);
} else {
logger.debug(
"Buffer is full. Writing buffered data immediately and trying again. Consider increasing bufferSize");
// Buffer is full, commit it immediately
flushBufferedData();
boolean buffered2 = addToBuffer(persistenceItem);
if (buffered2) {
logger.debug("Buffered item in {} ms (2nd try, flushed buffer in-between) [{}]",
System.currentTimeMillis() - bufferStart, uuid);
} else {
// The unlikely case happened -- buffer got full again immediately
logger.warn("Buffering failed for the second time -- Too small bufferSize? Discarding data [{}]",
uuid);
}
}
}
}
protected boolean addToBuffer(T persistenceItem) {
try {
return buffer != null && buffer.offer(persistenceItem, BUFFER_OFFER_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
logger.warn("Interrupted when trying to buffer data! Dropping data");
return false;
}
}
}

View File

@ -0,0 +1,216 @@
/**
* 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.persistence.dynamodb.internal;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Map;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.CallItem;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.LocationItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.PlayerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all DynamoDBItem. Represents openHAB Item serialized in a suitable format for the database
*
* @param <T> Type of the state as accepted by the AWS SDK.
*
* @author Sami Salonen - Initial contribution
*/
public abstract class AbstractDynamoDBItem<T> implements DynamoDBItem<T> {
public static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT)
.withZone(ZoneId.of("UTC"));
private static final String UNDEFINED_PLACEHOLDER = "<org.openhab.core.types.UnDefType.UNDEF>";
private static final Map<Class<? extends Item>, Class<? extends DynamoDBItem<?>>> ITEM_CLASS_MAP = new HashMap<>();
static {
ITEM_CLASS_MAP.put(CallItem.class, DynamoDBStringItem.class);
ITEM_CLASS_MAP.put(ContactItem.class, DynamoDBBigDecimalItem.class);
ITEM_CLASS_MAP.put(DateTimeItem.class, DynamoDBStringItem.class);
ITEM_CLASS_MAP.put(LocationItem.class, DynamoDBStringItem.class);
ITEM_CLASS_MAP.put(NumberItem.class, DynamoDBBigDecimalItem.class);
ITEM_CLASS_MAP.put(RollershutterItem.class, DynamoDBBigDecimalItem.class);
ITEM_CLASS_MAP.put(StringItem.class, DynamoDBStringItem.class);
ITEM_CLASS_MAP.put(SwitchItem.class, DynamoDBBigDecimalItem.class);
ITEM_CLASS_MAP.put(DimmerItem.class, DynamoDBBigDecimalItem.class); // inherited from SwitchItem (!)
ITEM_CLASS_MAP.put(ColorItem.class, DynamoDBStringItem.class); // inherited from DimmerItem
ITEM_CLASS_MAP.put(PlayerItem.class, DynamoDBStringItem.class);
}
public static final Class<DynamoDBItem<?>> getDynamoItemClass(Class<? extends Item> itemClass)
throws NullPointerException {
@SuppressWarnings("unchecked")
Class<DynamoDBItem<?>> dtoclass = (Class<DynamoDBItem<?>>) ITEM_CLASS_MAP.get(itemClass);
if (dtoclass == null) {
throw new IllegalArgumentException(String.format("Unknown item class %s", itemClass));
}
return dtoclass;
}
private final Logger logger = LoggerFactory.getLogger(AbstractDynamoDBItem.class);
protected String name;
protected T state;
protected ZonedDateTime time;
public AbstractDynamoDBItem(String name, T state, ZonedDateTime time) {
this.name = name;
this.state = state;
this.time = time;
}
public static DynamoDBItem<?> fromState(String name, State state, ZonedDateTime time) {
if (state instanceof DecimalType && !(state instanceof HSBType)) {
// also covers PercentType which is inherited from DecimalType
return new DynamoDBBigDecimalItem(name, ((DecimalType) state).toBigDecimal(), time);
} else if (state instanceof OnOffType) {
return new DynamoDBBigDecimalItem(name,
((OnOffType) state) == OnOffType.ON ? BigDecimal.ONE : BigDecimal.ZERO, time);
} else if (state instanceof OpenClosedType) {
return new DynamoDBBigDecimalItem(name,
((OpenClosedType) state) == OpenClosedType.OPEN ? BigDecimal.ONE : BigDecimal.ZERO, time);
} else if (state instanceof UpDownType) {
return new DynamoDBBigDecimalItem(name,
((UpDownType) state) == UpDownType.UP ? BigDecimal.ONE : BigDecimal.ZERO, time);
} else if (state instanceof DateTimeType) {
return new DynamoDBStringItem(name, ((DateTimeType) state).getZonedDateTime().format(DATEFORMATTER), time);
} else if (state instanceof UnDefType) {
return new DynamoDBStringItem(name, UNDEFINED_PLACEHOLDER, time);
} else if (state instanceof StringListType) {
return new DynamoDBStringItem(name, state.toFullString(), time);
} else {
// HSBType, PointType, PlayPauseType and StringType
return new DynamoDBStringItem(name, state.toFullString(), time);
}
}
@Override
public HistoricItem asHistoricItem(final Item item) {
final State[] state = new State[1];
accept(new DynamoDBItemVisitor() {
@Override
public void visit(DynamoDBStringItem dynamoStringItem) {
if (item instanceof ColorItem) {
state[0] = new HSBType(dynamoStringItem.getState());
} else if (item instanceof LocationItem) {
state[0] = new PointType(dynamoStringItem.getState());
} else if (item instanceof PlayerItem) {
String value = dynamoStringItem.getState();
try {
state[0] = PlayPauseType.valueOf(value);
} catch (IllegalArgumentException e) {
state[0] = RewindFastforwardType.valueOf(value);
}
} else if (item instanceof DateTimeItem) {
try {
// Parse ZoneDateTime from string. DATEFORMATTER assumes UTC in case it is not clear
// from the string (should be).
// We convert to default/local timezone for user convenience (e.g. display)
state[0] = new DateTimeType(ZonedDateTime.parse(dynamoStringItem.getState(), DATEFORMATTER)
.withZoneSameInstant(ZoneId.systemDefault()));
} catch (DateTimeParseException e) {
logger.warn("Failed to parse {} as date. Outputting UNDEF instead",
dynamoStringItem.getState());
state[0] = UnDefType.UNDEF;
}
} else if (dynamoStringItem.getState().equals(UNDEFINED_PLACEHOLDER)) {
state[0] = UnDefType.UNDEF;
} else if (item instanceof CallItem) {
String parts = dynamoStringItem.getState();
String[] strings = parts.split(",");
String orig = strings[0];
String dest = strings[1];
state[0] = new StringListType(orig, dest);
} else {
state[0] = new StringType(dynamoStringItem.getState());
}
}
@Override
public void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
if (item instanceof NumberItem) {
state[0] = new DecimalType(dynamoBigDecimalItem.getState());
} else if (item instanceof DimmerItem) {
state[0] = new PercentType(dynamoBigDecimalItem.getState());
} else if (item instanceof SwitchItem) {
state[0] = dynamoBigDecimalItem.getState().compareTo(BigDecimal.ONE) == 0 ? OnOffType.ON
: OnOffType.OFF;
} else if (item instanceof ContactItem) {
state[0] = dynamoBigDecimalItem.getState().compareTo(BigDecimal.ONE) == 0 ? OpenClosedType.OPEN
: OpenClosedType.CLOSED;
} else if (item instanceof RollershutterItem) {
state[0] = new PercentType(dynamoBigDecimalItem.getState());
} else {
logger.warn("Not sure how to convert big decimal item {} to type {}. Using StringType as fallback",
dynamoBigDecimalItem.getName(), item.getClass());
state[0] = new StringType(dynamoBigDecimalItem.getState().toString());
}
}
});
return new DynamoDBHistoricItem(getName(), state[0], getTime());
}
/**
* We define all getter and setters in the child class implement those. Having the getter
* and setter implementations here in the parent class does not work with introspection done by AWS SDK (1.11.56).
*/
/*
* (non-Javadoc)
*
* @see org.openhab.persistence.dynamodb.internal.DynamoItem#accept(org.openhab.persistence.dynamodb.internal.
* DynamoItemVisitor)
*/
@Override
public abstract void accept(DynamoDBItemVisitor visitor);
@Override
public String toString() {
return DateFormat.getDateTimeInstance().format(time) + ": " + name + " -> " + state.toString();
}
}

View File

@ -0,0 +1,95 @@
/**
* 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.persistence.dynamodb.internal;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.ZonedDateTime;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBDocument;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey;
/**
* DynamoDBItem for items that can be serialized as DynamoDB number
*
* @author Sami Salonen - Initial contribution
*/
@DynamoDBDocument
public class DynamoDBBigDecimalItem extends AbstractDynamoDBItem<BigDecimal> {
/**
* We get the following error if the BigDecimal has too many digits
* "Attempting to store more than 38 significant digits in a Number"
*
* See "Data types" section in
* http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
*/
private static final int MAX_DIGITS_SUPPORTED_BY_AMAZON = 38;
public DynamoDBBigDecimalItem() {
this(null, null, null);
}
public DynamoDBBigDecimalItem(String name, BigDecimal state, ZonedDateTime time) {
super(name, state, time);
}
@DynamoDBAttribute(attributeName = DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE)
@Override
public BigDecimal getState() {
// When serializing this to the wire, we round the number in order to ensure
// that it is within the dynamodb limits
return loseDigits(state);
}
@DynamoDBHashKey(attributeName = DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME)
@Override
public String getName() {
return name;
}
@Override
@DynamoDBRangeKey(attributeName = ATTRIBUTE_NAME_TIMEUTC)
public ZonedDateTime getTime() {
return time;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setState(BigDecimal state) {
this.state = state;
}
@Override
public void setTime(ZonedDateTime time) {
this.time = time;
}
@Override
public void accept(org.openhab.persistence.dynamodb.internal.DynamoDBItemVisitor visitor) {
visitor.visit(this);
}
static BigDecimal loseDigits(BigDecimal number) {
if (number == null) {
return null;
}
return number.round(new MathContext(MAX_DIGITS_SUPPORTED_BY_AMAZON));
}
}

View File

@ -0,0 +1,66 @@
/**
* 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.persistence.dynamodb.internal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
/**
* Shallow wrapper for Dynamo DB wrappers
*
* @author Sami Salonen - Initial contribution
*/
public class DynamoDBClient {
private final Logger logger = LoggerFactory.getLogger(DynamoDBClient.class);
private DynamoDB dynamo;
private AmazonDynamoDB client;
public DynamoDBClient(AWSCredentials credentials, Regions region) {
client = AmazonDynamoDBClientBuilder.standard().withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials)).build();
dynamo = new DynamoDB(client);
}
public DynamoDBClient(DynamoDBConfig clientConfig) {
this(clientConfig.getCredentials(), clientConfig.getRegion());
}
public AmazonDynamoDB getDynamoClient() {
return client;
}
public DynamoDB getDynamoDB() {
return dynamo;
}
public void shutdown() {
dynamo.shutdown();
}
public boolean checkConnection() {
try {
dynamo.listTables(1).firstPage();
} catch (Exception e) {
logger.warn("Got internal server error when trying to list tables: {}", e.getMessage());
return false;
}
return true;
}
}

View File

@ -0,0 +1,195 @@
/**
* 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.persistence.dynamodb.internal;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.profile.ProfilesConfigFile;
import com.amazonaws.regions.Regions;
/**
* Configuration for DynamoDB connections
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class DynamoDBConfig {
public static final String DEFAULT_TABLE_PREFIX = "openhab-";
public static final boolean DEFAULT_CREATE_TABLE_ON_DEMAND = true;
public static final long DEFAULT_READ_CAPACITY_UNITS = 1;
public static final long DEFAULT_WRITE_CAPACITY_UNITS = 1;
public static final long DEFAULT_BUFFER_COMMIT_INTERVAL_MILLIS = 1000;
public static final int DEFAULT_BUFFER_SIZE = 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBConfig.class);
private String tablePrefix = DEFAULT_TABLE_PREFIX;
private Regions region;
private AWSCredentials credentials;
private boolean createTable = DEFAULT_CREATE_TABLE_ON_DEMAND;
private long readCapacityUnits = DEFAULT_READ_CAPACITY_UNITS;
private long writeCapacityUnits = DEFAULT_WRITE_CAPACITY_UNITS;
private long bufferCommitIntervalMillis = DEFAULT_BUFFER_COMMIT_INTERVAL_MILLIS;
private int bufferSize = DEFAULT_BUFFER_SIZE;
/**
*
* @param config persistence service configuration
* @return DynamoDB configuration. Returns null in case of configuration errors
*/
public static @Nullable DynamoDBConfig fromConfig(Map<String, Object> config) {
try {
String regionName = (String) config.get("region");
if (regionName == null) {
return null;
}
final Regions region;
try {
region = Regions.fromName(regionName);
} catch (IllegalArgumentException e) {
LOGGER.error("Specify valid AWS region to use, got {}. Valid values include: {}", regionName, Arrays
.asList(Regions.values()).stream().map(r -> r.getName()).collect(Collectors.joining(",")));
return null;
}
AWSCredentials credentials;
String accessKey = (String) config.get("accessKey");
String secretKey = (String) config.get("secretKey");
if (accessKey != null && !accessKey.isBlank() && secretKey != null && !secretKey.isBlank()) {
LOGGER.debug("accessKey and secretKey specified. Using those.");
credentials = new BasicAWSCredentials(accessKey, secretKey);
} else {
LOGGER.debug("accessKey and/or secretKey blank. Checking profilesConfigFile and profile.");
String profilesConfigFile = (String) config.get("profilesConfigFile");
String profile = (String) config.get("profile");
if (profilesConfigFile == null || profilesConfigFile.isBlank() || profile == null
|| profile.isBlank()) {
LOGGER.error("Specify either 1) accessKey and secretKey; or 2) profilesConfigFile and "
+ "profile for providing AWS credentials");
return null;
}
credentials = new ProfilesConfigFile(profilesConfigFile).getCredentials(profile);
}
String table = (String) config.get("tablePrefix");
if (table == null || table.isBlank()) {
LOGGER.debug("Using default table name {}", DEFAULT_TABLE_PREFIX);
table = DEFAULT_TABLE_PREFIX;
}
final boolean createTable;
String createTableParam = (String) config.get("createTable");
if (createTableParam == null || createTableParam.isBlank()) {
LOGGER.debug("Creating table on demand: {}", DEFAULT_CREATE_TABLE_ON_DEMAND);
createTable = DEFAULT_CREATE_TABLE_ON_DEMAND;
} else {
createTable = Boolean.parseBoolean(createTableParam);
}
final long readCapacityUnits;
String readCapacityUnitsParam = (String) config.get("readCapacityUnits");
if (readCapacityUnitsParam == null || readCapacityUnitsParam.isBlank()) {
LOGGER.debug("Read capacity units: {}", DEFAULT_READ_CAPACITY_UNITS);
readCapacityUnits = DEFAULT_READ_CAPACITY_UNITS;
} else {
readCapacityUnits = Long.parseLong(readCapacityUnitsParam);
}
final long writeCapacityUnits;
String writeCapacityUnitsParam = (String) config.get("writeCapacityUnits");
if (writeCapacityUnitsParam == null || writeCapacityUnitsParam.isBlank()) {
LOGGER.debug("Write capacity units: {}", DEFAULT_WRITE_CAPACITY_UNITS);
writeCapacityUnits = DEFAULT_WRITE_CAPACITY_UNITS;
} else {
writeCapacityUnits = Long.parseLong(writeCapacityUnitsParam);
}
final long bufferCommitIntervalMillis;
String bufferCommitIntervalMillisParam = (String) config.get("bufferCommitIntervalMillis");
if (bufferCommitIntervalMillisParam == null || bufferCommitIntervalMillisParam.isBlank()) {
LOGGER.debug("Buffer commit interval millis: {}", DEFAULT_BUFFER_COMMIT_INTERVAL_MILLIS);
bufferCommitIntervalMillis = DEFAULT_BUFFER_COMMIT_INTERVAL_MILLIS;
} else {
bufferCommitIntervalMillis = Long.parseLong(bufferCommitIntervalMillisParam);
}
final int bufferSize;
String bufferSizeParam = (String) config.get("bufferSize");
if (bufferSizeParam == null || bufferSizeParam.isBlank()) {
LOGGER.debug("Buffer size: {}", DEFAULT_BUFFER_SIZE);
bufferSize = DEFAULT_BUFFER_SIZE;
} else {
bufferSize = Integer.parseInt(bufferSizeParam);
}
return new DynamoDBConfig(region, credentials, table, createTable, readCapacityUnits, writeCapacityUnits,
bufferCommitIntervalMillis, bufferSize);
} catch (Exception e) {
LOGGER.error("Error with configuration", e);
return null;
}
}
public DynamoDBConfig(Regions region, AWSCredentials credentials, String table, boolean createTable,
long readCapacityUnits, long writeCapacityUnits, long bufferCommitIntervalMillis, int bufferSize) {
this.region = region;
this.credentials = credentials;
this.tablePrefix = table;
this.createTable = createTable;
this.readCapacityUnits = readCapacityUnits;
this.writeCapacityUnits = writeCapacityUnits;
this.bufferCommitIntervalMillis = bufferCommitIntervalMillis;
this.bufferSize = bufferSize;
}
public AWSCredentials getCredentials() {
return credentials;
}
public String getTablePrefix() {
return tablePrefix;
}
public Regions getRegion() {
return region;
}
public boolean isCreateTable() {
return createTable;
}
public long getReadCapacityUnits() {
return readCapacityUnits;
}
public long getWriteCapacityUnits() {
return writeCapacityUnits;
}
public long getBufferCommitIntervalMillis() {
return bufferCommitIntervalMillis;
}
public int getBufferSize() {
return bufferSize;
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.persistence.dynamodb.internal;
import java.text.DateFormat;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.types.State;
/**
* This is a Java bean used to return historic items from Dynamodb.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class DynamoDBHistoricItem implements HistoricItem {
private final String name;
private final State state;
private final ZonedDateTime timestamp;
public DynamoDBHistoricItem(String name, State state, ZonedDateTime timestamp) {
this.name = name;
this.state = state;
this.timestamp = timestamp;
}
@Override
public String getName() {
return name;
}
@Override
public ZonedDateTime getTimestamp() {
return timestamp;
}
@Override
public State getState() {
return state;
}
@Override
public String toString() {
return DateFormat.getDateTimeInstance().format(timestamp) + ": " + name + " -> " + state.toString();
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.persistence.dynamodb.internal;
import java.time.ZonedDateTime;
import org.openhab.core.items.Item;
import org.openhab.core.persistence.HistoricItem;
/**
* Represents openHAB Item serialized in a suitable format for the database
*
* @param <T> Type of the state as accepted by the AWS SDK.
*
* @author Sami Salonen - Initial contribution
*/
public interface DynamoDBItem<T> {
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
static final String ATTRIBUTE_NAME_TIMEUTC = "timeutc";
static final String ATTRIBUTE_NAME_ITEMNAME = "itemname";
static final String ATTRIBUTE_NAME_ITEMSTATE = "itemstate";
/**
* Convert this AbstractDynamoItem as HistoricItem.
*
* @param item Item representing this item. Used to determine item type.
* @return HistoricItem representing this DynamoDBItem.
*/
HistoricItem asHistoricItem(Item item);
String getName();
T getState();
ZonedDateTime getTime();
void setName(String name);
void setState(T state);
void setTime(ZonedDateTime time);
void accept(DynamoDBItemVisitor visitor);
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.dynamodb.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Visitor for DynamoDBItem
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public interface DynamoDBItemVisitor {
public void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem);
public void visit(DynamoDBStringItem dynamoStringItem);
}

View File

@ -0,0 +1,569 @@
/**
* 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.persistence.dynamodb.internal;
import java.time.ZonedDateTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceItemInfo;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.strategy.PersistenceStrategy;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.FailedBatch;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.PaginationLoadingStrategy;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression;
import com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList;
import com.amazonaws.services.dynamodbv2.document.BatchWriteItemOutcome;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.model.TableStatus;
import com.amazonaws.services.dynamodbv2.model.WriteRequest;
/**
* This is the implementation of the DynamoDB {@link PersistenceService}. It persists item values
* using the <a href="https://aws.amazon.com/dynamodb/">Amazon DynamoDB</a> database. The states (
* {@link State}) of an {@link Item} are persisted in DynamoDB tables.
*
* The service creates tables automatically, one for numbers, and one for strings.
*
* @see AbstractDynamoDBItem.fromState for details how different items are persisted
*
* @author Sami Salonen - Initial contribution
* @author Kai Kreuzer - Migration to 3.x
*
*/
@NonNullByDefault
@Component(service = { PersistenceService.class,
QueryablePersistenceService.class }, configurationPid = "org.openhab.dynamodb", //
property = Constants.SERVICE_PID + "=org.openhab.dynamodb")
@ConfigurableService(category = "persistence", label = "DynamoDB Persistence Service", description_uri = DynamoDBPersistenceService.CONFIG_URI)
public class DynamoDBPersistenceService extends AbstractBufferedPersistenceService<DynamoDBItem<?>>
implements QueryablePersistenceService {
protected static final String CONFIG_URI = "persistence:dynamodb";
private class ExponentialBackoffRetry implements Runnable {
private int retry;
private Map<String, List<WriteRequest>> unprocessedItems;
private @Nullable Exception lastException;
public ExponentialBackoffRetry(Map<String, List<WriteRequest>> unprocessedItems) {
this.unprocessedItems = unprocessedItems;
}
@Override
public void run() {
logger.debug("Error storing object to dynamo, unprocessed items: {}. Retrying with exponential back-off",
unprocessedItems);
lastException = null;
while (!unprocessedItems.isEmpty() && retry < WAIT_MILLIS_IN_RETRIES.length) {
if (!sleep()) {
// Interrupted
return;
}
retry++;
try {
BatchWriteItemOutcome outcome = DynamoDBPersistenceService.this.db.getDynamoDB()
.batchWriteItemUnprocessed(unprocessedItems);
unprocessedItems = outcome.getUnprocessedItems();
lastException = null;
} catch (AmazonServiceException e) {
if (e instanceof ResourceNotFoundException) {
logger.debug(
"DynamoDB query raised unexpected exception: {}. This might happen if table was recently created",
e.getMessage());
} else {
logger.debug("DynamoDB query raised unexpected exception: {}.", e.getMessage());
}
lastException = e;
continue;
}
}
if (unprocessedItems.isEmpty()) {
logger.debug("After {} retries successfully wrote all unprocessed items", retry);
} else {
logger.warn(
"Even after retries failed to write some items. Last exception: {} {}, unprocessed items: {}",
lastException == null ? "null" : lastException.getClass().getName(),
lastException == null ? "null" : lastException.getMessage(), unprocessedItems);
}
}
private boolean sleep() {
try {
long sleepTime;
if (retry == 1 && lastException != null && lastException instanceof ResourceNotFoundException) {
sleepTime = WAIT_ON_FIRST_RESOURCE_NOT_FOUND_MILLIS;
} else {
sleepTime = WAIT_MILLIS_IN_RETRIES[retry];
}
Thread.sleep(sleepTime);
return true;
} catch (InterruptedException e) {
logger.debug("Interrupted while writing data!");
return false;
}
}
public Map<String, List<WriteRequest>> getUnprocessedItems() {
return unprocessedItems;
}
}
private static final int WAIT_ON_FIRST_RESOURCE_NOT_FOUND_MILLIS = 5000;
private static final int[] WAIT_MILLIS_IN_RETRIES = new int[] { 100, 100, 200, 300, 500 };
private static final String DYNAMODB_THREADPOOL_NAME = "dynamodbPersistenceService";
private final ItemRegistry itemRegistry;
private @Nullable DynamoDBClient db;
private final Logger logger = LoggerFactory.getLogger(DynamoDBPersistenceService.class);
private boolean isProperlyConfigured;
private @NonNullByDefault({}) DynamoDBConfig dbConfig;
private @NonNullByDefault({}) DynamoDBTableNameResolver tableNameResolver;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
new NamedThreadFactory(DYNAMODB_THREADPOOL_NAME));
private @Nullable ScheduledFuture<?> writeBufferedDataFuture;
@Activate
public DynamoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
this.itemRegistry = itemRegistry;
}
/**
* For testing. Allows access to underlying DynamoDBClient.
*
* @return DynamoDBClient connected to AWS Dyanamo DB.
*/
@Nullable
DynamoDBClient getDb() {
return db;
}
@Activate
public void activate(final @Nullable BundleContext bundleContext, final Map<String, Object> config) {
resetClient();
dbConfig = DynamoDBConfig.fromConfig(config);
if (dbConfig == null) {
// Configuration was invalid. Abort service activation.
// Error is already logger in fromConfig.
return;
}
tableNameResolver = new DynamoDBTableNameResolver(dbConfig.getTablePrefix());
try {
if (!ensureClient()) {
logger.error("Error creating dynamodb database client. Aborting service activation.");
return;
}
} catch (Exception e) {
logger.error("Error constructing dynamodb client", e);
return;
}
writeBufferedDataFuture = null;
resetWithBufferSize(dbConfig.getBufferSize());
long commitIntervalMillis = dbConfig.getBufferCommitIntervalMillis();
if (commitIntervalMillis > 0) {
writeBufferedDataFuture = scheduler.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
DynamoDBPersistenceService.this.flushBufferedData();
} catch (RuntimeException e) {
// We want to catch all unexpected exceptions since all unhandled exceptions make
// ScheduledExecutorService halt the regular running of the task.
// It is better to print out the exception, and try again
// (on next cycle)
logger.warn(
"Execution of scheduled flushing of buffered data failed unexpectedly. Ignoring exception, trying again according to configured commit interval of {} ms.",
commitIntervalMillis, e);
}
}
}, 0, commitIntervalMillis, TimeUnit.MILLISECONDS);
}
isProperlyConfigured = true;
logger.debug("dynamodb persistence service activated");
}
@Deactivate
public void deactivate() {
logger.debug("dynamodb persistence service deactivated");
if (writeBufferedDataFuture != null) {
writeBufferedDataFuture.cancel(false);
writeBufferedDataFuture = null;
}
resetClient();
}
/**
* Initializes DynamoDBClient (db field)
*
* If DynamoDBClient constructor throws an exception, error is logged and false is returned.
*
* @return whether initialization was successful.
*/
private boolean ensureClient() {
if (db == null) {
try {
db = new DynamoDBClient(dbConfig);
} catch (Exception e) {
logger.error("Error constructing dynamodb client", e);
return false;
}
}
return true;
}
@Override
public DynamoDBItem<?> persistenceItemFromState(String name, State state, ZonedDateTime time) {
return AbstractDynamoDBItem.fromState(name, state, time);
}
/**
* Create table (if not present) and wait for table to become active.
*
* Synchronized in order to ensure that at most single thread is creating the table at a time
*
* @param mapper
* @param dtoClass
* @return whether table creation succeeded.
*/
private synchronized boolean createTable(DynamoDBMapper mapper, Class<?> dtoClass) {
if (db == null) {
return false;
}
String tableName;
try {
ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput(dbConfig.getReadCapacityUnits(),
dbConfig.getWriteCapacityUnits());
CreateTableRequest request = mapper.generateCreateTableRequest(dtoClass);
request.setProvisionedThroughput(provisionedThroughput);
if (request.getGlobalSecondaryIndexes() != null) {
for (GlobalSecondaryIndex index : request.getGlobalSecondaryIndexes()) {
index.setProvisionedThroughput(provisionedThroughput);
}
}
tableName = request.getTableName();
try {
db.getDynamoClient().describeTable(tableName);
} catch (ResourceNotFoundException e) {
// No table present, continue with creation
db.getDynamoClient().createTable(request);
} catch (AmazonClientException e) {
logger.error("Table creation failed due to error in describeTable operation", e);
return false;
}
// table found or just created, wait
return waitForTableToBecomeActive(tableName);
} catch (AmazonClientException e) {
logger.error("Exception when creating table", e);
return false;
}
}
private boolean waitForTableToBecomeActive(String tableName) {
try {
logger.debug("Checking if table '{}' is created...", tableName);
final TableDescription tableDescription;
try {
tableDescription = db.getDynamoDB().getTable(tableName).waitForActive();
} catch (IllegalArgumentException e) {
logger.warn("Table '{}' is being deleted: {} {}", tableName, e.getClass().getSimpleName(),
e.getMessage());
return false;
} catch (ResourceNotFoundException e) {
logger.warn("Table '{}' was deleted unexpectedly: {} {}", tableName, e.getClass().getSimpleName(),
e.getMessage());
return false;
}
boolean success = TableStatus.ACTIVE.equals(TableStatus.fromValue(tableDescription.getTableStatus()));
if (success) {
logger.debug("Creation of table '{}' successful, table status is now {}", tableName,
tableDescription.getTableStatus());
} else {
logger.warn("Creation of table '{}' unsuccessful, table status is now {}", tableName,
tableDescription.getTableStatus());
}
return success;
} catch (AmazonClientException e) {
logger.error("Exception when checking table status (describe): {}", e.getMessage());
return false;
} catch (InterruptedException e) {
logger.error("Interrupted while trying to check table status: {}", e.getMessage());
return false;
}
}
private void resetClient() {
if (db == null) {
return;
}
db.shutdown();
db = null;
dbConfig = null;
tableNameResolver = null;
isProperlyConfigured = false;
}
private DynamoDBMapper getDBMapper(String tableName) {
try {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig.Builder()
.withTableNameOverride(new DynamoDBMapperConfig.TableNameOverride(tableName))
.withPaginationLoadingStrategy(PaginationLoadingStrategy.LAZY_LOADING).build();
return new DynamoDBMapper(db.getDynamoClient(), mapperConfig);
} catch (AmazonClientException e) {
logger.error("Error getting db mapper: {}", e.getMessage());
throw e;
}
}
@Override
protected boolean isReadyToStore() {
return isProperlyConfigured && ensureClient();
}
@Override
public String getId() {
return "dynamodb";
}
@Override
public String getLabel(@Nullable Locale locale) {
return "DynamoDB";
}
@Override
public Set<PersistenceItemInfo> getItemInfo() {
return Collections.emptySet();
}
@Override
protected void flushBufferedData() {
if (buffer != null && buffer.isEmpty()) {
return;
}
logger.debug("Writing buffered data. Buffer size: {}", buffer.size());
for (;;) {
Map<String, Deque<DynamoDBItem<?>>> itemsByTable = readBuffer();
// Write batch of data, one table at a time
for (Entry<String, Deque<DynamoDBItem<?>>> entry : itemsByTable.entrySet()) {
String tableName = entry.getKey();
Deque<DynamoDBItem<?>> batch = entry.getValue();
if (!batch.isEmpty()) {
flushBatch(getDBMapper(tableName), batch);
}
}
if (buffer != null && buffer.isEmpty()) {
break;
}
}
}
private Map<String, Deque<DynamoDBItem<?>>> readBuffer() {
Map<String, Deque<DynamoDBItem<?>>> batchesByTable = new HashMap<>(2);
// Get batch of data
while (!buffer.isEmpty()) {
DynamoDBItem<?> dynamoItem = buffer.poll();
if (dynamoItem == null) {
break;
}
String tableName = tableNameResolver.fromItem(dynamoItem);
Deque<DynamoDBItem<?>> batch = batchesByTable.computeIfAbsent(tableName, new Function<>() {
@Override
public Deque<DynamoDBItem<?>> apply(String t) {
return new ArrayDeque<>();
}
});
batch.add(dynamoItem);
}
return batchesByTable;
}
/**
* Flush batch of data to DynamoDB
*
* @param mapper mapper associated with the batch
* @param batch batch of data to write to DynamoDB
*/
private void flushBatch(DynamoDBMapper mapper, Deque<DynamoDBItem<?>> batch) {
long currentTimeMillis = System.currentTimeMillis();
List<FailedBatch> failed = mapper.batchSave(batch);
for (FailedBatch failedBatch : failed) {
if (failedBatch.getException() instanceof ResourceNotFoundException) {
// Table did not exist. Try again after creating table
retryFlushAfterCreatingTable(mapper, batch, failedBatch);
} else {
logger.debug("Batch failed with {}. Retrying next with exponential back-off",
failedBatch.getException().getMessage());
new ExponentialBackoffRetry(failedBatch.getUnprocessedItems()).run();
}
}
if (failed.isEmpty()) {
logger.debug("flushBatch ended with {} items in {} ms: {}", batch.size(),
System.currentTimeMillis() - currentTimeMillis, batch);
} else {
logger.warn(
"flushBatch ended with {} items in {} ms: {}. There were some failed batches that were retried -- check logs for ERRORs to see if writes were successful",
batch.size(), System.currentTimeMillis() - currentTimeMillis, batch);
}
}
/**
* Retry flushing data after creating table associated with mapper
*
* @param mapper mapper associated with the batch
* @param batch original batch of data. Used for logging and to determine table name
* @param failedBatch failed batch that should be retried
*/
private void retryFlushAfterCreatingTable(DynamoDBMapper mapper, Deque<DynamoDBItem<?>> batch,
FailedBatch failedBatch) {
logger.debug("Table was not found. Trying to create table and try saving again");
if (createTable(mapper, batch.peek().getClass())) {
logger.debug("Table creation successful, trying to save again");
if (!failedBatch.getUnprocessedItems().isEmpty()) {
ExponentialBackoffRetry retry = new ExponentialBackoffRetry(failedBatch.getUnprocessedItems());
retry.run();
if (retry.getUnprocessedItems().isEmpty()) {
logger.debug("Successfully saved items after table creation");
}
}
} else {
logger.warn("Table creation failed. Not storing some parts of batch: {}. Unprocessed items: {}", batch,
failedBatch.getUnprocessedItems());
}
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
logger.debug("got a query");
if (!isProperlyConfigured) {
logger.debug("Configuration for dynamodb not yet loaded or broken. Not storing item.");
return Collections.emptyList();
}
if (!ensureClient()) {
logger.warn("DynamoDB not connected. Not storing item.");
return Collections.emptyList();
}
String itemName = filter.getItemName();
Item item = getItemFromRegistry(itemName);
if (item == null) {
logger.warn("Could not get item {} from registry!", itemName);
return Collections.emptyList();
}
Class<DynamoDBItem<?>> dtoClass = AbstractDynamoDBItem.getDynamoItemClass(item.getClass());
String tableName = tableNameResolver.fromClass(dtoClass);
DynamoDBMapper mapper = getDBMapper(tableName);
logger.debug("item {} (class {}) will be tried to query using dto class {} from table {}", itemName,
item.getClass(), dtoClass, tableName);
List<HistoricItem> historicItems = new ArrayList<>();
DynamoDBQueryExpression<DynamoDBItem<?>> queryExpression = DynamoDBQueryUtils.createQueryExpression(dtoClass,
filter);
@SuppressWarnings("rawtypes")
final PaginatedQueryList<? extends DynamoDBItem> paginatedList;
try {
paginatedList = mapper.query(dtoClass, queryExpression);
} catch (AmazonServiceException e) {
logger.error(
"DynamoDB query raised unexpected exception: {}. Returning empty collection. "
+ "Status code 400 (resource not found) might occur if table was just created.",
e.getMessage());
return Collections.emptyList();
}
for (int itemIndexOnPage = 0; itemIndexOnPage < filter.getPageSize(); itemIndexOnPage++) {
int itemIndex = filter.getPageNumber() * filter.getPageSize() + itemIndexOnPage;
DynamoDBItem<?> dynamoItem;
try {
dynamoItem = paginatedList.get(itemIndex);
} catch (IndexOutOfBoundsException e) {
logger.debug("Index {} is out-of-bounds", itemIndex);
break;
}
if (dynamoItem != null) {
HistoricItem historicItem = dynamoItem.asHistoricItem(item);
logger.trace("Dynamo item {} converted to historic item: {}", item, historicItem);
historicItems.add(historicItem);
}
}
return historicItems;
}
/**
* Retrieves the item for the given name from the item registry
*
* @param itemName
* @return item with the given name, or null if no such item exists in item registry.
*/
private @Nullable Item getItemFromRegistry(String itemName) {
Item item = null;
try {
if (itemRegistry != null) {
item = itemRegistry.getItem(itemName);
}
} catch (ItemNotFoundException e1) {
logger.error("Unable to get item {} from registry", itemName);
}
return item;
}
@Override
public List<PersistenceStrategy> getDefaultStrategies() {
return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE);
}
}

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