mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Merge branch 'main' of https://github.com/cipianpascu/openhab-addons into feature/s-bus
This commit is contained in:
commit
2c07129ee9
23
.github/stale.yml
vendored
23
.github/stale.yml
vendored
@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale (two month)
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed (another six month)
|
||||
daysUntilClose: 180
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- "PR pending"
|
||||
# Only issues with all of these labels are checked if stale.
|
||||
onlyLabels:
|
||||
- "awaiting feedback"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
# Limit to only `issues``
|
||||
only: issues
|
38
.github/workflows/ci-build.yml
vendored
38
.github/workflows/ci-build.yml
vendored
@ -18,24 +18,24 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java: [ '17' ]
|
||||
maven: [ '3.9.4' ]
|
||||
os: [ 'ubuntu-22.04' ]
|
||||
java: [ '21' ]
|
||||
maven: [ '3.9.9' ]
|
||||
os: [ 'ubuntu-24.04' ]
|
||||
name: Build (Java ${{ matrix.java }}, ${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.head_ref == ''
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout merge
|
||||
if: github.head_ref != ''
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: refs/pull/${{github.event.pull_request.number}}/merge
|
||||
|
||||
- name: Set up Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.m2/repository
|
||||
@ -45,18 +45,18 @@ jobs:
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: Set up Java ${{ matrix.java }}
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: ${{ matrix.java }}
|
||||
|
||||
- name: Set up Maven ${{ matrix.maven }}
|
||||
uses: stCarolas/setup-maven@v4.5
|
||||
uses: stCarolas/setup-maven@v5
|
||||
with:
|
||||
maven-version: ${{ matrix.maven }}
|
||||
|
||||
- name: Register Problem Matchers
|
||||
if: ${{ matrix.java == '17' }}
|
||||
if: ${{ matrix.java == '21' }}
|
||||
id: problem_matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/openhab-compile-problems.json"
|
||||
@ -64,7 +64,7 @@ jobs:
|
||||
- name: Get Changed Files
|
||||
if: github.head_ref != ''
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@v2.2.0
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
format: 'csv'
|
||||
|
||||
@ -81,22 +81,34 @@ jobs:
|
||||
|
||||
- name: Upload Build Log
|
||||
if: ${{ always() && ((steps.build.outcome == 'success') || (steps.build.outcome == 'failure')) }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-log-java-${{ matrix.java }}-${{ matrix.os }}
|
||||
path: build.log
|
||||
|
||||
- name: Upload SAT Summary Report
|
||||
if: ${{ always() && ((steps.build.outcome == 'success') || (steps.build.outcome == 'failure')) }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sat-summary-report
|
||||
path: target/summary_report.html
|
||||
|
||||
- name: Report SAT Errors as Annotations
|
||||
if: ${{ matrix.java == '17' && always() && ((steps.build.outcome == 'success') || (steps.build.outcome == 'failure')) }}
|
||||
if: ${{ matrix.java == '21' && always() && ((steps.build.outcome == 'success') || (steps.build.outcome == 'failure')) }}
|
||||
uses: ghys/checkstyle-github-action@main
|
||||
with:
|
||||
title: CheckStyle Violations
|
||||
path: '**/checkstyle-result.xml'
|
||||
mode: inline
|
||||
|
||||
- name: Verify Changed Files
|
||||
uses: tj-actions/verify-changed-files@v20
|
||||
id: verify-changed-files
|
||||
|
||||
- name: Fail on Changed Files
|
||||
if: steps.verify-changed-files.outputs.changed_files != ''
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||
run: |
|
||||
echo "::error::Files have changed: $CHANGED_FILES"
|
||||
exit 1
|
||||
|
27
.github/workflows/stale-issues.yml
vendored
Normal file
27
.github/workflows/stale-issues.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: 'Stale issues check'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
name: Stale issues check
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Stale issues check
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 180
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
only-labels: 'awaiting feedback'
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'pinned,security,PR pending'
|
||||
stale-issue-message: >-
|
||||
This issue has been automatically marked as stale because it has not had recent activity.
|
||||
It will be closed if no further activity occurs. Thank you for your contributions.
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,8 +5,7 @@
|
||||
.DS_Store
|
||||
.gradle
|
||||
*.iml
|
||||
npm-debug.log
|
||||
.build.log
|
||||
*.log
|
||||
|
||||
.metadata/
|
||||
bin/
|
||||
|
66
CODEOWNERS
66
CODEOWNERS
@ -9,26 +9,29 @@
|
||||
/bundles/org.openhab.automation.jrubyscripting/ @ccutrer @jimtng
|
||||
/bundles/org.openhab.automation.jsscripting/ @jpg0 @florian-h05
|
||||
/bundles/org.openhab.automation.jsscriptingnashorn/ @wborn
|
||||
/bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.automation.jythonscripting/ @HolgerHees
|
||||
/bundles/org.openhab.automation.pidcontroller/ @fwolter
|
||||
/bundles/org.openhab.automation.pwm/ @fwolter
|
||||
/bundles/org.openhab.binding.adorne/ @theiding
|
||||
/bundles/org.openhab.binding.ahawastecollection/ @soenkekueper
|
||||
/bundles/org.openhab.binding.airq/ @aurelio1
|
||||
/bundles/org.openhab.binding.airgradient/ @austvik
|
||||
/bundles/org.openhab.binding.airq/ @aurelio1 @fwolter
|
||||
/bundles/org.openhab.binding.airquality/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.airvisualnode/ @3cky
|
||||
/bundles/org.openhab.binding.alarmdecoder/ @bobadair @billfor
|
||||
/bundles/org.openhab.binding.allplay/ @dominicdesu
|
||||
/bundles/org.openhab.binding.amazondashbutton/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.amazonechocontrol/ @mgeramb
|
||||
/bundles/org.openhab.binding.amberelectric/ @psmedley
|
||||
/bundles/org.openhab.binding.ambientweather/ @mhilbush
|
||||
/bundles/org.openhab.binding.amplipi/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.androiddebugbridge/ @GiviMAD
|
||||
/bundles/org.openhab.binding.androidtv/ @morph166955
|
||||
/bundles/org.openhab.binding.anel/ @paphko
|
||||
/bundles/org.openhab.binding.anthem/ @mhilbush
|
||||
/bundles/org.openhab.binding.asuswrt/ @wildcs
|
||||
/bundles/org.openhab.binding.argoclima/ @mbronk
|
||||
/bundles/org.openhab.binding.astro/ @gerrieg
|
||||
/bundles/org.openhab.binding.asuswrt/ @wildcs
|
||||
/bundles/org.openhab.binding.atlona/ @mlobstein
|
||||
/bundles/org.openhab.binding.autelis/ @digitaldan
|
||||
/bundles/org.openhab.binding.automower/ @maxpg
|
||||
@ -47,6 +50,7 @@
|
||||
/bundles/org.openhab.binding.bluetooth.generic/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.grundfosalpha/ @tisoft
|
||||
/bundles/org.openhab.binding.bluetooth.hdpowerview/ @andrewfg
|
||||
/bundles/org.openhab.binding.bluetooth.radoneye/ @petero-dk
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
@ -54,6 +58,7 @@
|
||||
/bundles/org.openhab.binding.boschindego/ @jofleck @jlaur
|
||||
/bundles/org.openhab.binding.boschshc/ @david-pace @GerdZanker
|
||||
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
|
||||
/bundles/org.openhab.binding.broadlink/ @dag81
|
||||
/bundles/org.openhab.binding.broadlinkthermostat/ @flo-02-mu
|
||||
/bundles/org.openhab.binding.bsblan/ @hypetsch
|
||||
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
|
||||
@ -77,6 +82,7 @@
|
||||
/bundles/org.openhab.binding.digitalstrom/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor
|
||||
/bundles/org.openhab.binding.dmx/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.dolbycp/ @Cybso
|
||||
/bundles/org.openhab.binding.dominoswiss/ @Friesoch
|
||||
/bundles/org.openhab.binding.doorbird/ @mhilbush
|
||||
/bundles/org.openhab.binding.draytonwiser/ @andrew-schofield
|
||||
@ -91,15 +97,18 @@
|
||||
/bundles/org.openhab.binding.ecovacs/ @maniac103
|
||||
/bundles/org.openhab.binding.ecowatt/ @lolodomo
|
||||
/bundles/org.openhab.binding.ekey/ @hmerk
|
||||
/bundles/org.openhab.binding.electroluxair/ @jannegpriv
|
||||
/bundles/org.openhab.binding.electroluxappliance/ @jannegpriv
|
||||
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
|
||||
/bundles/org.openhab.binding.elroconnects/ @mherwege
|
||||
/bundles/org.openhab.binding.emotiva/ @espenaf
|
||||
/bundles/org.openhab.binding.energenie/ @hmerk
|
||||
/bundles/org.openhab.binding.energidataservice/ @jlaur
|
||||
/bundles/org.openhab.binding.enigma2/ @gdolfen
|
||||
/bundles/org.openhab.binding.enocean/ @fruggy83
|
||||
/bundles/org.openhab.binding.enphase/ @Hilbrand
|
||||
/bundles/org.openhab.binding.entsoe/ @jmelhus
|
||||
/bundles/org.openhab.binding.enturno/ @klocsson
|
||||
/bundles/org.openhab.binding.ephemeris/ @clinique
|
||||
/bundles/org.openhab.binding.epsonprojector/ @mlobstein
|
||||
/bundles/org.openhab.binding.etherrain/ @dfad1469
|
||||
/bundles/org.openhab.binding.evcc/ @florian-h05
|
||||
@ -107,14 +116,19 @@
|
||||
/bundles/org.openhab.binding.exec/ @kgoderis
|
||||
/bundles/org.openhab.binding.feed/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.feican/ @Hilbrand
|
||||
/bundles/org.openhab.binding.fenecon/ @nixoso
|
||||
/bundles/org.openhab.binding.fineoffsetweatherstation/ @Andy2003
|
||||
/bundles/org.openhab.binding.flicbutton/ @pfink
|
||||
/bundles/org.openhab.binding.flume/ @jsjames
|
||||
/bundles/org.openhab.binding.fmiweather/ @ssalonen
|
||||
/bundles/org.openhab.binding.folderwatcher/ @goopilot
|
||||
/bundles/org.openhab.binding.folding/ @fa2k
|
||||
/bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand
|
||||
/bundles/org.openhab.binding.freeathome/ @andrasU
|
||||
/bundles/org.openhab.binding.freebox/ @lolodomo
|
||||
/bundles/org.openhab.binding.freeboxos/ @clinique
|
||||
/bundles/org.openhab.binding.freecurrency/ @J-N-K
|
||||
/bundles/org.openhab.binding.frenchgovtenergydata/ @clinique
|
||||
/bundles/org.openhab.binding.fronius/ @trokohl
|
||||
/bundles/org.openhab.binding.fsinternetradio/ @paphko
|
||||
/bundles/org.openhab.binding.ftpupload/ @paulianttila
|
||||
@ -127,8 +141,10 @@
|
||||
/bundles/org.openhab.binding.gpio/ @nils-bauer
|
||||
/bundles/org.openhab.binding.gpstracker/ @gbicskei
|
||||
/bundles/org.openhab.binding.gree/ @markus7017
|
||||
/bundles/org.openhab.binding.gridbox/ @benediktkuntz
|
||||
/bundles/org.openhab.binding.groheondus/ @FlorianSW
|
||||
/bundles/org.openhab.binding.groupepsa/ @arjanmels
|
||||
/bundles/org.openhab.binding.growatt/ @andrewfg
|
||||
/bundles/org.openhab.binding.guntamatic/ @MikeTheTux
|
||||
/bundles/org.openhab.binding.haassohnpelletstove/ @chingon007
|
||||
/bundles/org.openhab.binding.harmonyhub/ @digitaldan
|
||||
@ -144,17 +160,19 @@
|
||||
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
|
||||
/bundles/org.openhab.binding.homewizard/ @Daniel-42
|
||||
/bundles/org.openhab.binding.hpprinter/ @cossey
|
||||
/bundles/org.openhab.binding.http/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.http/ @J-N-K
|
||||
/bundles/org.openhab.binding.hue/ @cweitkamp @andrewfg
|
||||
/bundles/org.openhab.binding.huesync/ @pgfeller
|
||||
/bundles/org.openhab.binding.hydrawise/ @digitaldan
|
||||
/bundles/org.openhab.binding.hyperion/ @tavalin
|
||||
/bundles/org.openhab.binding.iammeter/ @lewei50
|
||||
/bundles/org.openhab.binding.iaqualink/ @digitaldan
|
||||
/bundles/org.openhab.binding.icalendar/ @daMihe
|
||||
/bundles/org.openhab.binding.icalendar/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.icloud/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.ihc/ @paulianttila
|
||||
/bundles/org.openhab.binding.insteon/ @robnielsen
|
||||
/bundles/org.openhab.binding.insteon/ @jsetton
|
||||
/bundles/org.openhab.binding.intesis/ @hmerk
|
||||
/bundles/org.openhab.binding.iotawatt/ @PRosenb
|
||||
/bundles/org.openhab.binding.ipcamera/ @Skinah
|
||||
/bundles/org.openhab.binding.ipobserver/ @Skinah
|
||||
/bundles/org.openhab.binding.ipp/ @peuter
|
||||
@ -180,6 +198,7 @@
|
||||
/bundles/org.openhab.binding.lgtvserial/ @fa2k
|
||||
/bundles/org.openhab.binding.lgwebos/ @sprehn
|
||||
/bundles/org.openhab.binding.lifx/ @wborn
|
||||
/bundles/org.openhab.binding.linktap/ @dag81
|
||||
/bundles/org.openhab.binding.linky/ @clinique @lolodomo
|
||||
/bundles/org.openhab.binding.linuxinput/ @t-8ch
|
||||
/bundles/org.openhab.binding.liquidcheck/ @marcelGoerentz
|
||||
@ -199,9 +218,11 @@
|
||||
/bundles/org.openhab.binding.mecmeter/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep
|
||||
/bundles/org.openhab.binding.mercedesme/ @weymann
|
||||
/bundles/org.openhab.binding.meteoalerte/ @clinique
|
||||
/bundles/org.openhab.binding.meteoblue/ @9037568
|
||||
/bundles/org.openhab.binding.meteofrance/ @clinique
|
||||
/bundles/org.openhab.binding.meteostick/ @cdjackson
|
||||
/bundles/org.openhab.binding.metofficedatahub/ @dag81
|
||||
/bundles/org.openhab.binding.mffan/ @mark-brooks-180
|
||||
/bundles/org.openhab.binding.miele/ @kgoderis @jlaur
|
||||
/bundles/org.openhab.binding.mielecloud/ @BjoernLange
|
||||
/bundles/org.openhab.binding.mihome/ @pboos
|
||||
@ -213,22 +234,26 @@
|
||||
/bundles/org.openhab.binding.modbus/ @ssalonen
|
||||
/bundles/org.openhab.binding.modbus.e3dc/ @weymann
|
||||
/bundles/org.openhab.binding.modbus.helioseasycontrols/ @bern77
|
||||
/bundles/org.openhab.binding.modbus.kermi/ @KaaNee
|
||||
/bundles/org.openhab.binding.modbus.sbc/ @fwolter
|
||||
/bundles/org.openhab.binding.modbus.stiebeleltron/ @pail23
|
||||
/bundles/org.openhab.binding.modbus.studer/ @giovannimirulla
|
||||
/bundles/org.openhab.binding.modbus.sungrow/ @soenkekueper
|
||||
/bundles/org.openhab.binding.modbus.sunspec/ @mrbig
|
||||
/bundles/org.openhab.binding.monopriceaudio/ @mlobstein
|
||||
/bundles/org.openhab.binding.mpd/ @stefanroellin
|
||||
/bundles/org.openhab.binding.mqtt/ @ccutrer
|
||||
/bundles/org.openhab.binding.mqtt.espmilighthub/ @Skinah
|
||||
/bundles/org.openhab.binding.mqtt.fpp/ @computergeek1507
|
||||
/bundles/org.openhab.binding.mqtt.generic/ @ccutrer
|
||||
/bundles/org.openhab.binding.mqtt.homeassistant/ @antroids @ccutrer
|
||||
/bundles/org.openhab.binding.mqtt.homie/ @ccutrer
|
||||
/bundles/org.openhab.binding.mqtt.ruuvigateway/ @ssalonen
|
||||
/bundles/org.openhab.binding.mycroft/ @dalgwen
|
||||
/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl
|
||||
/bundles/org.openhab.binding.mycroft/ @dalgwen
|
||||
/bundles/org.openhab.binding.mynice/ @clinique
|
||||
/bundles/org.openhab.binding.mystrom/ @pail23
|
||||
/bundles/org.openhab.binding.myuplink/ @alexf2015
|
||||
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn
|
||||
/bundles/org.openhab.binding.neato/ @jjlauterbach
|
||||
/bundles/org.openhab.binding.neeo/ @morph166955
|
||||
@ -238,7 +263,6 @@
|
||||
/bundles/org.openhab.binding.network/ @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.nobohub/ @espenaf
|
||||
@ -266,8 +290,10 @@
|
||||
/bundles/org.openhab.binding.orvibo/ @tavalin
|
||||
/bundles/org.openhab.binding.panasonicbdp/ @mlobstein
|
||||
/bundles/org.openhab.binding.paradoxalarm/ @theater
|
||||
/bundles/org.openhab.binding.pegelonline/ @weymann
|
||||
/bundles/org.openhab.binding.pentair/ @jsjames
|
||||
/bundles/org.openhab.binding.phc/ @gnlpfjh
|
||||
/bundles/org.openhab.binding.pihole/ @magx2
|
||||
/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler
|
||||
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
|
||||
/bundles/org.openhab.binding.pixometer/ @Confectrician
|
||||
@ -287,12 +313,13 @@
|
||||
/bundles/org.openhab.binding.pushsafer/ @appzer @cweitkamp
|
||||
/bundles/org.openhab.binding.qbus/ @QbusKoen
|
||||
/bundles/org.openhab.binding.qolsysiq/ @digitaldan
|
||||
/bundles/org.openhab.binding.radiobrowser/ @skinah
|
||||
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
|
||||
/bundles/org.openhab.binding.regoheatpump/ @crnjan
|
||||
/bundles/org.openhab.binding.revogi/ @andibraeu
|
||||
/bundles/org.openhab.binding.remoteopenhab/ @lolodomo
|
||||
/bundles/org.openhab.binding.renault/ @dougculnane
|
||||
/bundles/org.openhab.binding.resol/ @ramack
|
||||
/bundles/org.openhab.binding.revogi/ @andibraeu
|
||||
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
|
||||
/bundles/org.openhab.binding.rme/ @kgoderis
|
||||
/bundles/org.openhab.binding.robonect/ @reyem
|
||||
@ -300,7 +327,9 @@
|
||||
/bundles/org.openhab.binding.rotel/ @lolodomo
|
||||
/bundles/org.openhab.binding.russound/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.sagercaster/ @clinique
|
||||
/bundles/org.openhab.binding.samsungtv/ @paulianttila
|
||||
/bundles/org.openhab.binding.saicismart/ @tisoft @dougculnane
|
||||
/bundles/org.openhab.binding.salus/ @magx2
|
||||
/bundles/org.openhab.binding.samsungtv/ @NickWaterton
|
||||
/bundles/org.openhab.binding.satel/ @druciak
|
||||
/bundles/org.openhab.binding.semsportal/ @itb3
|
||||
/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP @eguib
|
||||
@ -311,6 +340,7 @@
|
||||
/bundles/org.openhab.binding.serial/ @MikeJMajor
|
||||
/bundles/org.openhab.binding.serialbutton/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.shelly/ @markus7017
|
||||
/bundles/org.openhab.binding.siemenshvac/ @lo92fr
|
||||
/bundles/org.openhab.binding.siemensrds/ @andrewfg
|
||||
/bundles/org.openhab.binding.silvercrestwifisocket/ @jmvaz
|
||||
/bundles/org.openhab.binding.sinope/ @chaton78
|
||||
@ -324,7 +354,9 @@
|
||||
/bundles/org.openhab.binding.sncf/ @clinique
|
||||
/bundles/org.openhab.binding.snmp/ @J-N-K
|
||||
/bundles/org.openhab.binding.solaredge/ @alexf2015
|
||||
/bundles/org.openhab.binding.solarforecast/ @weymann
|
||||
/bundles/org.openhab.binding.solarlog/ @johannrichard
|
||||
/bundles/org.openhab.binding.solarman/ @catalinsanda
|
||||
/bundles/org.openhab.binding.solarmax/ @jamietownsend
|
||||
/bundles/org.openhab.binding.solarwatt/ @sven-carstens
|
||||
/bundles/org.openhab.binding.solax/ @theater
|
||||
@ -339,6 +371,7 @@
|
||||
/bundles/org.openhab.binding.speedtest/ @MikeTheTux
|
||||
/bundles/org.openhab.binding.spotify/ @Hilbrand
|
||||
/bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush
|
||||
/bundles/org.openhab.binding.sunsynk/ @leeC77
|
||||
/bundles/org.openhab.binding.surepetcare/ @renescherer @HerzScheisse
|
||||
/bundles/org.openhab.binding.synopanalyzer/ @clinique
|
||||
/bundles/org.openhab.binding.systeminfo/ @mherwege
|
||||
@ -351,6 +384,8 @@
|
||||
/bundles/org.openhab.binding.teleinfo/ @Nokyyz @olivierkeke
|
||||
/bundles/org.openhab.binding.tellstick/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.tesla/ @kgoderis
|
||||
/bundles/org.openhab.binding.teslapowerwall/ @psmedley
|
||||
/bundles/org.openhab.binding.teslascope/ @psmedley
|
||||
/bundles/org.openhab.binding.tibber/ @kjoglum
|
||||
/bundles/org.openhab.binding.tivo/ @mlobstein
|
||||
/bundles/org.openhab.binding.touchwand/ @roieg
|
||||
@ -373,8 +408,10 @@
|
||||
/bundles/org.openhab.binding.verisure/ @jannegpriv
|
||||
/bundles/org.openhab.binding.vesync/ @dag81
|
||||
/bundles/org.openhab.binding.vigicrues/ @clinique
|
||||
/bundles/org.openhab.binding.visualcrossing/ @magx2
|
||||
/bundles/org.openhab.binding.vitotronic/ @steand
|
||||
/bundles/org.openhab.binding.vizio/ @mlobstein
|
||||
/bundles/org.openhab.binding.volumio/ @miloit
|
||||
/bundles/org.openhab.binding.volvooncall/ @Jamstah
|
||||
/bundles/org.openhab.binding.warmup/ @jamesmelville
|
||||
/bundles/org.openhab.binding.weathercompany/ @mhilbush
|
||||
@ -384,6 +421,7 @@
|
||||
/bundles/org.openhab.binding.wemo/ @hmerk @jlaur
|
||||
/bundles/org.openhab.binding.wifiled/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.windcentrale/ @marcelrv @wborn
|
||||
/bundles/org.openhab.binding.wiz/ @ccutrer @frejos
|
||||
/bundles/org.openhab.binding.wlanthermo/ @CSchlipp
|
||||
/bundles/org.openhab.binding.wled/ @Skinah
|
||||
/bundles/org.openhab.binding.wolfsmartset/ @BoBiene
|
||||
@ -395,7 +433,6 @@
|
||||
/bundles/org.openhab.binding.yamahareceiver/ @zarusz
|
||||
/bundles/org.openhab.binding.yeelight/ @claell
|
||||
/bundles/org.openhab.binding.yioremote/ @miloit
|
||||
/bundles/org.openhab.binding.volumio/ @miloit
|
||||
/bundles/org.openhab.binding.zoneminder/ @mhilbush
|
||||
/bundles/org.openhab.binding.zway/ @pathec
|
||||
/bundles/org.openhab.io.homekit/ @andylintner @ccutrer @yfre
|
||||
@ -411,6 +448,7 @@
|
||||
/bundles/org.openhab.persistence.mapdb/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.persistence.mongodb/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.persistence.rrd4j/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.transform.basicprofiles/ @J-N-K
|
||||
/bundles/org.openhab.transform.bin2json/ @paulianttila
|
||||
/bundles/org.openhab.transform.exec/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.transform.jinja/ @jochen314
|
||||
@ -428,11 +466,13 @@
|
||||
/bundles/org.openhab.voice.marytts/ @kaikreuzer
|
||||
/bundles/org.openhab.voice.mimictts/ @dalgwen
|
||||
/bundles/org.openhab.voice.picotts/ @FlorianSW
|
||||
/bundles/org.openhab.voice.pipertts/ @GiviMAD
|
||||
/bundles/org.openhab.voice.pollytts/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.voice.rustpotterks/ @GiviMAD
|
||||
/bundles/org.openhab.voice.voicerss/ @lolodomo
|
||||
/bundles/org.openhab.voice.voskstt/ @GiviMAD
|
||||
/bundles/org.openhab.voice.watsonstt/ @GiviMAD
|
||||
/bundles/org.openhab.voice.whisperstt/ @GiviMAD
|
||||
/itests/org.openhab.automation.groovyscripting.tests/ @wborn
|
||||
/itests/org.openhab.automation.jsscriptingnashorn.tests/ @wborn
|
||||
/itests/org.openhab.binding.astro.tests/ @gerrieg
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bom</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bom</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.addons.bom.openhab-addons</artifactId>
|
||||
@ -61,6 +61,11 @@
|
||||
<artifactId>org.openhab.binding.ahawastecollection</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.airgradient</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.airq</artifactId>
|
||||
@ -96,6 +101,11 @@
|
||||
<artifactId>org.openhab.binding.amazonechocontrol</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.amberelectric</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.ambientweather</artifactId>
|
||||
@ -126,6 +136,11 @@
|
||||
<artifactId>org.openhab.binding.anthem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.argoclima</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.astro</artifactId>
|
||||
@ -261,6 +276,11 @@
|
||||
<artifactId>org.openhab.binding.bosesoundtouch</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.broadlink</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.broadlinkthermostat</artifactId>
|
||||
@ -376,6 +396,11 @@
|
||||
<artifactId>org.openhab.binding.dmx</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.dolbycp</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.dominoswiss</artifactId>
|
||||
@ -448,7 +473,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.electroluxair</artifactId>
|
||||
<artifactId>org.openhab.binding.electroluxappliance</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -461,6 +486,11 @@
|
||||
<artifactId>org.openhab.binding.elroconnects</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.emotiva</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.energenie</artifactId>
|
||||
@ -486,11 +516,21 @@
|
||||
<artifactId>org.openhab.binding.enphase</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.entsoe</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.enturno</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.ephemeris</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.epsonprojector</artifactId>
|
||||
@ -526,6 +566,11 @@
|
||||
<artifactId>org.openhab.binding.feican</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.fenecon</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.fineoffsetweatherstation</artifactId>
|
||||
@ -536,6 +581,11 @@
|
||||
<artifactId>org.openhab.binding.flicbutton</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.flume</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.fmiweather</artifactId>
|
||||
@ -556,6 +606,11 @@
|
||||
<artifactId>org.openhab.binding.foobot</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.freeathome</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.freebox</artifactId>
|
||||
@ -566,6 +621,16 @@
|
||||
<artifactId>org.openhab.binding.freeboxos</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.freecurrency</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.frenchgovtenergydata</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.fronius</artifactId>
|
||||
@ -626,6 +691,11 @@
|
||||
<artifactId>org.openhab.binding.gree</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.gridbox</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.groheondus</artifactId>
|
||||
@ -636,6 +706,11 @@
|
||||
<artifactId>org.openhab.binding.groupepsa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.growatt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.guntamatic</artifactId>
|
||||
@ -721,6 +796,11 @@
|
||||
<artifactId>org.openhab.binding.hue</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.huesync</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.hydrawise</artifactId>
|
||||
@ -766,6 +846,11 @@
|
||||
<artifactId>org.openhab.binding.intesis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.iotawatt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.ipcamera</artifactId>
|
||||
@ -891,6 +976,11 @@
|
||||
<artifactId>org.openhab.binding.lifx</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.linktap</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.linky</artifactId>
|
||||
@ -986,21 +1076,31 @@
|
||||
<artifactId>org.openhab.binding.mercedesme</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.meteoalerte</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.meteoblue</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.meteofrance</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.meteostick</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.metofficedatahub</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.mffan</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.miele</artifactId>
|
||||
@ -1056,6 +1156,11 @@
|
||||
<artifactId>org.openhab.binding.modbus.helioseasycontrols</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.modbus.kermi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.modbus.sbc</artifactId>
|
||||
@ -1071,6 +1176,11 @@
|
||||
<artifactId>org.openhab.binding.modbus.studer</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.modbus.sungrow</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.modbus.sunspec</artifactId>
|
||||
@ -1131,6 +1241,11 @@
|
||||
<artifactId>org.openhab.binding.mystrom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.myuplink</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.nanoleaf</artifactId>
|
||||
@ -1176,11 +1291,6 @@
|
||||
<artifactId>org.openhab.binding.nibeheatpump</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.nibeuplink</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.nikobus</artifactId>
|
||||
@ -1316,6 +1426,11 @@
|
||||
<artifactId>org.openhab.binding.paradoxalarm</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.pegelonline</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.pentair</artifactId>
|
||||
@ -1326,6 +1441,11 @@
|
||||
<artifactId>org.openhab.binding.phc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.pihole</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.pilight</artifactId>
|
||||
@ -1421,6 +1541,11 @@
|
||||
<artifactId>org.openhab.binding.qolsysiq</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.radiobrowser</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.radiothermostat</artifactId>
|
||||
@ -1486,6 +1611,16 @@
|
||||
<artifactId>org.openhab.binding.sagercaster</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.saicismart</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.salus</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.samsungtv</artifactId>
|
||||
@ -1541,6 +1676,11 @@
|
||||
<artifactId>org.openhab.binding.shelly</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.siemenshvac</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.siemensrds</artifactId>
|
||||
@ -1606,11 +1746,21 @@
|
||||
<artifactId>org.openhab.binding.solaredge</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.solarforecast</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.solarlog</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.solarman</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.solarmax</artifactId>
|
||||
@ -1681,6 +1831,11 @@
|
||||
<artifactId>org.openhab.binding.squeezebox</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.sunsynk</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.surepetcare</artifactId>
|
||||
@ -1741,6 +1896,16 @@
|
||||
<artifactId>org.openhab.binding.tesla</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.teslapowerwall</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.teslascope</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.tibber</artifactId>
|
||||
@ -1851,6 +2016,11 @@
|
||||
<artifactId>org.openhab.binding.vigicrues</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.visualcrossing</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.vitotronic</artifactId>
|
||||
@ -1911,6 +2081,11 @@
|
||||
<artifactId>org.openhab.binding.windcentrale</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.wiz</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.wlanthermo</artifactId>
|
||||
@ -2041,6 +2216,11 @@
|
||||
<artifactId>org.openhab.persistence.rrd4j</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.transform.basicprofiles</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.transform.bin2json</artifactId>
|
||||
@ -2126,6 +2306,11 @@
|
||||
<artifactId>org.openhab.voice.picotts</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.voice.pipertts</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.voice.pollytts</artifactId>
|
||||
@ -2151,6 +2336,11 @@
|
||||
<artifactId>org.openhab.voice.watsonstt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.voice.whisperstt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bom</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bom</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.addons.bom.openhab-core-index</artifactId>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons</groupId>
|
||||
<artifactId>org.openhab.addons.reactor</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>org.openhab.addons.bom</groupId>
|
||||
@ -29,7 +29,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<version>3.1.0</version>
|
||||
<inherited>false</inherited>
|
||||
<executions>
|
||||
<execution>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bom</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bom</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.addons.bom.runtime-index</artifactId>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bom</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bom</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.addons.bom.test-index</artifactId>
|
||||
|
@ -10,7 +10,7 @@ IF %ARGC% NEQ 3 (
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
SET OpenhabVersion="4.2.0-SNAPSHOT"
|
||||
SET OpenhabVersion="4.3.0-SNAPSHOT"
|
||||
|
||||
SET BindingIdInCamelCase=%~1
|
||||
SET BindingIdInLowerCase=%BindingIdInCamelCase%
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[ $# -lt 3 ] && { echo "Usage: $0 <BindingIdInCamelCase> <Author> <GitHub Username>"; exit 1; }
|
||||
|
||||
openHABVersion=4.2.0-SNAPSHOT
|
||||
openHABVersion=4.3.0-SNAPSHOT
|
||||
|
||||
camelcaseId=$1
|
||||
id=`echo $camelcaseId | tr '[:upper:]' '[:lower:]'`
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Groovy Scripting
|
||||
|
||||
This add-on provides support for [Groovy](https://groovy-lang.org/) 4.0.11 that can be used as a scripting language within automation rules and which eliminates the need to manually install Groovy.
|
||||
This add-on provides support for [Groovy](https://groovy-lang.org/) 4.0.23 that can be used as a scripting language within automation rules and which eliminates the need to manually install Groovy.
|
||||
|
||||
## Creating Groovy Scripts
|
||||
|
||||
@ -15,12 +15,19 @@ If you create an empty file called `test.groovy`, you will see a log line with i
|
||||
|
||||
To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to enable debug logging for the automation functionality:
|
||||
|
||||
```text
|
||||
```shell
|
||||
log:set DEBUG org.openhab.core.automation
|
||||
```
|
||||
|
||||
For more information on the available APIs in scripts see the [JSR223 Scripting]({{base}}/configuration/jsr223.html) documentation.
|
||||
|
||||
## Code reuse
|
||||
|
||||
One can place *.groovy files with Groovy classes under `automation/groovy` configuration directory.
|
||||
Those classes can be imported in JSR-223 scripts or the UI rules action with the usual Groovy `import` statement.
|
||||
|
||||
To apply shared code changes, one has to restart the `openHAB Core :: Bundles :: Automation` bundle on the Console or an openHAB instance altogether.
|
||||
|
||||
## Script Examples
|
||||
|
||||
Groovy scripts provide access to almost all the functionality in an openHAB runtime environment.
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.groovyscripting</artifactId>
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
<properties>
|
||||
<bnd.importpackage>com.ibm.icu.*;resolution:=optional,groovy.runtime.metaclass;resolution:=optional,groovyjarjarantlr4.stringtemplate;resolution:=optional,org.abego.treelayout.*;resolution:=optional,org.apache.ivy.*;resolution:=optional,org.fusesource.jansi.*;resolution:=optional,org.stringtemplate.v4.*;resolution:=optional</bnd.importpackage>
|
||||
<groovy.version>4.0.11</groovy.version>
|
||||
<groovy.version>4.0.23</groovy.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.automation.groovyscripting.internal;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.codehaus.groovy.control.CompilerConfiguration;
|
||||
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
|
||||
import org.openhab.core.OpenHAB;
|
||||
|
||||
import groovy.lang.GroovyClassLoader;
|
||||
|
||||
/**
|
||||
* Customizes the {@link GroovyClassLoader} so that {@link CompilationCustomizer}s can be added which allows for
|
||||
* importing additional classes via scopes.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
public class CustomizableGroovyClassLoader extends GroovyClassLoader {
|
||||
|
||||
private static final String FILE_DIRECTORY = "automation" + File.separator + "groovy";
|
||||
|
||||
private CompilerConfiguration config;
|
||||
|
||||
public CustomizableGroovyClassLoader() {
|
||||
this(CustomizableGroovyClassLoader.class.getClassLoader(), new CompilerConfiguration(), true);
|
||||
}
|
||||
|
||||
public CustomizableGroovyClassLoader(ClassLoader parent, CompilerConfiguration config,
|
||||
boolean useConfigurationClasspath) {
|
||||
super(parent, config, useConfigurationClasspath);
|
||||
this.config = config;
|
||||
addClasspath(OpenHAB.getConfigFolder() + File.separator + FILE_DIRECTORY);
|
||||
}
|
||||
|
||||
public void addCompilationCustomizers(CompilationCustomizer... customizers) {
|
||||
config.addCompilationCustomizers(customizers);
|
||||
}
|
||||
}
|
@ -12,22 +12,20 @@
|
||||
*/
|
||||
package org.openhab.automation.groovyscripting.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
|
||||
import org.codehaus.groovy.control.customizers.ImportCustomizer;
|
||||
import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.automation.module.script.AbstractScriptEngineFactory;
|
||||
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
import groovy.lang.GroovyClassLoader;
|
||||
|
||||
/**
|
||||
* This is an implementation of a {@link ScriptEngineFactory} for Groovy.
|
||||
*
|
||||
@ -37,20 +35,11 @@ import groovy.lang.GroovyClassLoader;
|
||||
@NonNullByDefault
|
||||
public class GroovyScriptEngineFactory extends AbstractScriptEngineFactory {
|
||||
|
||||
private static final String FILE_DIRECTORY = "automation" + File.separator + "groovy";
|
||||
private final org.codehaus.groovy.jsr223.GroovyScriptEngineFactory factory = new org.codehaus.groovy.jsr223.GroovyScriptEngineFactory();
|
||||
|
||||
private final List<String> scriptTypes = (List<String>) Stream.of(factory.getExtensions(), factory.getMimeTypes())
|
||||
private final List<String> scriptTypes = Stream.of(factory.getExtensions(), factory.getMimeTypes())
|
||||
.flatMap(List::stream) //
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
|
||||
private final GroovyClassLoader gcl = new GroovyClassLoader(GroovyScriptEngineFactory.class.getClassLoader());
|
||||
|
||||
public GroovyScriptEngineFactory() {
|
||||
String scriptDir = OpenHAB.getConfigFolder() + File.separator + FILE_DIRECTORY;
|
||||
logger.debug("Adding script directory {} to the GroovyScriptEngine class path.", scriptDir);
|
||||
gcl.addClasspath(scriptDir);
|
||||
}
|
||||
.toList();
|
||||
|
||||
@Override
|
||||
public List<String> getScriptTypes() {
|
||||
@ -58,10 +47,32 @@ public class GroovyScriptEngineFactory extends AbstractScriptEngineFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
|
||||
if (scriptTypes.contains(scriptType)) {
|
||||
return new org.codehaus.groovy.jsr223.GroovyScriptEngineImpl(gcl);
|
||||
public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValues) {
|
||||
ImportCustomizer importCustomizer = new ImportCustomizer();
|
||||
for (Map.Entry<String, Object> entry : scopeValues.entrySet()) {
|
||||
if (entry.getValue() instanceof Class<?> clazz) {
|
||||
String canonicalName = clazz.getCanonicalName();
|
||||
try {
|
||||
// Only add imports for classes that are available to the classloader
|
||||
getClass().getClassLoader().loadClass(canonicalName);
|
||||
importCustomizer.addImport(entry.getKey(), canonicalName);
|
||||
logger.debug("Added import for {} as {}", entry.getKey(), canonicalName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
logger.debug("Unable to add import for {} as {}", entry.getKey(), canonicalName, e);
|
||||
}
|
||||
} else {
|
||||
scriptEngine.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
GroovyScriptEngineImpl gse = (GroovyScriptEngineImpl) scriptEngine;
|
||||
CustomizableGroovyClassLoader cl = (CustomizableGroovyClassLoader) gse.getClassLoader();
|
||||
cl.addCompilationCustomizers(importCustomizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
|
||||
return scriptTypes.contains(scriptType) ? new GroovyScriptEngineImpl(new CustomizableGroovyClassLoader())
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ If you're new to Ruby, you may want to check out [Ruby Basics](https://openhab.g
|
||||
- [Script is Loaded](#script-is-loaded)
|
||||
- [openHAB System Started](#openhab-system-started)
|
||||
- [Cron Trigger](#cron-trigger)
|
||||
- [DateTimeItem Trigger](#datetimeitem-trigger)
|
||||
- [Other Triggers](#other-triggers)
|
||||
- [Combining Multiple Triggers](#combining-multiple-triggers)
|
||||
- [Combining Multiple Conditions](#combining-multiple-conditions)
|
||||
@ -60,6 +61,7 @@ If you're new to Ruby, you may want to check out [Ruby Basics](https://openhab.g
|
||||
- [Terse Rules](#terse-rules)
|
||||
- [Early Exit From a Rule](#early-exit-from-a-rule)
|
||||
- [Dynamic Generation of Rules](#dynamic-generation-of-rules)
|
||||
- [Scenes and Scripts](#scenes-and-scripts)
|
||||
- [Hooks](#hooks)
|
||||
- [Calling Java From JRuby](#calling-java-from-jruby)
|
||||
- [Full Documentation](#full-documentation)
|
||||
@ -99,7 +101,7 @@ Additional [example rules are available](https://openhab.github.io/openhab-jruby
|
||||
|
||||
1. Go to `Settings -> Add-ons -> Automation` and install the jrubyscripting automation addon following the [openHAB instructions](https://www.openhab.org/docs/configuration/addons.html).
|
||||
In openHAB 4.0+ the defaults are set so the next step can be skipped.
|
||||
1. Go to `Settings -> Other Services -> JRuby Scripting`:
|
||||
1. Go to `Settings -> Add-on Settings -> JRuby Scripting`:
|
||||
- **Ruby Gems**: `openhab-scripting=~>5.0`
|
||||
- **Require Scripts**: `openhab/dsl` (not required, but recommended)
|
||||
|
||||
@ -118,7 +120,7 @@ Additional [example rules are available](https://openhab.github.io/openhab-jruby
|
||||
|
||||
## Configuration
|
||||
|
||||
After installing this add-on, you will find configuration options in the openHAB portal under _Settings -> Other Services -> JRuby Scripting_.
|
||||
After installing this add-on, you will find configuration options in the openHAB portal under _Settings -> Add-on Settings -> JRuby Scripting_.
|
||||
Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/`.
|
||||
|
||||
By default this add-on includes the [openhab-scripting](https://github.com/openhab/openhab-jruby) Ruby gem and automatically `require`s it.
|
||||
@ -237,7 +239,7 @@ logger.info("Kitchen Light State: #{KitchenLight.state}")
|
||||
Sending a notification:
|
||||
|
||||
```ruby
|
||||
notify("romeo@montague.org", "Balcony door is open")
|
||||
Notification.send("romeo@montague.org", "Balcony door is open")
|
||||
```
|
||||
|
||||
Querying the status of a thing:
|
||||
@ -262,12 +264,13 @@ When you use "Item event" as trigger (i.e. "[item] received a command", "[item]
|
||||
This tables gives an overview of the `event` object for most common trigger types.
|
||||
For full details, explore [OpenHAB::Core::Events](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Events.html).
|
||||
|
||||
| Property Name | Type | Trigger Types | Description | Rules DSL Equivalent |
|
||||
| ------------- | -------------------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------- | ---------------------- |
|
||||
| `state` | [State](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/State.html) or `nil` | `[item] changed`, `[item] was updated` | State that triggered event | `triggeringItem.state` |
|
||||
| `was` | [State](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/State.html) or `nil` | `[item] changed` | Previous state of Item or Group that triggered event | `previousState` |
|
||||
| `command` | [Command](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/Command.html) | `[item] received a command` | Command that triggered event | `receivedCommand` |
|
||||
| `item` | [Item](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Items/Item.html) | all | Item that triggered event | `triggeringItem` |
|
||||
| Property Name | Type | Trigger Types | Description | Rules DSL Equivalent |
|
||||
| ------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------- |
|
||||
| `state` | [State](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/State.html) or `nil` | `[item] changed`, `[item] was updated` | State that triggered event | `triggeringItem.state` |
|
||||
| `was` | [State](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/State.html) or `nil` | `[item] changed` | Previous state of Item or Group that triggered event | `previousState` |
|
||||
| `command` | [Command](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/Command.html) | `[item] received a command` | Command that triggered event | `receivedCommand` |
|
||||
| `item` | [Item](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Items/Item.html) | All item related triggers | Item that triggered event | `triggeringItem` |
|
||||
| `group` | [GroupItem](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Items/Item.html) | `Member of [group] changed`, `Member of [group] was updated`, `Member of [group] received a command` | Group whose member triggered the event | `triggeringGroup` |
|
||||
|
||||
```ruby
|
||||
logger.info(event.state == ON)
|
||||
@ -473,6 +476,9 @@ My_Item.ensure << ON
|
||||
logger.info("Turning off the light") if My_Item.ensure.off
|
||||
```
|
||||
|
||||
See [ensure_states](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#ensure_states-class_method), [ensure_states!](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#ensure_states!-class_method),
|
||||
[ensure](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Items/Ensure/Ensurable.html#ensure-instance_method).
|
||||
|
||||
##### Timed Commands
|
||||
|
||||
A [Timed Command](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Items/TimedCommand.html) is similar to the openHAB Item's [expire parameter](https://www.openhab.org/docs/configuration/items.html#parameter-expire) but it offers more flexibility.
|
||||
@ -664,7 +670,7 @@ items.build do
|
||||
|
||||
# dimension Temperature inferred
|
||||
number_item OutdoorTemp, format: "%.1f %unit%", unit: "°F"
|
||||
|
||||
|
||||
# unit lx, dimension Illuminance, format "%s %unit%" inferred
|
||||
number_item OutdoorBrightness, state: 10_000 | "lx"
|
||||
end
|
||||
@ -911,7 +917,7 @@ Furthermore, you can manipulate the managed timers using the built-in [timers](h
|
||||
```ruby
|
||||
# timers is a special object to access the timers created with an id
|
||||
rule "cancel all timers" do
|
||||
received_command Cancel_All_Timers, to: ON # Send a command to this item to cancel all timers
|
||||
received_command Cancel_All_Timers, command: ON # Send a command to this item to cancel all timers
|
||||
run do
|
||||
gOutdoorLights.members.each do |item_as_timer_id|
|
||||
timers.cancel(item_as_timer_id)
|
||||
@ -920,7 +926,7 @@ rule "cancel all timers" do
|
||||
end
|
||||
|
||||
rule "reschedule all timers" do
|
||||
received_command Reschedule_All_Timers, to: ON # Send a command to this item to restart all timers
|
||||
received_command Reschedule_All_Timers, command: ON # Send a command to this item to restart all timers
|
||||
run do
|
||||
gOutdoorLights.members.each do |item_as_timer_id|
|
||||
timers.reschedule(item_as_timer_id)
|
||||
@ -1148,7 +1154,7 @@ Time.now.between?("5am".."11pm")
|
||||
Time.now.holiday? # => false
|
||||
MonthDay.parse("12-25").holiday # => :christmas
|
||||
1.day.from_now.next_holiday # => :thanksgiving
|
||||
notify("It's #{Ephemeris.holiday_name(Date.today)}!") if Date.today.holiday?
|
||||
Notification.send("It's #{Ephemeris.holiday_name(Date.today)}!") if Date.today.holiday?
|
||||
|
||||
Date.today.weekend? # => true
|
||||
Date.today.in_dayset?(:school) # => false
|
||||
@ -1462,9 +1468,9 @@ See [#updated](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/Bu
|
||||
|
||||
```ruby
|
||||
rule "Received a command" do
|
||||
received_command DoorBell, to: ON
|
||||
received_command DoorBell, command: ON
|
||||
run do |event|
|
||||
notify "Someone pressed the door bell"
|
||||
Notification.send "Someone pressed the door bell"
|
||||
play_sound "doorbell.mp3"
|
||||
end
|
||||
end
|
||||
@ -1556,6 +1562,30 @@ end
|
||||
|
||||
See [#every](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/BuilderDSL.html#every-instance_method)
|
||||
|
||||
#### DateTimeItem Trigger
|
||||
|
||||
To trigger based on the date and time stored in a DateTime item, use [at ItemName](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/BuilderDSL.html#at-instance_method):
|
||||
|
||||
```ruby
|
||||
rule "DateTime Trigger" do
|
||||
at My_DateTimeItem
|
||||
run do |event|
|
||||
logger.info "Triggered by #{event.item} at #{event.item.state}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
To trigger based on only the _time_ part of a DateTime item, use [every :day, at: ItemName](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/BuilderDSL.html#every-instance_method):
|
||||
|
||||
```ruby
|
||||
rule "TimeOnly Trigger" do
|
||||
every :day, at: My_DateTimeItem
|
||||
run do |event|
|
||||
logger.info "Triggered by #{event.item} at #{event.item.state}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Other Triggers
|
||||
|
||||
There are more triggers supported by this library.
|
||||
@ -1656,7 +1686,7 @@ rule "Check for offline things 15 minutes after openHAB had started" do
|
||||
delay 15.minutes
|
||||
run do
|
||||
offline_things = things.select(&:offline?).map(&:uid).join(", ")
|
||||
notify("Things that are still offline: #{offline_things}")
|
||||
Notification.send("Things that are still offline: #{offline_things}")
|
||||
end
|
||||
end
|
||||
```
|
||||
@ -1668,7 +1698,7 @@ See [Execution Blocks](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/
|
||||
A rule with a trigger and an execution block can be created with just one line.
|
||||
|
||||
```ruby
|
||||
received_command(My_Switch, to: ON) { My_Light.on }
|
||||
received_command(My_Switch, command: ON) { My_Light.on }
|
||||
```
|
||||
|
||||
See [Terse Rules](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/Terse.html) for full details.
|
||||
@ -1740,6 +1770,24 @@ virtual_switches.each do |switch|
|
||||
end
|
||||
```
|
||||
|
||||
### Scenes and Scripts
|
||||
|
||||
A `scene` can be created using the [.scene](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#scene-class_method) method.
|
||||
|
||||
```ruby
|
||||
scene "Movie", id: "movie", description: "Set up the theatre for movie watching" do
|
||||
Theatre_Window_Blinds.down
|
||||
Theatre_Screen_Curtain.up
|
||||
Theatre_Mood_Light.on
|
||||
Theatre_Light.off
|
||||
Theatre_Projector.on
|
||||
Theatre_Receiver.on
|
||||
end
|
||||
```
|
||||
|
||||
To create a `script`, use the [.script](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#script-class_method) method.
|
||||
Note that scripts can be executed with additional contexts.
|
||||
|
||||
### Hooks
|
||||
|
||||
File based scripts can also register [hooks](https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/ScriptHandling.html) that will be called when the script has completed loading (`script_loaded`) and when it gets unloaded (`script_unloaded`).
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.jrubyscripting</artifactId>
|
||||
@ -15,8 +15,8 @@
|
||||
<name>openHAB Add-ons :: Bundles :: Automation :: JRuby Scripting</name>
|
||||
|
||||
<properties>
|
||||
<bnd.importpackage>com.sun.nio.*;resolution:=optional,com.sun.security.*;resolution:=optional,org.apache.tools.ant.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.joda.*;resolution:=optional,sun.management.*;resolution:=optional,sun.nio.*;resolution:=optional,jakarta.annotation;resolution:=optional</bnd.importpackage>
|
||||
<jruby.version>9.4.5.0</jruby.version>
|
||||
<bnd.importpackage>com.sun.nio.*;resolution:=optional,com.sun.security.*;resolution:=optional,org.apache.tools.ant.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.joda.*;resolution:=optional,sun.management.*;resolution:=optional,sun.nio.*;resolution:=optional,jakarta.annotation;resolution:=optional,jdk.crac.management;resolution:=optional</bnd.importpackage>
|
||||
<jruby.version>9.4.9.0</jruby.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.automation.jrubyscripting.internal;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This is a wrapper for {@link CompiledScript}.
|
||||
*
|
||||
* The purpose of this class is to intercept the call to eval and save the context into
|
||||
* a global variable for use in the helper library.
|
||||
*
|
||||
* @author Jimmy Tanagra - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JRubyCompiledScriptWrapper extends CompiledScript {
|
||||
|
||||
private final CompiledScript compiledScript;
|
||||
|
||||
private static final String CONTEXT_VAR_NAME = "ctx";
|
||||
private static final String GLOBAL_VAR_NAME = "$" + CONTEXT_VAR_NAME;
|
||||
|
||||
JRubyCompiledScriptWrapper(CompiledScript compiledScript) {
|
||||
this.compiledScript = Objects.requireNonNull(compiledScript);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object eval(@Nullable ScriptContext context) throws ScriptException {
|
||||
Object ctx = Objects.requireNonNull(context).getBindings(ScriptContext.ENGINE_SCOPE).get(CONTEXT_VAR_NAME);
|
||||
if (ctx == null) {
|
||||
return compiledScript.eval(context);
|
||||
}
|
||||
|
||||
context.setAttribute(GLOBAL_VAR_NAME, ctx, ScriptContext.ENGINE_SCOPE);
|
||||
try {
|
||||
return compiledScript.eval(context);
|
||||
} finally {
|
||||
context.removeAttribute(GLOBAL_VAR_NAME, ScriptContext.ENGINE_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEngine getEngine() {
|
||||
return compiledScript.getEngine();
|
||||
}
|
||||
}
|
@ -50,12 +50,12 @@ public class JRubyEngineWrapper implements Compilable, Invocable, ScriptEngine {
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(@Nullable String script) throws ScriptException {
|
||||
return engine.compile(script);
|
||||
return new JRubyCompiledScriptWrapper(engine.compile(script));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(@Nullable Reader reader) throws ScriptException {
|
||||
return engine.compile(reader);
|
||||
return new JRubyCompiledScriptWrapper(engine.compile(reader));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -379,7 +379,7 @@ public class JRubyScriptEngineConfiguration {
|
||||
private final String defaultValue;
|
||||
private final Optional<String> mappedTo;
|
||||
private final Type type;
|
||||
private Optional<String> value;
|
||||
private @Nullable String value;
|
||||
|
||||
private OptionalConfigurationElement(String defaultValue) {
|
||||
this(Type.OTHER, defaultValue, null);
|
||||
@ -389,19 +389,19 @@ public class JRubyScriptEngineConfiguration {
|
||||
this.type = type;
|
||||
this.defaultValue = defaultValue;
|
||||
this.mappedTo = Optional.ofNullable(mappedTo);
|
||||
value = Optional.empty();
|
||||
}
|
||||
|
||||
private String getValue() {
|
||||
return value.orElse(defaultValue);
|
||||
String value = this.value;
|
||||
return value != null ? value : this.defaultValue;
|
||||
}
|
||||
|
||||
private void setValue(String value) {
|
||||
this.value = Optional.of(value);
|
||||
private void setValue(@Nullable String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private void clearValue() {
|
||||
this.value = Optional.empty();
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
private Optional<String> mappedTo() {
|
||||
|
@ -26,7 +26,7 @@
|
||||
<parameter name="gems" type="text" required="false" groupName="gems">
|
||||
<label>Ruby Gems</label>
|
||||
<description><![CDATA[A comma separated list of Ruby Gems to install. Versions may be constrained by separating with an
|
||||
<tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>5.0</tt>".
|
||||
<tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>5.0</tt>".
|
||||
]]></description>
|
||||
<default>openhab-scripting=~>5.0.0</default>
|
||||
</parameter>
|
||||
|
@ -1,33 +0,0 @@
|
||||
automation.config.jruby.check_update.label = Check for Gem Updates
|
||||
automation.config.jruby.check_update.description = Check RubyGems for updates to the above gems when OpenHAB starts or JRuby settings are changed. Otherwise it will try to fulfill the requirements with locally installed gems, and you can manage them yourself with an external Ruby by setting the same GEM_HOME.
|
||||
automation.config.jruby.dependency_tracking.label = Enable Dependency Tracking
|
||||
automation.config.jruby.dependency_tracking.description = Dependency tracking allows your scripts to automatically reload when one of its dependencies is updated. You may want to disable dependency tracking if you plan on editing or updating a shared library, but don't want all your scripts to reload until you can test it.
|
||||
automation.config.jruby.gem_home.label = GEM_HOME
|
||||
automation.config.jruby.gem_home.description = Location Ruby Gems will be installed to and loaded from. Directory will be created if necessary. You can use <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> and/or <tt>{RUBY_VERSION}</tt> replacements in this value to automatically point to a new directory when the addon is updated with a new version of JRuby. Defaults to "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" when not specified.
|
||||
automation.config.jruby.gems.label = Ruby Gems
|
||||
automation.config.jruby.gems.description = A comma separated list of Ruby Gems to install. Versions may be constrained by separating with an <tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>5.0</tt>".
|
||||
automation.config.jruby.group.environment.label = Ruby Environment
|
||||
automation.config.jruby.group.environment.description = This group defines Ruby's environment.
|
||||
automation.config.jruby.group.gems.label = Ruby Gems
|
||||
automation.config.jruby.group.gems.description = This group defines the list of Ruby Gems to install.
|
||||
automation.config.jruby.group.system.label = System Properties
|
||||
automation.config.jruby.group.system.description = This group defines JRuby system properties.
|
||||
automation.config.jruby.local_context.label = Context Instance Type
|
||||
automation.config.jruby.local_context.description = The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See <a href="https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type">the documentation</a> for options and details.
|
||||
automation.config.jruby.local_context.option.singleton = Singleton
|
||||
automation.config.jruby.local_context.option.threadsafe = ThreadSafe
|
||||
automation.config.jruby.local_context.option.singlethread = SingleThread
|
||||
automation.config.jruby.local_context.option.concurrent = Concurrent
|
||||
automation.config.jruby.local_variable.label = Local Variable Behavior
|
||||
automation.config.jruby.local_variable.description = Defines how variables are shared between Ruby and Java. See <a href="https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options">the documentation</a> for options and details.
|
||||
automation.config.jruby.local_variable.option.transient = Transient
|
||||
automation.config.jruby.local_variable.option.persistent = Persistent
|
||||
automation.config.jruby.local_variable.option.global = Global
|
||||
automation.config.jruby.require.label = Require Scripts
|
||||
automation.config.jruby.require.description = A comma separated list of script names to be required by the JRuby Scripting Engine before running user scripts.
|
||||
automation.config.jruby.rubylib.label = RUBYLIB
|
||||
automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" when not specified.
|
||||
|
||||
# service
|
||||
|
||||
service.automation.jrubyscripting.label = JRuby Scripting
|
@ -1,33 +0,0 @@
|
||||
automation.config.jruby.check_update.label = Nach Gem Updates suchen
|
||||
automation.config.jruby.check_update.description = Prüfen Sie RubyGems auf Aktualisierungen der oben genannten Gems, wenn OpenHAB startet oder JRuby-Einstellungen geändert werden. Andernfalls wird versucht, die Anforderungen mit lokal installierten Gems zu erfüllen. Sie können auch ein eigenes, externes Ruby einsetzen, indem Sie dasselbe GEM_HOME definieren.
|
||||
automation.config.jruby.dependency_tracking.label = Abhängigkeitsverfolgung aktivieren
|
||||
automation.config.jruby.dependency_tracking.description = Die Abhängigkeitsverfolgung ermöglicht es Ihren Skripten, automatisch neu zu laden, wenn eine ihrer Abhängigkeiten aktualisiert wird. Sie können die Abhängigkeitsverfolgung deaktivieren, wenn Sie eine gemeinsam genutzte Bibliothek bearbeiten oder aktualisieren möchten, aber nicht wollen, dass alle Skripte neu geladen werden, bis Sie sie testen können.
|
||||
automation.config.jruby.gem_home.label = GEM_HOME
|
||||
automation.config.jruby.gem_home.description = Speicherort Ruby Gems wird installiert und von dort geladen. Das Verzeichnis wird bei Bedarf erstellt. Sie können die Ersetzungen <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> und/oder <tt>{RUBY_VERSION}</tt> in diesem Wert verwenden, um automatisch auf ein neues Verzeichnis zu verweisen, wenn das Addon wird mit einer neuen Version von JRuby aktualisiert. Standardmäßig „<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>“, wenn nicht angegeben.
|
||||
automation.config.jruby.gems.label = Ruby Gems
|
||||
automation.config.jruby.gems.description = Eine kommagetrennte Liste von zu installierenden Ruby Gems. Versionen können eingeschränkt werden, indem sie von <tt>\=</tt> getrennt werden, und dann die Standard RubyGems Versionsbeschränkung, wie "<tt>openhab-scripting\=~>5.0.0</tt>".
|
||||
automation.config.jruby.group.environment.label = Ruby-Umgebung
|
||||
automation.config.jruby.group.environment.description = Diese Gruppe definiert die Ruby Umgebung.
|
||||
automation.config.jruby.group.gems.label = Ruby Gems
|
||||
automation.config.jruby.group.gems.description = Diese Gruppe definiert die Liste der zu installierenden Ruby Gems.
|
||||
automation.config.jruby.group.system.label = Systemeigenschaften
|
||||
automation.config.jruby.group.system.description = Diese Gruppe definiert JRuby Systemeigenschaften.
|
||||
automation.config.jruby.local_context.label = Context Instance Type
|
||||
automation.config.jruby.local_context.description = Der lokale Kontext enthält Ruby Laufzeitumgebung, Namens-Wert-Paare für das Teilen von Variablen zwischen Java und Ruby. Siehe <a href\="https\://github.com/jruby/jruby/wiki/RedBridge\#Context_Instance_Type">die Dokumentation</a> für Optionen und Details.
|
||||
automation.config.jruby.local_context.option.singleton = Singleton
|
||||
automation.config.jruby.local_context.option.threadsafe = ThreadSafe
|
||||
automation.config.jruby.local_context.option.singlethread = SingleThread
|
||||
automation.config.jruby.local_context.option.concurrent = Concurrent
|
||||
automation.config.jruby.local_variable.label = Lokales Variablenverhalten
|
||||
automation.config.jruby.local_variable.description = Legt fest, wie Variablen zwischen Ruby und Java geteilt werden. Siehe <a href\="https\://github.com/jruby/jruby/wiki/RedBridge\#local-variable-behavior-options">die Dokumentation</a> für Optionen und Details.
|
||||
automation.config.jruby.local_variable.option.transient = Transient
|
||||
automation.config.jruby.local_variable.option.persistent = Persistent
|
||||
automation.config.jruby.local_variable.option.global = Global
|
||||
automation.config.jruby.require.label = Skripte anfordern
|
||||
automation.config.jruby.require.description = Eine durch Kommata getrennte Liste von Skriptnamen, die von der JRuby Scripting Engine verlangt werden, bevor Benutzerskripte ausgeführt werden.
|
||||
automation.config.jruby.rubylib.label = RUBYLIB
|
||||
automation.config.jruby.rubylib.description = Suchpfad für Benutzerbibliotheken. Trennen Sie jeden Pfad mit einem Doppelpunkt (Semikolon unter Windows). Standardmäßig "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" wenn nicht angegeben.
|
||||
|
||||
# service
|
||||
|
||||
service.automation.jrubyscripting.label = JRuby Scripting
|
@ -1,30 +0,0 @@
|
||||
|
||||
# service
|
||||
|
||||
service.automation.jrubyscripting.label = JRuby szkriptek
|
||||
|
||||
# bundle config
|
||||
|
||||
automation.config.jruby.gem_home.label = GEM_HOME
|
||||
automation.config.jruby.gem_home.description = A Ruby Gems (gyémántok) telepítési és betöltési helye, ha a gyémántok telepítése engedélyezve van. A mappa automatikusan létrehozásra kerül
|
||||
automation.config.jruby.gems.label = Ruby gyémántok
|
||||
automation.config.jruby.gems.description = Vesszővel elválasztott lista a telepítendő gyémántokról.
|
||||
automation.config.jruby.group.environment.label = Ruby környezet
|
||||
automation.config.jruby.group.environment.description = Ez a csoport adja meg a Ruby környezetet.
|
||||
automation.config.jruby.group.gems.label = Ruby gyémántok
|
||||
automation.config.jruby.group.gems.description = Ez a csoport adja meg a Ruby gyémántok telepítendő listáját.
|
||||
automation.config.jruby.group.system.label = Rendszer tulajdonságok
|
||||
automation.config.jruby.group.system.description = Ez a csoport adja meg a JRuby rendszer tulajdonságokat.
|
||||
automation.config.jruby.local_context.label = A kontextus példány típusa
|
||||
automation.config.jruby.local_context.description = A helyi kontextus tartalmazza a Ruby futtatási környezetét, név-érték párjait a Java és Ruby közti változók átadásához. A lehetőségek és részletek megtekintéséhez nyissa meg a következő linket\: https\://github.com/jruby/jruby/wiki/RedBridge\#Context_Instance_Type.
|
||||
automation.config.jruby.local_context.option.singleton = Egyke (singleton)
|
||||
automation.config.jruby.local_context.option.threadsafe = Szálbiztos
|
||||
automation.config.jruby.local_context.option.singlethread = Egyszálú
|
||||
automation.config.jruby.local_context.option.concurrent = Párhuzamos
|
||||
automation.config.jruby.local_variable.label = Helyi változó viselkedés
|
||||
automation.config.jruby.local_variable.description = Megadja, hogy a változók átadása a Ruby és a Java környezet között miként történjen. Részletek\: https\://github.com/jruby/jruby/wiki/RedBridge\#local-variable-behavior-options.
|
||||
automation.config.jruby.local_variable.option.transient = Átmeneti
|
||||
automation.config.jruby.local_variable.option.persistent = Tartós
|
||||
automation.config.jruby.local_variable.option.global = Teljes körű
|
||||
automation.config.jruby.rubylib.label = RUBYLIB
|
||||
automation.config.jruby.rubylib.description = A felhasználói könyvtárak keresési útvonala. Elválasztó a kettőspont (Pontosvessző Windows alatt).
|
@ -1,33 +0,0 @@
|
||||
automation.config.jruby.check_update.label = Controlla per Aggiornamenti di Gem
|
||||
automation.config.jruby.check_update.description = Controllare RubyGems per gli aggiornamenti alle gem di cui sopra quando OpenHAB inizia o le impostazioni JRuby vengono modificate. Altrimenti proverà a soddisfare i requisiti con gem installate localmente, e puoi gestirli da soli con un Ruby esterno impostando lo stesso GEM_HOME.
|
||||
automation.config.jruby.dependency_tracking.label = Abilita Monitoraggio Delle Dipendenze
|
||||
automation.config.jruby.dependency_tracking.description = Il monitoraggio delle dipendenze consente agli script di ricaricare automaticamente quando una delle sue dipendenze viene aggiornata. Si consiglia di disabilitare il tracciamento delle dipendenze se si prevede di modificare o aggiornare una libreria condivisa, ma non vuoi che tutti gli script siano ricaricati fino a quando non puoi provarlo.
|
||||
automation.config.jruby.gem_home.label = GEM_HOME
|
||||
automation.config.jruby.gem_home.description = Il percorso da dove le Ruby Gems saranno installate e caricate. La directory verrà creata se necessario. È possibile utilizare i valori <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> e/o <tt>{RUBY_VERSION}</tt> come sostituzioni per puntare automaticamente a una nuova directory quando l''addon viene aggiornato con una nuova versione di JRuby. Il valore predefinito è "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" se non specificato.
|
||||
automation.config.jruby.gems.label = Ruby Gems
|
||||
automation.config.jruby.gems.description = Un elenco separato da virgola di Ruby Gems da installare. Le versioni possono essere vincolate separando con un <tt>\=</tt> e quindi il vincolo di versione standard di RubyGems, come "<tt>openhab-scripting\=~>5.</tt>".
|
||||
automation.config.jruby.group.environment.label = Ambiente Di Ruby
|
||||
automation.config.jruby.group.environment.description = Questo gruppo definisce l'ambiente di Ruby.
|
||||
automation.config.jruby.group.gems.label = Ruby Gems
|
||||
automation.config.jruby.group.gems.description = Questo gruppo definisce la lista di Ruby Gems da installare.
|
||||
automation.config.jruby.group.system.label = Proprietà di sistema
|
||||
automation.config.jruby.group.system.description = Questo gruppo definisce le proprietà del sistema JRuby.
|
||||
automation.config.jruby.local_context.label = Tipo Context Instance
|
||||
automation.config.jruby.local_context.description = Il contesto locale contiene le coppie di runtime Ruby, none-valore per la condivisione delle variabili tra Java e Ruby. Vedere <a href\="https\://github.com/jruby/jruby/wiki/RedBridge\#Context_Instance_Type">la documentazione</a> per le opzioni e i dettagli.
|
||||
automation.config.jruby.local_context.option.singleton = Singleton
|
||||
automation.config.jruby.local_context.option.threadsafe = Thread Sicura
|
||||
automation.config.jruby.local_context.option.singlethread = Thread Singola
|
||||
automation.config.jruby.local_context.option.concurrent = Concorrente
|
||||
automation.config.jruby.local_variable.label = Comportamento Variabile Locale
|
||||
automation.config.jruby.local_variable.description = Definisce come le variabili sono condivise tra Ruby e Java. Vedi <a href\="https\://github.com/jruby/jruby/wiki/RedBridge\#local-variable-behavior-options">la documentazione</a> per le opzioni e i dettagli.
|
||||
automation.config.jruby.local_variable.option.transient = Transitorio
|
||||
automation.config.jruby.local_variable.option.persistent = Persistente
|
||||
automation.config.jruby.local_variable.option.global = Globale
|
||||
automation.config.jruby.require.label = Richiede Scripts
|
||||
automation.config.jruby.require.description = Un elenco separato da virgola di nomi di script che devono essere richiesti dal motore di scripting JRuby prima di eseguire gli script utente.
|
||||
automation.config.jruby.rubylib.label = RUBYLIB
|
||||
automation.config.jruby.rubylib.description = Percorso di ricerca per le librerie utente. Separare ogni percorso con due punti (con un punto e virgola in Windows). Predefinito "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" se non specificato.
|
||||
|
||||
# service
|
||||
|
||||
service.automation.jrubyscripting.label = JRuby Scripting
|
@ -0,0 +1,36 @@
|
||||
# add-on
|
||||
|
||||
addon.jrubyscripting.name = JRuby Scripting
|
||||
addon.jrubyscripting.description = This adds a JRuby script engine.
|
||||
|
||||
# add-on
|
||||
|
||||
automation.config.jrubyscripting.check_update.label = Check for Gem Updates
|
||||
automation.config.jrubyscripting.check_update.description = Check RubyGems for updates to the above gems when OpenHAB starts or JRuby settings are changed. Otherwise it will try to fulfill the requirements with locally installed gems, and you can manage them yourself with an external Ruby by setting the same GEM_HOME.
|
||||
automation.config.jrubyscripting.dependency_tracking.label = Enable Dependency Tracking
|
||||
automation.config.jrubyscripting.dependency_tracking.description = Dependency tracking allows your scripts to automatically reload when one of its dependencies is updated. You may want to disable dependency tracking if you plan on editing or updating a shared library, but don't want all your scripts to reload until you can test it.
|
||||
automation.config.jrubyscripting.gem_home.label = GEM_HOME
|
||||
automation.config.jrubyscripting.gem_home.description = Location Ruby Gems will be installed to and loaded from. Directory will be created if necessary. You can use <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> and/or <tt>{RUBY_VERSION}</tt> replacements in this value to automatically point to a new directory when the addon is updated with a new version of JRuby. Defaults to "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" when not specified.
|
||||
automation.config.jrubyscripting.gems.label = Ruby Gems
|
||||
automation.config.jrubyscripting.gems.description = A comma separated list of Ruby Gems to install. Versions may be constrained by separating with an <tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>5.0</tt>".
|
||||
automation.config.jrubyscripting.group.environment.label = Ruby Environment
|
||||
automation.config.jrubyscripting.group.environment.description = This group defines Ruby's environment.
|
||||
automation.config.jrubyscripting.group.gems.label = Ruby Gems
|
||||
automation.config.jrubyscripting.group.gems.description = This group defines the list of Ruby Gems to install.
|
||||
automation.config.jrubyscripting.group.system.label = System Properties
|
||||
automation.config.jrubyscripting.group.system.description = This group defines JRuby system properties.
|
||||
automation.config.jrubyscripting.local_context.label = Context Instance Type
|
||||
automation.config.jrubyscripting.local_context.description = The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See <a href="https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type">the documentation</a> for options and details.
|
||||
automation.config.jrubyscripting.local_context.option.singleton = Singleton
|
||||
automation.config.jrubyscripting.local_context.option.threadsafe = ThreadSafe
|
||||
automation.config.jrubyscripting.local_context.option.singlethread = SingleThread
|
||||
automation.config.jrubyscripting.local_context.option.concurrent = Concurrent
|
||||
automation.config.jrubyscripting.local_variable.label = Local Variable Behavior
|
||||
automation.config.jrubyscripting.local_variable.description = Defines how variables are shared between Ruby and Java. See <a href="https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options">the documentation</a> for options and details.
|
||||
automation.config.jrubyscripting.local_variable.option.transient = Transient
|
||||
automation.config.jrubyscripting.local_variable.option.persistent = Persistent
|
||||
automation.config.jrubyscripting.local_variable.option.global = Global
|
||||
automation.config.jrubyscripting.require.label = Require Scripts
|
||||
automation.config.jrubyscripting.require.description = A comma separated list of script names to be required by the JRuby Scripting Engine before running user scripts.
|
||||
automation.config.jrubyscripting.rubylib.label = RUBYLIB
|
||||
automation.config.jrubyscripting.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" when not specified.
|
@ -0,0 +1,36 @@
|
||||
# add-on
|
||||
|
||||
addon.jrubyscripting.name = JRuby Scripting
|
||||
addon.jrubyscripting.description = Questo aggiunge un motore script JRuby.
|
||||
|
||||
# add-on
|
||||
|
||||
automation.config.jrubyscripting.check_update.label = Controlla per Aggiornamenti di Gem
|
||||
automation.config.jrubyscripting.check_update.description = Controllare RubyGems per gli aggiornamenti alle gem di cui sopra quando OpenHAB inizia o le impostazioni JRuby vengono modificate. Altrimenti proverà a soddisfare i requisiti con gem installate localmente, e puoi gestirli da soli con un Ruby esterno impostando lo stesso GEM_HOME.
|
||||
automation.config.jrubyscripting.dependency_tracking.label = Abilita Monitoraggio Delle Dipendenze
|
||||
automation.config.jrubyscripting.dependency_tracking.description = Il monitoraggio delle dipendenze consente agli script di ricaricare automaticamente quando una delle sue dipendenze viene aggiornata. Si consiglia di disabilitare il tracciamento delle dipendenze se si prevede di modificare o aggiornare una libreria condivisa, ma non vuoi che tutti gli script siano ricaricati fino a quando non puoi provarlo.
|
||||
automation.config.jrubyscripting.gem_home.label = GEM_HOME
|
||||
automation.config.jrubyscripting.gem_home.description = Il percorso da dove le Ruby Gems saranno installate e caricate. La directory verrà creata se necessario. È possibile utilizare i valori <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> e/o <tt>{RUBY_VERSION}</tt> come sostituzioni per puntare automaticamente a una nuova directory quando l''addon viene aggiornato con una nuova versione di JRuby. Il valore predefinito è "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" se non specificato.
|
||||
automation.config.jrubyscripting.gems.label = Ruby Gems
|
||||
automation.config.jrubyscripting.gems.description = Un elenco separato da virgola di Ruby Gems da installare. Le versioni possono essere vincolate separando con un <tt>\\\=</tt> e quindi il vincolo di versione standard di RubyGems, come "<tt>openhab-scripting\\\=~>5.</tt>".
|
||||
automation.config.jrubyscripting.group.environment.label = Ambiente Di Ruby
|
||||
automation.config.jrubyscripting.group.environment.description = Questo gruppo definisce l'ambiente di Ruby.
|
||||
automation.config.jrubyscripting.group.gems.label = Ruby Gems
|
||||
automation.config.jrubyscripting.group.gems.description = Questo gruppo definisce la lista di Ruby Gems da installare.
|
||||
automation.config.jrubyscripting.group.system.label = Proprietà di sistema
|
||||
automation.config.jrubyscripting.group.system.description = Questo gruppo definisce le proprietà del sistema JRuby.
|
||||
automation.config.jrubyscripting.local_context.label = Tipo Context Instance
|
||||
automation.config.jrubyscripting.local_context.description = Il contesto locale contiene le coppie di runtime Ruby, none-valore per la condivisione delle variabili tra Java e Ruby. Vedere <a href\\\="https\\\://github.com/jruby/jruby/wiki/RedBridge\\\#Context_Instance_Type">la documentazione</a> per le opzioni e i dettagli.
|
||||
automation.config.jrubyscripting.local_context.option.singleton = Singleton
|
||||
automation.config.jrubyscripting.local_context.option.threadsafe = Thread Sicura
|
||||
automation.config.jrubyscripting.local_context.option.singlethread = Thread Singola
|
||||
automation.config.jrubyscripting.local_context.option.concurrent = Concorrente
|
||||
automation.config.jrubyscripting.local_variable.label = Comportamento Variabile Locale
|
||||
automation.config.jrubyscripting.local_variable.description = Definisce come le variabili sono condivise tra Ruby e Java. Vedi <a href\\\="https\\\://github.com/jruby/jruby/wiki/RedBridge\\\#local-variable-behavior-options">la documentazione</a> per le opzioni e i dettagli.
|
||||
automation.config.jrubyscripting.local_variable.option.transient = Transitorio
|
||||
automation.config.jrubyscripting.local_variable.option.persistent = Persistente
|
||||
automation.config.jrubyscripting.local_variable.option.global = Globale
|
||||
automation.config.jrubyscripting.require.label = Richiede Scripts
|
||||
automation.config.jrubyscripting.require.description = Un elenco separato da virgola di nomi di script che devono essere richiesti dal motore di scripting JRuby prima di eseguire gli script utente.
|
||||
automation.config.jrubyscripting.rubylib.label = RUBYLIB
|
||||
automation.config.jrubyscripting.rubylib.description = Percorso di ricerca per le librerie utente. Separare ogni percorso con due punti (con un punto e virgola in Windows). Predefinito "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" se non specificato.
|
@ -313,6 +313,7 @@ See [openhab-js : items](https://openhab.github.io/openhab-js/items.html) for fu
|
||||
|
||||
- items : `object`
|
||||
- .NAME ⇒ `Item`
|
||||
- .existsItem(name) ⇒ `boolean`
|
||||
- .getItem(name, nullIfMissing) ⇒ `Item`
|
||||
- .getItems() ⇒ `Array[Item]`
|
||||
- .getItemsByTag(...tagNames) ⇒ `Array[Item]`
|
||||
@ -332,7 +333,7 @@ Calling `getItem(...)` or `...` returns an `Item` object with the following prop
|
||||
|
||||
- Item : `object`
|
||||
- .rawItem ⇒ `HostItem`
|
||||
- .history ⇒ [`ItemHistory`](#itemhistory)
|
||||
- .persistence ⇒ [`ItemPersistence`](#itempersistence)
|
||||
- .semantics ⇒ [`ItemSemantics`](https://openhab.github.io/openhab-js/items.ItemSemantics.html)
|
||||
- .type ⇒ `string`
|
||||
- .name ⇒ `string`
|
||||
@ -351,6 +352,9 @@ Calling `getItem(...)` or `...` returns an `Item` object with the following prop
|
||||
- .removeMetadata(namespace) ⇒ `object|null`
|
||||
- .sendCommand(value): `value` can be a string, a [`time.ZonedDateTime`](#time) or a [`Quantity`](#quantity)
|
||||
- .sendCommandIfDifferent(value) ⇒ `boolean`: `value` can be a string, a [`time.ZonedDateTime`](#time) or a [`Quantity`](#quantity)
|
||||
- .sendIncreaseCommand(value) ⇒ `boolean`: `value` can be a number, or a [`Quantity`](#quantity)
|
||||
- .sendDecreaseCommand(value) ⇒ `boolean`: `value` can be a number, or a [`Quantity`](#quantity)
|
||||
- .sendToggleCommand(): Sends a command to flip the Item's state (e.g. if it is 'ON' an 'OFF' command is sent).
|
||||
- .postUpdate(value): `value` can be a string, a [`time.ZonedDateTime`](#time) or a [`Quantity`](#quantity)
|
||||
- .addGroups(...groupNamesOrItems)
|
||||
- .removeGroups(...groupNamesOrItems)
|
||||
@ -402,8 +406,8 @@ items.replaceItem({
|
||||
tags: ['Lightbulb'],
|
||||
channels: {
|
||||
'binding:thing:device:hallway#light': {},
|
||||
'binding:thing:device:livingroom#light': {
|
||||
profile: 'system:follow'
|
||||
'binding:thing:device:livingroom#light': {
|
||||
profile: 'system:follow'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
@ -432,62 +436,121 @@ items.replaceItem({
|
||||
|
||||
See [openhab-js : ItemConfig](https://openhab.github.io/openhab-js/global.html#ItemConfig) for full API documentation.
|
||||
|
||||
#### `ItemHistory`
|
||||
#### `ItemPersistence`
|
||||
|
||||
Calling `Item.history` returns an `ItemHistory` object with the following functions:
|
||||
Calling `Item.persistence` returns an `ItemPersistence` object with the following functions:
|
||||
|
||||
- ItemHistory :`object`
|
||||
- .averageBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .averageSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .changedBetween(begin, end, serviceId) ⇒ `boolean`
|
||||
- ItemPersistence :`object`
|
||||
- .averageSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .averageUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .averageBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
- .changedSince(timestamp, serviceId) ⇒ `boolean`
|
||||
- .deltaBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .deltaSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .deviationBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .deviationSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .evolutionRateBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .changedUntil(timestamp, serviceId) ⇒ `boolean`
|
||||
- .changedBetween(begin, end, serviceId) ⇒ `boolean`
|
||||
- .countSince(timestamp, serviceId) ⇒ `number`
|
||||
- .countUntil(timestamp, serviceId) ⇒ `number`
|
||||
- .countBetween(begin, end, serviceId) ⇒ `number`
|
||||
- .countStateChangesSince(timestamp, serviceId) ⇒ `number`
|
||||
- .countStateChangesUntil(timestamp, serviceId) ⇒ `number`
|
||||
- .countStateChangesBetween(begin, end, serviceId) ⇒ `number`
|
||||
- .deltaSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .deltaUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .deltaBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
- .deviationSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .deviationUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .deviationBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
- .evolutionRateSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .getAllStatesBetween(begin, end, serviceId) ⇒ `Array[HistoricItem]`
|
||||
- .getAllStatesSince(timestamp, serviceId) ⇒ `Array[HistoricItem]`
|
||||
- .historicState(timestamp, serviceId) ⇒ `HistoricItem | null`
|
||||
- .evolutionRateUntil(timestamp, serviceId) ⇒ `number | null`
|
||||
- .evolutionRateBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .getAllStatesSince(timestamp, serviceId) ⇒ `Array[PersistedItem]`
|
||||
- .getAllStatesUntil(timestamp, serviceId) ⇒ `Array[PersistedItem]`
|
||||
- .getAllStatesBetween(begin, end, serviceId) ⇒ `Array[PersistedItem]`
|
||||
- .lastUpdate(serviceId) ⇒ `ZonedDateTime | null`
|
||||
- .latestState(serviceId) ⇒ `string | null`
|
||||
- .maximumBetween(begin, end, serviceId) ⇒ `HistoricItem | null`
|
||||
- .maximumSince(timestamp,serviceId) ⇒ `HistoricItem | null`
|
||||
- .minimumSince(begin, end, serviceId) ⇒ `HistoricItem | null`
|
||||
- .minimumSince(timestamp, serviceId) ⇒ `HistoricItem | null`
|
||||
- .persist(serviceId)
|
||||
- .previousState(skipEqual, serviceId) ⇒ `HistoricItem | null`
|
||||
- .sumBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .sumSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .updatedBetween(begin, end, serviceId) ⇒ `boolean`
|
||||
- .nextUpdate(serviceId) ⇒ `ZonedDateTime | null`
|
||||
- .lastChange(serviceId) ⇒ `ZonedDateTime | null`
|
||||
- .nextChange(serviceId) ⇒ `ZonedDateTime | null`
|
||||
- .maximumSince(timestamp, serviceId) ⇒ `PersistedItem | null`
|
||||
- .maximumUntil(timestamp, serviceId) ⇒ `PersistedItem | null`
|
||||
- .maximumBetween(begin, end, serviceId) ⇒ `PersistedItem | null`
|
||||
- .minimumSince(timestamp, serviceId) ⇒ `PersistedItem | null`
|
||||
- .minimumUntil(timestamp, serviceId) ⇒ `PersistedItem | null`
|
||||
- .minimumBetween(begin, end, serviceId) ⇒ `PersistedItem | null`
|
||||
- .medianSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .medianUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .medianBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
- .persist(serviceId): Tells the persistence service to store the current Item state, which is then done asynchronously.
|
||||
**Warning:** This has the side effect, that if the Item state changes shortly after `.persist` has been called, the new Item state will be persisted. See [JSDoc](https://openhab.github.io/openhab-js/items.ItemPersistence.html#persist) for a possible work-around.
|
||||
- .persist(timestamp, state, serviceId): Tells the persistence service to store the given state at the given timestamp, which is then done asynchronously.
|
||||
- .persist(timeSeries, serviceId): Tells the persistence service to store the given [`TimeSeries`](#timeseries), which is then done asynchronously.
|
||||
- .persistedState(timestamp, serviceId) ⇒ `PersistedItem | null`
|
||||
- .previousState(skipEqual, serviceId) ⇒ `PersistedItem | null`
|
||||
- .nextState(skipEqual, serviceId) ⇒ `PersistedItem | null`
|
||||
- .sumSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .sumUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .sumBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
- .updatedSince(timestamp, serviceId) ⇒ `boolean`
|
||||
- .varianceBetween(begin, end, serviceId) ⇒ `number | null`
|
||||
- .varianceSince(timestamp, serviceId) ⇒ `number | null`
|
||||
- .updatedUntil(timestamp, serviceId) ⇒ `boolean`
|
||||
- .updatedBetween(begin, end, serviceId) ⇒ `boolean`
|
||||
- .varianceSince(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .varianceUntil(timestamp, serviceId) ⇒ `PersistedState | null`
|
||||
- .varianceBetween(begin, end, serviceId) ⇒ `PersistedState | null`
|
||||
|
||||
Note: `serviceId` is optional, if omitted, the default persistence service will be used.
|
||||
|
||||
```javascript
|
||||
var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
||||
var item = items.KitchenDimmer;
|
||||
console.log('KitchenDimmer averageSince', item.history.averageSince(yesterday));
|
||||
console.log('KitchenDimmer averageSince', item.persistence.averageSince(yesterday));
|
||||
```
|
||||
|
||||
The `HistoricItem` object contains the following properties, representing Item state and the respective timestamp:
|
||||
The `PersistedState` object contains the following properties, representing Item state:
|
||||
|
||||
- `state`: State as string
|
||||
- `numericState`: State as number, if state can be represented as number, or `null` if that's not the case
|
||||
- `quantityState`: Item state as [`Quantity`](#quantity) or `null` if state is not Quantity-compatible
|
||||
- `rawState`: State as Java `State` object
|
||||
|
||||
The `PersistedItem` object extends `PersistedState` with the following properties, representing Item state and the respective timestamp:
|
||||
|
||||
- `timestamp`: Timestamp as [`time.ZonedDateTime`](#time)
|
||||
- `instant`: Timestamp as [`time.Instant`](#time)
|
||||
|
||||
```javascript
|
||||
var midnight = time.toZDT('00:00');
|
||||
var historic = items.KitchenDimmer.history.maximumSince(midnight);
|
||||
var historic = items.KitchenDimmer.persistence.maximumSince(midnight);
|
||||
console.log('KitchenDimmer maximum was ', historic.state, ' at ', historic.timestamp);
|
||||
```
|
||||
|
||||
See [openhab-js : ItemHistory](https://openhab.github.io/openhab-js/items.ItemHistory.html) for full API documentation.
|
||||
See [openhab-js : ItemPersistence](https://openhab.github.io/openhab-js/items.ItemPersistence.html) for full API documentation.
|
||||
|
||||
#### `TimeSeries`
|
||||
|
||||
A `TimeSeries` is used to transport a set of states together with their timestamp.
|
||||
It is usually used for persisting historic state or forecasts in a persistence service by using [`ItemPersistence.persist`](#itempersistence).
|
||||
|
||||
When creating a new `TimeSeries`, a policy must be chosen - it defines how the `TimeSeries` is persisted in a persistence service:
|
||||
|
||||
- `ADD` adds the content to the persistence, well suited for persisting historic data.
|
||||
- `REPLACE` first removes all persisted elements in the timespan given by begin and end of the `TimeSeries`, well suited for persisting forecasts.
|
||||
|
||||
A `TimeSeries` object has the following properties and methods:
|
||||
|
||||
- `policy`: The persistence policy, either `ADD` or `REPLACE`.
|
||||
- `begin`: Timestamp of the first element of the `TimeSeries`.
|
||||
- `end`: Timestamp of the last element of the `TimeSeries`.
|
||||
- `size`: Number of elements in the `TimeSeries`.
|
||||
- `states`: States of the `TimeSeries` together with their timestamp and sorted by their timestamps.
|
||||
Be aware that this returns a reference to the internal state array, so changes to the array will affect the `TimeSeries`.
|
||||
- `add(timestamp, state)`: Add a given state to the `TimeSeries` at the given timestamp.
|
||||
|
||||
The following example shows how to create a `TimeSeries`:
|
||||
|
||||
```javascript
|
||||
var timeSeries = new items.TimeSeries('ADD'); // Create a new TimeSeries with policy ADD
|
||||
timeSeries.add(time.toZDT('2024-01-01T14:53'), Quantity('5 m')).add(time.toZDT().minusMinutes(2), Quantity('0 m')).add(time.toZDT().plusMinutes(5), Quantity('5 m'));
|
||||
console.log(ts); // Let's have a look at the TimeSeries
|
||||
items.getItem('MyDistanceItem').persistence.persist(timeSeries, 'influxdb'); // Persist the TimeSeries for the Item 'MyDistanceItem' using the InfluxDB persistence service
|
||||
```
|
||||
|
||||
### Things
|
||||
|
||||
@ -531,7 +594,10 @@ thing.setEnabled(false);
|
||||
The actions namespace allows interactions with openHAB actions.
|
||||
The following are a list of standard actions.
|
||||
|
||||
Note that most of the actions currently do **not** provide type definitions and therefore auto-completion does not work.
|
||||
**Warning:** Please be aware, that (unless not explicitly noted) there is **no** type conversion from Java to JavaScript types for the return values of actions.
|
||||
Read the JavaDoc linked from the JSDoc to learn about the returned Java types.
|
||||
|
||||
Please note that most of the actions currently do **not** provide type definitions and therefore auto-completion does not work.
|
||||
|
||||
See [openhab-js : actions](https://openhab.github.io/openhab-js/actions.html) for full API documentation and additional actions.
|
||||
|
||||
@ -539,20 +605,27 @@ See [openhab-js : actions](https://openhab.github.io/openhab-js/actions.html) fo
|
||||
|
||||
See [openhab-js : actions.Audio](https://openhab.github.io/openhab-js/actions.html#.Audio) for complete documentation.
|
||||
|
||||
#### BusEvent
|
||||
#### BusEvent Actions
|
||||
|
||||
See [openhab-js : actions.BusEvent](https://openhab.github.io/openhab-js/actions.html#.BusEvent) for complete documentation.
|
||||
|
||||
#### CoreUtil Actions
|
||||
|
||||
See [openhab-js : actions.CoreUtil](https://openhab.github.io/openhab-js/actions.html#.CoreUtil) for complete documentation.
|
||||
|
||||
The `CoreUtil` actions provide access to parts of the utilities included in openHAB core, see [org.openhab.core.util](https://www.openhab.org/javadoc/latest/org/openhab/core/util/package-summary).
|
||||
These include several methods to convert between color types like HSB, RGB, sRGB, RGBW and XY.
|
||||
|
||||
#### Ephemeris Actions
|
||||
|
||||
See [openhab-js : actions.Ephemeris](https://openhab.github.io/openhab-js/actions.html#.Ephemeris) for complete documentation.
|
||||
|
||||
Ephemeris is a way to determine what type of day today or a number of days before or after today is. For example, a way to determine if today is a weekend, a bank holiday, someone’s birthday, trash day, etc.
|
||||
Ephemeris is a way to determine what type of day today or a number of days before or after today is.
|
||||
For example, a way to determine if today is a weekend, a public holiday, someone’s birthday, trash day, etc.
|
||||
|
||||
Additional information can be found on the [Ephemeris Actions Docs](https://www.openhab.org/docs/configuration/actions.html#ephemeris) as well as the [Ephemeris JavaDoc](https://www.openhab.org/javadoc/latest/org/openhab/core/model/script/actions/ephemeris).
|
||||
|
||||
```javascript
|
||||
// Example
|
||||
var weekend = actions.Ephemeris.isWeekend();
|
||||
```
|
||||
|
||||
@ -584,6 +657,15 @@ var response = actions.HTTP.sendHttpGetRequest('<url>');
|
||||
|
||||
Replace `<url>` with the request url.
|
||||
|
||||
#### Ping Actions
|
||||
|
||||
See [openhab-js : actions.Ping](https://openhab.github.io/openhab-js/actions.html#.Ping) for complete documentation.
|
||||
|
||||
```javascript
|
||||
// Check if a host is reachable
|
||||
var reachable = actions.Ping.checkVitality(host, port, timeout); // host: string, port: int, timeout: int
|
||||
```
|
||||
|
||||
#### ScriptExecution Actions
|
||||
|
||||
The `ScriptExecution` actions provide the `callScript(string scriptName)` method, which calls a script located at the `$OH_CONF/scripts` folder, as well as the `createTimer` method.
|
||||
@ -643,16 +725,6 @@ myTimer.reschedule(now.plusSeconds(5));
|
||||
|
||||
See [openhab-js : actions.ScriptExecution](https://openhab.github.io/openhab-js/actions.ScriptExecution.html) for complete documentation.
|
||||
|
||||
#### Semantics Actions
|
||||
|
||||
See [openhab-js : actions.Semantics](https://openhab.github.io/openhab-js/actions.html#.Semantics) for complete documentation.
|
||||
|
||||
#### Thing Actions
|
||||
|
||||
It is possible to get the actions for a Thing using `actions.Things.getActions(bindingId, thingUid)`, e.g. `actions.Things.getActions('network', 'network:pingdevice:pc')`.
|
||||
|
||||
See [openhab-js : actions.Things](https://openhab.github.io/openhab-js/actions.html#.Things) for complete documentation.
|
||||
|
||||
#### Transformation Actions
|
||||
|
||||
openHAB provides various [data transformation services](https://www.openhab.org/addons/#transform) which can translate between technical and human-readable values.
|
||||
@ -671,45 +743,99 @@ See [openhab-js : actions.Voice](https://openhab.github.io/openhab-js/actions.ht
|
||||
|
||||
#### Cloud Notification Actions
|
||||
|
||||
Note: Optional action if [openHAB Cloud Connector](https://www.openhab.org/addons/integrations/openhabcloud/) is installed.
|
||||
Requires the [openHAB Cloud Connector](https://www.openhab.org/addons/integrations/openhabcloud/) to be installed.
|
||||
|
||||
Notification actions may be placed in rules to send alerts to mobile devices registered with an [openHAB Cloud instance](https://github.com/openhab/openhab-cloud) such as [myopenHAB.org](https://myopenhab.org/).
|
||||
|
||||
For available actions have a look at the [Cloud Notification Actions Docs](https://www.openhab.org/docs/configuration/actions.html#cloud-notification-actions).
|
||||
There are three different types of notifications:
|
||||
|
||||
- Broadcast Notifications: Sent to all registered devices and shown as notification on these devices.
|
||||
- Standard Notifications: Sent to the registered devices of the specified user and shown as notification on his devices.
|
||||
- Log Notifications: Only shown in the notification log, e.g. inside the Android and iOS Apps.
|
||||
|
||||
In addition to that, notifications can be updated later be re-using the same `referenceId` and hidden/removed either by `referenceId` or `tag`.
|
||||
|
||||
To send these three types of notifications, use the `notificationBuilder(message)` method of the `actions` namespace.
|
||||
`message` is optional and may be omitted.
|
||||
It returns a new `NotificationBuilder` object, which by default sends a broadcast notification and provides the following methods:
|
||||
|
||||
- `.logOnly()`: Send a log notification only.
|
||||
- `.hide()`: Hides notification(s) with the specified `referenceId` or `tag` (`referenceId` has precedence over `tag`).
|
||||
- `.addUserId(emailAddress)`: By adding the email address(es) of specific openHAB Cloud user(s), the notification is only sent to this (these) user(s).
|
||||
To add multiple users, either call `addUserId` multiple times or pass mutiple emails as multiple params, e.g. `addUserId(emailAddress1, emailAddress2)`.
|
||||
- `.withIcon(icon)`: Sets the icon of the notification.
|
||||
- `.withTag(tag)`: Sets the tag of the notification. Used for grouping notifications and to hide/remove groups of notifications.
|
||||
- `.withTitle(title)`: Sets the title of the notification.
|
||||
- `.withReferenceId(referenceId)`: Sets the reference ID of the notification. If none is set, but it might be useful, a random UUID will be generated.
|
||||
The reference ID can be used to update or hide the notification later by using the same reference ID again.
|
||||
- `.withOnClickAction(action)`: Sets the action to be executed when the notification is clicked.
|
||||
- `.withMediaAttachmentUrl(mediaAttachmentUrl)`: Sets the URL of a media attachment to be displayed with the notification. This URL must be reachable by the push notification client.
|
||||
- `.addActionButton(label, action)`: Adds an action button to the notification. Please note that due to Android and iOS limitations, only three action buttons are supported.
|
||||
- `.send()` ⇒ `string|null`: Sends the notification and returns the reference ID or `null` for log notifications and when hiding notifications.
|
||||
|
||||
The syntax for the `action` parameter is described in [openHAB Cloud Connector: Action Syntax](https://www.openhab.org/addons/integrations/openhabcloud/#action-syntax).
|
||||
|
||||
The syntax for the `mediaAttachmentUrl` parameter is described in [openHAB Cloud Connector](https://www.openhab.org/addons/integrations/openhabcloud/).
|
||||
|
||||
```javascript
|
||||
// Example
|
||||
actions.NotificationAction.sendNotification('<email>', '<message>'); // to a single myopenHAB user identified by e-mail
|
||||
actions.NotificationAction.sendBroadcastNotification('<message>'); // to all myopenHAB users
|
||||
// Send a simple broadcast notification
|
||||
actions.notificationBuilder('Hello World!').send();
|
||||
// Send a broadcast notification with icon, tag and title
|
||||
actions.notificationBuilder('Hello World!')
|
||||
.withIcon('f7:bell_fill').withTag('important').withTitle('Important Notification').send();
|
||||
// Send a broadcast notification with icon, tag, title, media attachment URL and actions
|
||||
actions.notificationBuilder('Hello World!')
|
||||
.withIcon('f7:bell_fill').withTag('important').withTitle('Important Notification')
|
||||
.withOnClickAction('ui:navigate:/page/my_floorplan_page').withMediaAttachmentUrl('http://example.com/image.jpg')
|
||||
.addActionButton('Turn Kitchen Light ON', 'command:KitchenLights:ON').addActionButton('Turn Kitchen Light OFF', 'command:KitchenLights:OFF').send();
|
||||
|
||||
// Send a simple standard notification to two specific users
|
||||
actions.notificationBuilder('Hello World!').addUserId('florian@example.com').addUserId('florian@example.org').send();
|
||||
// Send a standard notification with icon, tag and title to two specific users
|
||||
actions.notificationBuilder('Hello World!').addUserId('florian@example.com').addUserId('florian@example.org')
|
||||
.withIcon('f7:bell_fill').withTag('important').withTitle('Important notification').send();
|
||||
|
||||
// Sends a simple log notification
|
||||
actions.notificationBuilder('Hello World!').logOnly().send();
|
||||
// Sends a simple log notification with icon and tag
|
||||
actions.notificationBuilder('Hello World!').logOnly()
|
||||
.withIcon('f7:bell_fill').withTag('important').send();
|
||||
```
|
||||
|
||||
Replace `<email>` with the e-mail address of the user.
|
||||
Replace `<message>` with the notification text.
|
||||
See [openhab-js : actions.NotificationBuilder](https://openhab.github.io/openhab-js/actions.html#.notificationBuilder) for complete documentation.
|
||||
|
||||
### Cache
|
||||
|
||||
The cache namespace provides both a private and a shared cache that can be used to set and retrieve objects that will be persisted between subsequent runs of the same or between scripts.
|
||||
The cache namespace provides both a private and a shared cache that can be used to set and retrieve data that will be persisted between subsequent runs of the same or between scripts.
|
||||
|
||||
The private cache can only be accessed by the same script and is cleared when the script is unloaded.
|
||||
You can use it to e.g. store timers or counters between subsequent runs of that script.
|
||||
You can use it to store primitives and objects, e.g. store timers or counters between subsequent runs of that script.
|
||||
When a script is unloaded and its cache is cleared, all timers (see [`createTimer`](#createtimer)) stored in its private cache are automatically cancelled.
|
||||
|
||||
The shared cache is shared across all rules and scripts, it can therefore be accessed from any automation language.
|
||||
The access to every key is tracked and the key is removed when all scripts that ever accessed that key are unloaded.
|
||||
If that key stored a timer, the timer is cancelled.
|
||||
If that key stored a timer, the timer will be cancelled.
|
||||
You can use it to store primitives and **Java** objects, e.g. store timers or counters between multiple scripts.
|
||||
|
||||
Due to a multi-threading limitation in GraalJS (the JavaScript engine used by JavaScript Scripting), it is not recommended to store JavaScript objects in the shared cache.
|
||||
Multi-threaded access to JavaScript objects will lead to script execution failure!
|
||||
You can work-around that limitation by either serialising and deserialising JS objects or by switching to their Java counterparts.
|
||||
|
||||
Timers as created by [`createTimer`](#createtimer) can be stored in the shared cache.
|
||||
The ids of timers and intervals as created by `setTimeout` and `setInterval` cannot be shared across scripts as these ids are local to the script where they were created.
|
||||
|
||||
See [openhab-js : cache](https://openhab.github.io/openhab-js/cache.html) for full API documentation.
|
||||
|
||||
- cache : <code>object</code>
|
||||
- .private
|
||||
- .get(key, defaultSupplier) ⇒ <code>Object | null</code>
|
||||
- .put(key, value) ⇒ <code>Previous Object | null</code>
|
||||
- .remove(key) ⇒ <code>Previous Object | null</code>
|
||||
- .get(key, defaultSupplier) ⇒ <code>* | null</code>
|
||||
- .put(key, value) ⇒ <code>Previous * | null</code>
|
||||
- .remove(key) ⇒ <code>Previous * | null</code>
|
||||
- .exists(key) ⇒ <code>boolean</code>
|
||||
- .shared
|
||||
- .get(key, defaultSupplier) ⇒ <code>Object | null</code>
|
||||
- .put(key, value) ⇒ <code>Previous Object | null</code>
|
||||
- .remove(key) ⇒ <code>Previous Object | null</code>
|
||||
- .get(key, defaultSupplier) ⇒ <code>* | null</code>
|
||||
- .put(key, value) ⇒ <code>Previous * | null</code>
|
||||
- .remove(key) ⇒ <code>Previous * | null</code>
|
||||
- .exists(key) ⇒ <code>boolean</code>
|
||||
|
||||
The `defaultSupplier` provided function will return a default value if a specified key is not already associated with a value.
|
||||
@ -717,19 +843,17 @@ The `defaultSupplier` provided function will return a default value if a specifi
|
||||
**Example** *(Get a previously set value with a default value (times = 0))*
|
||||
|
||||
```js
|
||||
var counter = cache.private.get('counter', () => ({ 'times': 0 }));
|
||||
console.log('Count', counter.times++);
|
||||
var counter = cache.shared.get('counter', () => 0);
|
||||
console.log('Counter: ' + counter);
|
||||
```
|
||||
|
||||
**Example** *(Get a previously set object)*
|
||||
**Example** *(Get a previously set value, modify and store it)*
|
||||
|
||||
```js
|
||||
var counter = cache.private.get('counter');
|
||||
if (counter === null) {
|
||||
counter = { times: 0 };
|
||||
cache.private.put('counter', counter);
|
||||
}
|
||||
console.log('Count', counter.times++);
|
||||
counter++;
|
||||
console.log('Counter: ' + counter);
|
||||
cache.private.put('counter', counter);
|
||||
```
|
||||
|
||||
### Time
|
||||
@ -746,7 +870,7 @@ Examples:
|
||||
var now = time.ZonedDateTime.now();
|
||||
var yesterday = time.ZonedDateTime.now().minusHours(24);
|
||||
var item = items.Kitchen;
|
||||
console.log("averageSince", item.history.averageSince(yesterday));
|
||||
console.log("averageSince", item.persistence.averageSince(yesterday));
|
||||
```
|
||||
|
||||
```javascript
|
||||
@ -755,6 +879,32 @@ actions.Exec.executeCommandLine(time.Duration.ofSeconds(20), 'echo', 'Hello Worl
|
||||
|
||||
See [JS-Joda](https://js-joda.github.io/js-joda/) for more examples and complete API usage.
|
||||
|
||||
#### Parsing and Formatting
|
||||
|
||||
Occasionally, one will need to parse a non-supported date time string or generate one from a ZonedDateTime.
|
||||
To do this you will use [JS-Joda DateTimeFormatter and potentially your Locale](https://js-joda.github.io/js-joda/manual/formatting.html).
|
||||
However, shipping all the locales with the openhab-js library would lead to an unacceptable large size.
|
||||
Therefore, if you attempt to use the `DateTimeFormatter` and receive an error saying it cannot find your locale, you will need to manually install your locale and import it into your rule.
|
||||
|
||||
[JS-Joda Locales](https://github.com/js-joda/js-joda/tree/master/packages/locale#use-prebuilt-locale-packages) includes a list of all the supported locales.
|
||||
Each locale consists of a two letter language indicator followed by a "-" and a two letter dialect indicator: e.g. "EN-US".
|
||||
Installing a locale can be done through the command `npm install @js-joda/locale_de-de` from the *$OPENHAB_CONF/automation/js* folder.
|
||||
|
||||
To import and use a local into your rule you need to require it and create a `DateTimeFormatter` that uses it:
|
||||
|
||||
```javascript
|
||||
var Locale = require('@js-joda/locale_de-de').Locale.GERMAN;
|
||||
var formatter = time.DateTimeFormatter.ofPattern('dd.MM.yyyy HH:mm').withLocale(Locale);
|
||||
```
|
||||
|
||||
#### `time.javaInstantToJsInstant()`
|
||||
|
||||
Converts a [`java.time.Instant`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Instant.html) to a JS-Joda [`Instant`](https://js-joda.github.io/js-joda/manual/Instant.html).
|
||||
|
||||
#### `time.javaZDTToJsZDT()`
|
||||
|
||||
Converts a [`java.time.ZonedDateTime`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/ZonedDateTime.html) to a JS-Joda [`ZonedDateTime`](https://js-joda.github.io/js-joda/manual/ZonedDateTime.html).
|
||||
|
||||
#### `time.toZDT()`
|
||||
|
||||
There will be times when this automatic conversion is not available (for example when working with date times within a rule).
|
||||
@ -902,7 +1052,7 @@ The argument `value` can be a Quantity-compatible `Item`, a string, a `Quantity`
|
||||
`value` strings have the `$amount $unit` format and must follow these rules:
|
||||
|
||||
- `$amount` is required with a number provided as string
|
||||
- `$unit` is optional (unitless quantities are possible) and can have a prefix like `m` (milli) or `M` (mega)
|
||||
- `$unit` is optional (unit-less quantities are possible) and can have a prefix like `m` (milli) or `M` (mega)
|
||||
- `$unit` does not allow whitespaces.
|
||||
- `$unit` does allow superscript, e.g. `²` instead of `^2`.
|
||||
- `$unit` requires the `*` between two units to be present, although you usually omit it (which is mathematically seen allowed, but openHAB needs the `*`).
|
||||
@ -1000,7 +1150,7 @@ See [openhab-js : rules](https://openhab.github.io/openhab-js/rules.html) for fu
|
||||
|
||||
### JSRule
|
||||
|
||||
JSRules provides a simple, declarative syntax for defining rules that will be executed based on a trigger condition
|
||||
`JSRule` provides a simple, declarative syntax for defining rules that will be executed based on a trigger condition:
|
||||
|
||||
```javascript
|
||||
var email = "juliet@capulet.org"
|
||||
@ -1064,6 +1214,9 @@ triggers.DateTimeTrigger('MyDateTimeItem');
|
||||
|
||||
You can use `null` for a trigger parameter to skip its configuration.
|
||||
|
||||
You may use `SwitchableJSRule` to create a rule that can be enabled and disabled with a Switch Item.
|
||||
As an extension to `JSRule`, its syntax is the same, however you can specify an Item name (using the `switchItemName` rule config property) if you don't like the automatically created Item's name.
|
||||
|
||||
See [openhab-js : triggers](https://openhab.github.io/openhab-js/triggers.html) in the API documentation for a full list of all triggers.
|
||||
|
||||
### Rule Builder
|
||||
@ -1094,7 +1247,7 @@ Operations and conditions can also optionally take functions:
|
||||
|
||||
```javascript
|
||||
rules.when().item("F1_light").changed().then(event => {
|
||||
console.log(event);
|
||||
console.log(event);
|
||||
}).build("Test Rule", "My Test Rule");
|
||||
```
|
||||
|
||||
@ -1106,11 +1259,11 @@ See [Examples](#rule-builder-examples) for further patterns.
|
||||
|
||||
- `when()`
|
||||
- `or()`
|
||||
- `.channel(channelName)` Specifies a channel event as a source for the rule to fire.
|
||||
- `.triggered(event)` Trigger on a specific event name
|
||||
- `.cron(cronExpression)` Specifies a cron schedule for the rule to fire.
|
||||
- `.timeOfDay(time)` Specifies a time of day in `HH:mm` for the rule to fire.
|
||||
- `.item(itemName)` Specifies an Item as the source of changes to trigger a rule.
|
||||
- `.channel(channelName)`: Specifies a channel event as a source for the rule to fire.
|
||||
- `.triggered(event)`: Trigger on a specific event name
|
||||
- `.cron(cronExpression)`: Specifies a cron schedule for the rule to fire.
|
||||
- `.timeOfDay(time)`: Specifies a time of day in `HH:mm` for the rule to fire.
|
||||
- `.item(itemName)`: Specifies an Item as the source of changes to trigger a rule.
|
||||
- `.for(duration)`
|
||||
- `.from(state)`
|
||||
- `.fromOn()`
|
||||
@ -1121,7 +1274,7 @@ See [Examples](#rule-builder-examples) for further patterns.
|
||||
- `.receivedCommand()`
|
||||
- `.receivedUpdate()`
|
||||
- `.changed()`
|
||||
- `.memberOf(groupName)` Specifies a group Item as the source of changes to trigger the rule.
|
||||
- `.memberOf(groupName)`: Specifies a group Item as the source of changes to trigger the rule.
|
||||
- `.for(duration)`
|
||||
- `.from(state)`
|
||||
- `.fromOn()`
|
||||
@ -1132,20 +1285,21 @@ See [Examples](#rule-builder-examples) for further patterns.
|
||||
- `.receivedCommand()`
|
||||
- `.receivedUpdate()`
|
||||
- `.changed()`
|
||||
- `.system()` Specifies a system event as a source for the rule to fire.
|
||||
- `.system()`: Specifies a system event as a source for the rule to fire.
|
||||
- `.ruleEngineStarted()`
|
||||
- `.rulesLoaded()`
|
||||
- `.startupComplete()`
|
||||
- `.thingsInitialized()`
|
||||
- `.userInterfacesStarted()`
|
||||
- `.startLevel(level)`
|
||||
- `.thing(thingName)` Specifies a Thing event as a source for the rule to fire.
|
||||
- `.thing(thingName)`: Specifies a Thing event as a source for the rule to fire.
|
||||
- `changed()`
|
||||
- `updated()`
|
||||
- `from(state)`
|
||||
- `to(state)`
|
||||
- `.dateTime(itemName)` Specifies a DateTime Item whose (optional) date and time schedule the rule to fire.
|
||||
- `.timeOnly()` Only the time of the Item should be compared, the date should be ignored.
|
||||
- `.dateTime(itemName)`: Specifies a DateTime Item whose (optional) date and time schedule the rule to fire.
|
||||
- `.timeOnly()`: Only the time of the Item should be compared, the date should be ignored.
|
||||
- `.withOffset(offset)`: The offset in seconds to add to the time of the DateTime Item.
|
||||
|
||||
Additionally, all the above triggers have the following functions:
|
||||
|
||||
@ -1158,6 +1312,8 @@ Additionally, all the above triggers have the following functions:
|
||||
- `if(optionalFunction)`
|
||||
- `.stateOfItem(itemName)`
|
||||
- `is(state)`
|
||||
- `isOn()`
|
||||
- `isOff()`
|
||||
- `in(state...)`
|
||||
|
||||
#### Rule Builder Operations
|
||||
@ -1238,12 +1394,15 @@ This table gives an overview over the `event` object:
|
||||
| `thingUID` | `Thing****Trigger` | UID of Thing that triggered event | N/A |
|
||||
| `cronExpression` | `GenericCronTrigger` | Cron expression of the trigger | N/A |
|
||||
| `time` | `TimeOfDayTrigger` | Time of day value of the trigger | N/A |
|
||||
| `timeOnly` | `DateTimeTrigger` | Whether the trigger only considers the time part of the DateTime Item | N/A |
|
||||
| `offset` | `DateTimeTrigger` | Offset in seconds added to the time of the DateTime Item | N/A |
|
||||
| `eventType` | all except `PWMTrigger`, `PIDTrigger` | Type of event that triggered event (change, command, triggered, update, time) | N/A |
|
||||
| `triggerType` | all except `PWMTrigger`, `PIDTrigger` | Type of trigger that triggered event | N/A |
|
||||
| `eventClass` | all | Java class name of the triggering event | N/A |
|
||||
| `module` | all | (user-defined or auto-generated) name of trigger | N/A |
|
||||
| `raw` | all | Original contents of the event including data passed from a calling rule | N/A |
|
||||
|
||||
All properties are typeof `string`.
|
||||
All properties are typeof `string` except for properties contained by `raw` which are unmodified from the original types.
|
||||
|
||||
Please note that when using `GenericEventTrigger`, the available properties depend on the chosen event types.
|
||||
It is not possible for the openhab-js library to provide type conversions for all properties of all openHAB events, as those are too many.
|
||||
@ -1290,7 +1449,7 @@ Follow these steps to create your own library (it's called a CommonJS module):
|
||||
function someFunction () {
|
||||
console.log('Hello from your personal library!');
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
someProperty,
|
||||
someFunction
|
||||
@ -1298,7 +1457,7 @@ Follow these steps to create your own library (it's called a CommonJS module):
|
||||
```
|
||||
|
||||
4. Tar it up by running `npm pack` from your library's folder.
|
||||
5. Install it by running `npm install <name>-<version>.tgz` from the `automation/js` folder.
|
||||
5. Install it by running `npm install <path-to-library-folder>/<name>-<version>.tgz` from the `automation/js` folder.
|
||||
6. After you've installed it with `npm`, you can continue development of the library inside `node_modules`.
|
||||
|
||||
It is also possible to upload your library to [npm](https://npmjs.com) to share it with other users.
|
||||
@ -1307,76 +1466,24 @@ If you want to get some advanced information, you can read [this blog post](http
|
||||
|
||||
### @runtime
|
||||
|
||||
One can access many useful utilities and types using `require("@runtime")`, e.g.
|
||||
In most cases, the [Standard Library](#standard-library) provides pure-JS APIs to interact with the openHAB runtime.
|
||||
Generally speaking, you should therefore prefer to use [Standard Library](#standard-library) provided by this library instead.
|
||||
|
||||
However, in some cases, e.g. when needing a [`HSBType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/hsbtype), one needs to access raw Java utilities and types.
|
||||
This can be achieved by using `require('@runtime')`, e.g.
|
||||
|
||||
```javascript
|
||||
var { ON, OFF, QuantityType } = require("@runtime");
|
||||
var { ON, OFF, QuantityType } = require('@runtime');
|
||||
// Alternative, more verbose way to achieve the same:
|
||||
//
|
||||
// var runtime = require("@runtime");
|
||||
// var runtime = require('@runtime');
|
||||
//
|
||||
// var ON = runtime.ON;
|
||||
// var OFF = runtime.OFF;
|
||||
// var QuantityType = runtime.QuantityType;
|
||||
```
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `State` | [`org.openhab.core.types.State`](https://www.openhab.org/javadoc/latest/org/openhab/core/types/state) |
|
||||
| `Command` | [`org.openhab.core.types.Command`](https://www.openhab.org/javadoc/latest/org/openhab/core/types/command) |
|
||||
| `URLEncoder` | [`java.net.URLEncoder`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/URLEncoder.html) |
|
||||
| `File` | [`java.io.File`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/File.html) |
|
||||
| `Files` | [`java.nio.file.Files`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/Files.html) |
|
||||
| `Path` | [`java.nio.file.Path`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/Path.html) |
|
||||
| `Paths` | [`java.nio.file.Paths`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/Paths.html) |
|
||||
| `IncreaseDecreaseType` | [`org.openhab.core.library.types.IncreaseDecreaseType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/increasedecreasetype) |
|
||||
| `DECREASE` | `IncreaseDecreaseType` enum item |
|
||||
| `INCREASE` | `IncreaseDecreaseType` enum item |
|
||||
| `OnOffType` | [`org.openhab.core.library.types.OnOffType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/onofftype) |
|
||||
| `ON` | `OnOffType` enum item |
|
||||
| `OFF` | `OnOffType` enum item |
|
||||
| `OpenClosedType` | [`org.openhab.core.library.types.OpenClosedType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/openclosedtype) |
|
||||
| `OPEN` | `OpenClosedType` enum item |
|
||||
| `CLOSED` | `OpenClosedType` enum item |
|
||||
| `StopMoveType` | [`org.openhab.core.library.types.StopMoveType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/stopmovetype) |
|
||||
| `STOP` | `StopMoveType` enum item |
|
||||
| `MOVE` | `StopMoveType` enum item |
|
||||
| `UpDownType` | [`org.openhab.core.library.types.UpDownType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/updowntype) |
|
||||
| `UP` | `UpDownType` enum item |
|
||||
| `DOWN` | `UpDownType` enum item |
|
||||
| `UnDefType` | [`org.openhab.core.library.types.UnDefType`](https://www.openhab.org/javadoc/latest/org/openhab/core/types/undeftype) |
|
||||
| `NULL` | `UnDefType` enum item |
|
||||
| `UNDEF` | `UnDefType` enum item |
|
||||
| `RefreshType` | [`org.openhab.core.library.types.RefreshType`](https://www.openhab.org/javadoc/latest/org/openhab/core/types/refreshtype) |
|
||||
| `REFRESH` | `RefreshType` enum item |
|
||||
| `NextPreviousType` | [`org.openhab.core.library.types.NextPreviusType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/nextprevioustype) |
|
||||
| `NEXT` | `NextPreviousType` enum item |
|
||||
| `PREVIOUS` | `NextPreviousType` enum item |
|
||||
| `PlayPauseType` | [`org.openhab.core.library.types.PlayPauseType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/playpausetype) |
|
||||
| `PLAY` | `PlayPauseType` enum item |
|
||||
| `PAUSE` | `PlayPauseType` enum item |
|
||||
| `RewindFastforwardType` | [`org.openhab.core.library.types.RewindFastforwardType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/rewindfastforwardtype) |
|
||||
| `REWIND` | `RewindFastforwardType` enum item |
|
||||
| `FASTFORWARD` | `RewindFastforwardType` enum item |
|
||||
| `QuantityType` | [`org.openhab.core.library.types.QuantityType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/quantitytype) |
|
||||
| `StringListType` | [`org.openhab.core.library.types.StringListType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/stringlisttype) |
|
||||
| `RawType` | [`org.openhab.core.library.types.RawType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/rawtype) |
|
||||
| `DateTimeType` | [`org.openhab.core.library.types.DateTimeType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/datetimetype) |
|
||||
| `DecimalType` | [`org.openhab.core.library.types.DecimalType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/decimaltype) |
|
||||
| `HSBType` | [`org.openhab.core.library.types.HSBType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/hsbtype) |
|
||||
| `PercentType` | [`org.openhab.core.library.types.PercentType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/percenttype) |
|
||||
| `PointType` | [`org.openhab.core.library.types.PointType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/pointtype) |
|
||||
| `StringType` | [`org.openhab.core.library.types.StringType`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/types/stringtype) |
|
||||
| `SIUnits` | [`org.openhab.core.library.unit.SIUnits`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/unit/siunits) |
|
||||
| `ImperialUnits` | [`org.openhab.core.library.unit.ImperialUnits`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/unit/imperialunits) |
|
||||
| `MetricPrefix` | [`org.openhab.core.library.unit.MetricPrefix`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/unit/metricprefix) |
|
||||
| `Units` | [`org.openhab.core.library.unit.Units`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/unit/units) |
|
||||
| `BinaryPrefix` | [`org.openhab.core.library.unit.BinaryPrefix`](https://www.openhab.org/javadoc/latest/org/openhab/core/library/unit/binaryprefix) |
|
||||
| `ChronoUnit` | [`java.time.temporal.ChronoUnit`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/temporal/ChronoUnit.html) |
|
||||
| `Duration` | [`java.time.Duration`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) |
|
||||
| `ZoneId` | [`java.time.ZoneId`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/ZoneId.html) |
|
||||
| `ZonedDateTime` | [`java.time.ZonedDateTime`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/ZonedDateTime.html) |
|
||||
A list of available utilities and types can be found in the [JSR223 Default Preset documentation](https://www.openhab.org/docs/configuration/jsr223.html#default-preset-importpreset-not-required).
|
||||
|
||||
`require("@runtime")` also defines "services" such as `items`, `things`, `rules`, `events`, `actions`, `ir`, `itemRegistry`.
|
||||
`require('@runtime')` also defines "services" such as `items`, `things`, `rules`, `events`, `actions`, `ir`, `itemRegistry`.
|
||||
You can use these services for backwards compatibility purposes or ease migration from JSR223 scripts.
|
||||
Generally speaking, you should prefer to use [Standard Library](#standard-library) provided by this library instead.
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.jsscripting</artifactId>
|
||||
@ -22,32 +22,47 @@
|
||||
!jdk.internal.reflect.*,
|
||||
!jdk.vm.ci.services
|
||||
</bnd.importpackage>
|
||||
<graal.version>22.0.0.2</graal.version> <!-- DO NOT UPGRADE: 22.0.0.2 is the latest version working on armv7l / OpenJDK 11.0.16 & armv7l / Zulu 17.0.5+8 -->
|
||||
<!-- Remember to check if the fix https://github.com/openhab/openhab-core/pull/4437 still works when upgrading GraalJS -->
|
||||
<graaljs.version>24.1.1</graaljs.version>
|
||||
<oh.version>${project.version}</oh.version>
|
||||
<ohjs.version>openhab@4.7.3</ohjs.version>
|
||||
<ohjs.version>openhab@5.8.1</ohjs.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- bundle the modular dependencies into an uber-JAR -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>embed-dependencies</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>unpack-dependencies</goal>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<excludes>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</excludes> <!-- we'll provide this -->
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>org.lastnpe.eea:eea-all</exclude>
|
||||
<exclude>org.apache.karaf.features:framework</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<transformers>
|
||||
<!-- Transformer to merge module-info.class files, if needed -->
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- bundle the openhab-js library -->
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.12.1</version>
|
||||
<version>1.15.0</version>
|
||||
<configuration>
|
||||
<nodeVersion>v16.17.1</nodeVersion> <!-- DO NOT DOWNGRADE: NodeJS < 16 doesn't support Apple Silicon -->
|
||||
<workingDirectory>target/js</workingDirectory>
|
||||
@ -67,7 +82,7 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<!--suppress UnresolvedMavenProperty -->
|
||||
<arguments>install ${ohjs.version} webpack@^5.87.0 webpack-cli@^5.1.4</arguments> <!-- webpack & webpack-cli versions should match to the ones from openhab-js -->
|
||||
<arguments>install ${ohjs.version} webpack@^5.94.0 webpack-cli@^5.1.4</arguments> <!-- webpack & webpack-cli versions should match to the ones from openhab-js -->
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -114,6 +129,7 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- run SAT -->
|
||||
<plugin>
|
||||
<groupId>org.openhab.tools.sat</groupId>
|
||||
<artifactId>sat-plugin</artifactId>
|
||||
@ -126,31 +142,29 @@
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.graalvm.truffle</groupId>
|
||||
<artifactId>truffle-api</artifactId>
|
||||
<version>${graal.version}</version>
|
||||
<groupId>org.graalvm.polyglot</groupId>
|
||||
<artifactId>polyglot</artifactId>
|
||||
<version>${graaljs.version}</version>
|
||||
</dependency>
|
||||
<!-- Graal JavaScript ScriptEngine JSR 223 support -->
|
||||
<dependency>
|
||||
<groupId>org.graalvm.js</groupId>
|
||||
<artifactId>js-scriptengine</artifactId>
|
||||
<version>${graal.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.graalvm.sdk</groupId>
|
||||
<artifactId>graal-sdk</artifactId>
|
||||
<version>${graal.version}</version>
|
||||
<version>${graaljs.version}</version>
|
||||
</dependency>
|
||||
<!-- Graal TRegex engine (internally used by Graal JavaScript engine) -->
|
||||
<dependency>
|
||||
<groupId>org.graalvm.regex</groupId>
|
||||
<artifactId>regex</artifactId>
|
||||
<version>${graal.version}</version>
|
||||
<version>${graaljs.version}</version>
|
||||
</dependency>
|
||||
<dependency> <!-- this must come AFTER the regex lib -->
|
||||
<groupId>org.graalvm.js</groupId>
|
||||
<artifactId>js</artifactId>
|
||||
<version>${graal.version}</version>
|
||||
<!-- Graal JavaScript engine (depends on Graal TRegex engine, must be added after it) -->
|
||||
<dependency>
|
||||
<groupId>org.graalvm.polyglot</groupId>
|
||||
<artifactId>js-community</artifactId>
|
||||
<version>${graaljs.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- GraalJS changelog says that com.ibm.icu/icu4j is not required for GraalJS >= 22.0.0 as it moved to org.graalvm.truffle;
|
||||
but GraalJS >= 22.2.0 requires it, so we'll need to add it when we upgrade -->
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -12,11 +12,22 @@
|
||||
*/
|
||||
package org.openhab.automation.jsscripting.internal;
|
||||
|
||||
import static org.openhab.core.automation.module.script.ScriptTransformationService.OPENHAB_TRANSFORMATION_SCRIPT;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.script.Compilable;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.graalvm.polyglot.PolyglotException;
|
||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable;
|
||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -24,26 +35,112 @@ import org.slf4j.LoggerFactory;
|
||||
* Wraps ScriptEngines provided by Graal to provide error messages and stack traces for scripts.
|
||||
*
|
||||
* @author Jonathan Gilbert - Initial contribution
|
||||
* @author Florian Hotze - Improve logger name, Fix memory leak caused by exception logging
|
||||
*/
|
||||
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoCloseable>
|
||||
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<T> {
|
||||
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoCloseable & Compilable & Lock>
|
||||
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T> implements Lock {
|
||||
|
||||
private static final Logger STACK_LOGGER = LoggerFactory
|
||||
.getLogger("org.openhab.automation.script.javascript.stack");
|
||||
private static final int STACK_TRACE_LENGTH = 5;
|
||||
|
||||
private @Nullable Logger logger;
|
||||
|
||||
public DebuggingGraalScriptEngine(T delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeInvocation() {
|
||||
super.beforeInvocation();
|
||||
// OpenhabGraalJSScriptEngine::beforeInvocation will be executed after
|
||||
// DebuggingGraalScriptEngine::beforeInvocation, because GraalJSScriptEngineFactory::createScriptEngine returns
|
||||
// a DebuggingGraalScriptEngine instance.
|
||||
// We therefore need to synchronize logger setup here and cannot rely on the synchronization in
|
||||
// OpenhabGraalJSScriptEngine.
|
||||
delegate.lock();
|
||||
try {
|
||||
if (logger == null) {
|
||||
initializeLogger();
|
||||
}
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid deadlocks
|
||||
delegate.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception afterThrowsInvocation(Exception e) {
|
||||
Throwable cause = e.getCause();
|
||||
// OPS4J Pax Logging holds a reference to the exception, which causes the OpenhabGraalJSScriptEngine to not be
|
||||
// removed from heap by garbage collection and causing a memory leak.
|
||||
// Therefore, don't pass the exceptions itself to the logger, but only their message!
|
||||
if (cause instanceof IllegalArgumentException) {
|
||||
STACK_LOGGER.error("Failed to execute script:", e);
|
||||
}
|
||||
if (cause instanceof PolyglotException) {
|
||||
STACK_LOGGER.error("Failed to execute script:", cause);
|
||||
logger.error("Failed to execute script: {}", stringifyThrowable(cause));
|
||||
} else if (cause instanceof PolyglotException) {
|
||||
logger.error("Failed to execute script: {}", stringifyThrowable(cause));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
private String stringifyThrowable(Throwable throwable) {
|
||||
String message = throwable.getMessage();
|
||||
StackTraceElement[] stackTraceElements = throwable.getStackTrace();
|
||||
String stackTrace = Arrays.stream(stackTraceElements).limit(STACK_TRACE_LENGTH)
|
||||
.map(t -> " at " + t.toString()).collect(Collectors.joining(System.lineSeparator()))
|
||||
+ System.lineSeparator() + " ... " + stackTraceElements.length + " more";
|
||||
return (message != null) ? message + System.lineSeparator() + stackTrace : stackTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the logger.
|
||||
* This cannot be done on script engine creation because the context variables are not yet initialized.
|
||||
* Therefore, the logger needs to be initialized on the first use after script engine creation.
|
||||
*/
|
||||
private void initializeLogger() {
|
||||
ScriptContext ctx = delegate.getContext();
|
||||
Object fileName = ctx.getAttribute("javax.script.filename");
|
||||
Object ruleUID = ctx.getAttribute("ruleUID");
|
||||
Object ohEngineIdentifier = ctx.getAttribute("oh.engine-identifier");
|
||||
|
||||
String identifier = "stack";
|
||||
if (fileName != null) {
|
||||
identifier = fileName.toString().replaceAll("^.*[/\\\\]", "");
|
||||
} else if (ruleUID != null) {
|
||||
identifier = ruleUID.toString();
|
||||
} else if (ohEngineIdentifier != null) {
|
||||
if (ohEngineIdentifier.toString().startsWith(OPENHAB_TRANSFORMATION_SCRIPT)) {
|
||||
identifier = ohEngineIdentifier.toString().replaceAll(OPENHAB_TRANSFORMATION_SCRIPT, "transformation.");
|
||||
}
|
||||
}
|
||||
|
||||
logger = LoggerFactory.getLogger("org.openhab.automation.script.javascript." + identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lock() {
|
||||
delegate.lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lockInterruptibly() throws InterruptedException {
|
||||
delegate.lockInterruptibly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock() {
|
||||
return delegate.tryLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
|
||||
return delegate.tryLock(l, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock() {
|
||||
delegate.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Condition newCondition() {
|
||||
return delegate.newCondition();
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
@ -50,7 +52,7 @@ import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
|
||||
import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
|
||||
import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel;
|
||||
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
|
||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable;
|
||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable;
|
||||
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
@ -69,27 +71,27 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
|
||||
* {@link Lock} for multi-thread synchronization; globals and openhab-js injection code caching
|
||||
*/
|
||||
public class OpenhabGraalJSScriptEngine
|
||||
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
|
||||
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<GraalJSScriptEngine>
|
||||
implements Lock {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
|
||||
private static Source GLOBAL_SOURCE;
|
||||
private static final Source GLOBAL_SOURCE;
|
||||
static {
|
||||
try {
|
||||
GLOBAL_SOURCE = Source.newBuilder("js", getFileAsReader("node_modules/@jsscripting-globals.js"),
|
||||
"@jsscripting-globals.js").cached(true).build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load @jsscripting-globals.js", e);
|
||||
throw new IllegalStateException("Failed to load @jsscripting-globals.js", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Source OPENHAB_JS_SOURCE;
|
||||
private static final Source OPENHAB_JS_SOURCE;
|
||||
static {
|
||||
try {
|
||||
OPENHAB_JS_SOURCE = Source
|
||||
.newBuilder("js", getFileAsReader("node_modules/@openhab-globals.js"), "@openhab-globals.js")
|
||||
.cached(true).build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load @openhab-globals.js", e);
|
||||
throw new IllegalStateException("Failed to load @openhab-globals.js", e);
|
||||
}
|
||||
}
|
||||
private static final String OPENHAB_JS_INJECTION_CODE = "Object.assign(this, require('openhab'));";
|
||||
@ -129,13 +131,15 @@ public class OpenhabGraalJSScriptEngine
|
||||
v -> v.getMember("rawQtyType").as(QuantityType.class), HostAccess.TargetMappingPrecedence.LOW)
|
||||
.build();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
|
||||
|
||||
/** {@link Lock} synchronization of multi-thread access */
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final JSRuntimeFeatures jsRuntimeFeatures;
|
||||
|
||||
// these fields start as null because they are populated on first use
|
||||
private String engineIdentifier;
|
||||
private @Nullable Consumer<String> scriptDependencyListener;
|
||||
private String engineIdentifier; // this field is very helpful for debugging, please do not remove it
|
||||
|
||||
private boolean initialized = false;
|
||||
private final boolean injectionEnabled;
|
||||
@ -152,8 +156,6 @@ public class OpenhabGraalJSScriptEngine
|
||||
this.injectionCachingEnabled = injectionCachingEnabled;
|
||||
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
|
||||
|
||||
LOGGER.debug("Initializing GraalJS script engine...");
|
||||
|
||||
delegate = GraalJSScriptEngine.create(ENGINE,
|
||||
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
|
||||
.allowHostAccess(HOST_ACCESS)
|
||||
@ -161,8 +163,8 @@ public class OpenhabGraalJSScriptEngine
|
||||
.option("js.nashorn-compat", "true") // Enable Nashorn compat mode as openhab-js relies on
|
||||
// accessors, see
|
||||
// https://github.com/oracle/graaljs/blob/master/docs/user/NashornMigrationGuide.md#accessors
|
||||
.option("js.ecmascript-version", "2022") // If Nashorn compat is enabled, it will enforce ES5
|
||||
// compatibility, we want ECMA2022
|
||||
.option("js.ecmascript-version", "2024") // If Nashorn compat is enabled, it will enforce ES5
|
||||
// compatibility, we want ECMA2024
|
||||
.option("js.commonjs-require", "true") // Enable CommonJS module support
|
||||
.hostClassLoader(getClass().getClassLoader())
|
||||
.fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
|
||||
@ -227,7 +229,10 @@ public class OpenhabGraalJSScriptEngine
|
||||
protected void beforeInvocation() {
|
||||
super.beforeInvocation();
|
||||
|
||||
logger.debug("Initializing GraalJS script engine...");
|
||||
|
||||
lock.lock();
|
||||
logger.debug("Lock acquired before invocation.");
|
||||
|
||||
if (initialized) {
|
||||
return;
|
||||
@ -239,10 +244,11 @@ public class OpenhabGraalJSScriptEngine
|
||||
}
|
||||
|
||||
// these are added post-construction, so we need to fetch them late
|
||||
this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
|
||||
if (this.engineIdentifier == null) {
|
||||
String localEngineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
|
||||
if (localEngineIdentifier == null) {
|
||||
throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
|
||||
}
|
||||
this.engineIdentifier = localEngineIdentifier;
|
||||
|
||||
ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
|
||||
.getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
|
||||
@ -250,52 +256,54 @@ public class OpenhabGraalJSScriptEngine
|
||||
throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
|
||||
}
|
||||
|
||||
scriptDependencyListener = (Consumer<String>) ctx
|
||||
.getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
|
||||
if (scriptDependencyListener == null) {
|
||||
LOGGER.warn(
|
||||
Consumer<String> localScriptDependencyListener = (Consumer<String>) ctx
|
||||
.getAttribute(CONTEXT_KEY_DEPENDENCY_LISTENER);
|
||||
if (localScriptDependencyListener == null) {
|
||||
logger.warn(
|
||||
"Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
|
||||
}
|
||||
scriptDependencyListener = localScriptDependencyListener;
|
||||
|
||||
ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
|
||||
scriptExtensionAccessor, lock);
|
||||
|
||||
// Wrap the "require" function to also allow loading modules from the ScriptExtensionModuleProvider
|
||||
Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
|
||||
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
|
||||
.locatorFor(delegate.getPolyglotContext(), localEngineIdentifier).locateModule(moduleName)
|
||||
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
|
||||
delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
|
||||
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
|
||||
|
||||
// Injections into the JS runtime
|
||||
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
|
||||
LOGGER.debug("Injecting {} into the JS runtime...", key);
|
||||
logger.debug("Injecting {} into the JS runtime...", key);
|
||||
delegate.put(key, obj);
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
LOGGER.debug("Evaluating cached global script...");
|
||||
logger.debug("Evaluating cached global script...");
|
||||
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
|
||||
if (this.injectionEnabled) {
|
||||
if (this.injectionCachingEnabled) {
|
||||
LOGGER.debug("Evaluating cached openhab-js injection...");
|
||||
logger.debug("Evaluating cached openhab-js injection...");
|
||||
delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
|
||||
} else {
|
||||
LOGGER.debug("Evaluating openhab-js injection from the file system...");
|
||||
logger.debug("Evaluating openhab-js injection from the file system...");
|
||||
eval(OPENHAB_JS_INJECTION_CODE);
|
||||
}
|
||||
}
|
||||
LOGGER.debug("Successfully initialized GraalJS script engine.");
|
||||
logger.debug("Successfully initialized GraalJS script engine.");
|
||||
} catch (ScriptException e) {
|
||||
LOGGER.error("Could not inject global script", e);
|
||||
logger.error("Could not inject global script", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object afterInvocation(Object obj) {
|
||||
lock.unlock();
|
||||
logger.debug("Lock released after invocation.");
|
||||
return super.afterInvocation(obj);
|
||||
}
|
||||
|
||||
@ -316,7 +324,7 @@ public class OpenhabGraalJSScriptEngine
|
||||
* @param path a root path
|
||||
* @return whether the given path is a node root directory
|
||||
*/
|
||||
private boolean isRootNodePath(Path path) {
|
||||
private static boolean isRootNodePath(Path path) {
|
||||
return path.startsWith(path.getRoot().resolve(NODE_DIR));
|
||||
}
|
||||
|
||||
@ -327,7 +335,7 @@ public class OpenhabGraalJSScriptEngine
|
||||
* @param path a root path, e.g. C:\node_modules\foo.js
|
||||
* @return the class resource path for loading local modules
|
||||
*/
|
||||
private String nodeFileToResource(Path path) {
|
||||
private static String nodeFileToResource(Path path) {
|
||||
return "/" + path.subpath(0, path.getNameCount()).toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
@ -344,4 +352,36 @@ public class OpenhabGraalJSScriptEngine
|
||||
|
||||
return new InputStreamReader(ioStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lock() {
|
||||
lock.lock();
|
||||
logger.debug("Lock acquired.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lockInterruptibly() throws InterruptedException {
|
||||
lock.lockInterruptibly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock() {
|
||||
return lock.tryLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
|
||||
return lock.tryLock(l, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock() {
|
||||
lock.unlock();
|
||||
logger.debug("Lock released.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Condition newCondition() {
|
||||
return lock.newCondition();
|
||||
}
|
||||
}
|
||||
|
@ -48,9 +48,10 @@ public class JSScriptFileWatcher extends AbstractScriptFileWatcher {
|
||||
|
||||
@Override
|
||||
protected Optional<String> getScriptType(Path scriptFilePath) {
|
||||
String scriptType = super.getScriptType(scriptFilePath).orElse(null);
|
||||
if (!scriptFilePath.startsWith(getWatchPath().resolve("node_modules")) && ("js".equals(scriptType))) {
|
||||
return Optional.of(scriptType);
|
||||
Optional<String> scriptType = super.getScriptType(scriptFilePath);
|
||||
if (scriptType.isPresent() && !scriptFilePath.startsWith(getWatchPath().resolve("node_modules"))
|
||||
&& ("js".equals(scriptType.get()))) {
|
||||
return scriptType;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ package org.openhab.automation.jsscripting.internal.scriptengine;
|
||||
import java.io.Reader;
|
||||
|
||||
import javax.script.Bindings;
|
||||
import javax.script.Compilable;
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
@ -29,11 +31,11 @@ import org.eclipse.jdt.annotation.NonNull;
|
||||
*
|
||||
* @author Jonathan Gilbert - Initial contribution
|
||||
*/
|
||||
public abstract class DelegatingScriptEngineWithInvocableAndAutocloseable<T extends ScriptEngine & Invocable & AutoCloseable>
|
||||
implements ScriptEngine, Invocable, AutoCloseable {
|
||||
public abstract class DelegatingScriptEngineWithInvocableAndCompilableAndAutocloseable<T extends ScriptEngine & Invocable & Compilable & AutoCloseable>
|
||||
implements ScriptEngine, Invocable, Compilable, AutoCloseable {
|
||||
protected @NonNull T delegate;
|
||||
|
||||
public DelegatingScriptEngineWithInvocableAndAutocloseable(@NonNull T delegate) {
|
||||
public DelegatingScriptEngineWithInvocableAndCompilableAndAutocloseable(@NonNull T delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@ -128,6 +130,16 @@ public abstract class DelegatingScriptEngineWithInvocableAndAutocloseable<T exte
|
||||
return delegate.getInterface(o, aClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(String s) throws ScriptException {
|
||||
return delegate.compile(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(Reader reader) throws ScriptException {
|
||||
return delegate.compile(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
delegate.close();
|
@ -16,22 +16,24 @@ import java.io.Reader;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
import javax.script.Bindings;
|
||||
import javax.script.Compilable;
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
/**
|
||||
* Delegate allowing AOP-style interception of calls, either before Invocation, or upon a {@link ScriptException}.
|
||||
* being thrown.
|
||||
* Delegate allowing AOP-style interception of calls, either before Invocation, or upon a {@link ScriptException} being
|
||||
* thrown.
|
||||
*
|
||||
* @param <T> The delegate class
|
||||
* @author Jonathan Gilbert - Initial contribution
|
||||
*/
|
||||
public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<T extends ScriptEngine & Invocable & AutoCloseable>
|
||||
extends DelegatingScriptEngineWithInvocableAndAutocloseable<T> {
|
||||
public abstract class InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T extends ScriptEngine & Invocable & Compilable & AutoCloseable>
|
||||
extends DelegatingScriptEngineWithInvocableAndCompilableAndAutocloseable<T> {
|
||||
|
||||
public InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable(T delegate) {
|
||||
public InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable(T delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@ -155,4 +157,28 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
||||
throw new UndeclaredThrowableException(afterThrowsInvocation(e)); // Wrap and rethrow other exceptions
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(String s) throws ScriptException {
|
||||
try {
|
||||
beforeInvocation();
|
||||
return (CompiledScript) afterInvocation(super.compile(s));
|
||||
} catch (ScriptException se) {
|
||||
throw (ScriptException) afterThrowsInvocation(se);
|
||||
} catch (Exception e) {
|
||||
throw new UndeclaredThrowableException(afterThrowsInvocation(e)); // Wrap and rethrow other exceptions
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(Reader reader) throws ScriptException {
|
||||
try {
|
||||
beforeInvocation();
|
||||
return (CompiledScript) afterInvocation(super.compile(reader));
|
||||
} catch (ScriptException se) {
|
||||
throw (ScriptException) afterThrowsInvocation(se);
|
||||
} catch (Exception e) {
|
||||
throw new UndeclaredThrowableException(afterThrowsInvocation(e)); // Wrap and rethrow other exceptions
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,8 @@ import org.openhab.core.automation.module.script.action.Timer;
|
||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||
import org.openhab.core.scheduler.Scheduler;
|
||||
import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
|
||||
@ -36,13 +38,15 @@ import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
||||
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
|
||||
*/
|
||||
public class ThreadsafeTimers {
|
||||
private final Logger logger = LoggerFactory.getLogger(ThreadsafeTimers.class);
|
||||
|
||||
private final Lock lock;
|
||||
private final Scheduler scheduler;
|
||||
private final ScriptExecution scriptExecution;
|
||||
// Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
|
||||
private final Map<Long, ScheduledCompletableFuture<Object>> idSchedulerMapping = new ConcurrentHashMap<>();
|
||||
private AtomicLong lastId = new AtomicLong();
|
||||
private String identifier = "noIdentifier";
|
||||
private String identifier = "javascript";
|
||||
|
||||
public ThreadsafeTimers(Lock lock, ScriptExecution scriptExecution, Scheduler scheduler) {
|
||||
this.lock = lock;
|
||||
@ -81,11 +85,13 @@ public class ThreadsafeTimers {
|
||||
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
|
||||
return scriptExecution.createTimer(identifier, instant, () -> {
|
||||
lock.lock();
|
||||
logger.debug("Lock acquired before timer execution");
|
||||
try {
|
||||
closure.run();
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||
// deadlocks
|
||||
lock.unlock();
|
||||
logger.debug("Lock released after timer execution");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -99,22 +105,28 @@ public class ThreadsafeTimers {
|
||||
* @return Positive integer value which identifies the timer created; this value can be passed to
|
||||
* <code>clearTimeout()</code> to cancel the timeout.
|
||||
*/
|
||||
public long setTimeout(Runnable callback, Long delay) {
|
||||
public long setTimeout(Runnable callback, long delay) {
|
||||
long id = lastId.incrementAndGet();
|
||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||
lock.lock();
|
||||
logger.debug("Lock acquired before timeout execution");
|
||||
try {
|
||||
callback.run();
|
||||
idSchedulerMapping.remove(id);
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||
// deadlocks
|
||||
lock.unlock();
|
||||
logger.debug("Lock released after timeout execution");
|
||||
}
|
||||
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
|
||||
idSchedulerMapping.put(id, future);
|
||||
return id;
|
||||
}
|
||||
|
||||
public long setTimeout(Runnable callback, double delay) {
|
||||
return setTimeout(callback, Math.round(delay));
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout"><code>clearTimeout()</code></a> polyfill.
|
||||
* Cancels a timeout previously created by <code>setTimeout()</code>.
|
||||
@ -138,21 +150,27 @@ public class ThreadsafeTimers {
|
||||
* @return Numeric, non-zero value which identifies the timer created; this value can be passed to
|
||||
* <code>clearInterval()</code> to cancel the interval.
|
||||
*/
|
||||
public long setInterval(Runnable callback, Long delay) {
|
||||
public long setInterval(Runnable callback, long delay) {
|
||||
long id = lastId.incrementAndGet();
|
||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||
lock.lock();
|
||||
logger.debug("Lock acquired before interval execution");
|
||||
try {
|
||||
callback.run();
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||
// deadlocks
|
||||
lock.unlock();
|
||||
logger.debug("Lock released after interval execution");
|
||||
}
|
||||
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
|
||||
idSchedulerMapping.put(id, future);
|
||||
return id;
|
||||
}
|
||||
|
||||
public long setInterval(Runnable callback, double delay) {
|
||||
return setInterval(callback, Math.round(delay));
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
|
||||
* polyfill.
|
||||
|
@ -1,2 +0,0 @@
|
||||
com.oracle.truffle.regex.RegexLanguageProvider
|
||||
com.oracle.truffle.js.lang.JavaScriptLanguageProvider
|
@ -1,8 +1,15 @@
|
||||
# add-on
|
||||
|
||||
addon.jsscripting.name = Javascript programok
|
||||
addon.jsscripting.description = Ez a JavaScript (ECMAScript-2021) programozó motor.
|
||||
|
||||
# add-on
|
||||
|
||||
automation.config.jsscripting.injectionCachingEnabled.label = Az openHAB Javascript könyvtár betöltésének gyorsítótárazása
|
||||
automation.config.jsscripting.injectionCachingEnabled.description = Az openHAB JavaScript könyvtár betöltésének gyorsítótárazását végzi<br>Kapcsolja ki, ha a könyvtárat a felhasználó helyi konfigurációs könyvtárából akarja betölteni "automation/node_modules". A kikapcsolás megnövelheti a betöltési időt, főleg gyengébb rendszereken.
|
||||
automation.config.jsscripting.injectionCachingEnabled.option.true = Könyvtár betöltés gyorsítótárazása
|
||||
automation.config.jsscripting.injectionCachingEnabled.option.false = Ne gyorsítótárazza a könyvtár betöltését
|
||||
automation.config.jsscripting.injectionEnabled.label = Beépített teljes körű változók használata
|
||||
automation.config.jsscripting.injectionEnabled.description = Az összes változó betöltése az OH szkript könyvtárból az összes közös szolgáltatási szabály számára, pl.\: elemek, dolgok, akciók, naplók, stb... <br> Ha kikapcsolja, az OH szkript könyvtárt betöltheti a "<i>require('openhab')</i>" paranccsal
|
||||
automation.config.jsscripting.injectionEnabled.option.true = Beépített változók használata
|
||||
automation.config.jsscripting.injectionEnabled.option.false = Ne használja a beépített változókat
|
||||
|
||||
# service
|
||||
|
||||
service.automation.jsscripting.label = JS szkriptek
|
||||
|
@ -0,0 +1,15 @@
|
||||
# add-on
|
||||
|
||||
addon.jsscripting.name = JavaScript Scripting
|
||||
addon.jsscripting.description = Dit voegt een JS (ECMAScript-2021) scriptengine toe.
|
||||
|
||||
# add-on
|
||||
|
||||
automation.config.jsscripting.injectionCachingEnabled.label = Cache openHAB JavaScript Bibliotheek Injectie
|
||||
automation.config.jsscripting.injectionCachingEnabled.description = Cache de openHAB JavaScript bibliotheek injectie voor optimale prestaties.<br>Schakel deze optie uit om het laden van de bibliotheek uit de lokale gebruikersconfiguratie folder "automation/js/node_modules" toe te staan. Het uitschakelen van caching kan de laadtijd van scripts verhogen, vooral bij minder krachtige systemen.
|
||||
automation.config.jsscripting.injectionCachingEnabled.option.true = Bibliotheek Injectie Cache
|
||||
automation.config.jsscripting.injectionCachingEnabled.option.false = Geen Bibliotheek Injectie Cache
|
||||
automation.config.jsscripting.injectionEnabled.label = Gebruik Ingebouwde Globale Variabelen
|
||||
automation.config.jsscripting.injectionEnabled.description = Importeer alle variabelen uit de openHAB JavaScript bibliotheek in alle rules en scripts voor algemene services zoals items, things, actions, log, enz... <br> Als dit is uitgeschakeld, kan de openHAB JavaScript bibliotheek handmatig worden geïmporteerd met "<i>require('openhab')</i>"
|
||||
automation.config.jsscripting.injectionEnabled.option.true = Ingebouwde Variabelen
|
||||
automation.config.jsscripting.injectionEnabled.option.false = Geen Ingebouwde Variabelen
|
@ -1,21 +1,37 @@
|
||||
// ThreadsafeTimers is injected into the JS runtime
|
||||
// The default identifier of the script has to be computed on script executon, as it is not available at the time of script compilation
|
||||
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
// Append the script file name OR rule UID depending on which is available
|
||||
const defaultIdentifier = 'org.openhab.automation.script' + (globalThis['javax.script.filename'] ? '.file.' + globalThis['javax.script.filename'].replace(/^.*[\\\/]/, '') : globalThis.ruleUID ? '.ui.' + globalThis.ruleUID : '');
|
||||
const System = Java.type('java.lang.System');
|
||||
const formatRegExp = /%[sdj%]/g;
|
||||
// Pass the defaultIdentifier to ThreadsafeTimers to enable naming of scheduled jobs
|
||||
ThreadsafeTimers.setIdentifier(defaultIdentifier);
|
||||
|
||||
function createLogger (name = defaultIdentifier) {
|
||||
return Java.type('org.slf4j.LoggerFactory').getLogger(name);
|
||||
/**
|
||||
* Gets the default identifier of the script.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDefaultIdentifier () {
|
||||
return 'org.openhab.automation.script' + (globalThis['javax.script.filename'] ? '.file.' + globalThis['javax.script.filename'].replace(/^.*[\\\/]/, '') : globalThis.ruleUID ? '.ui.' + globalThis.ruleUID : '');
|
||||
}
|
||||
|
||||
// User configurable
|
||||
let log = createLogger();
|
||||
/**
|
||||
* Gets a logger instance for given name.
|
||||
* @param {string} [name] optional logger name, defaults to the default identifier of the script (see {@link getDefaultIdentifier})
|
||||
* @returns {*} logger instance
|
||||
*/
|
||||
function getLogger (name) {
|
||||
if (typeof name === 'string') {
|
||||
log = Java.type('org.slf4j.LoggerFactory').getLogger(name);
|
||||
}
|
||||
if (log === null) {
|
||||
log = Java.type('org.slf4j.LoggerFactory').getLogger(getDefaultIdentifier());
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
let log = null;
|
||||
|
||||
function stringify (value) {
|
||||
try {
|
||||
@ -86,7 +102,7 @@
|
||||
const console = {
|
||||
assert: function (expression, message) {
|
||||
if (!expression) {
|
||||
log.error(message);
|
||||
getLogger().error(message);
|
||||
}
|
||||
},
|
||||
|
||||
@ -102,32 +118,32 @@
|
||||
|
||||
// update
|
||||
counters[label] = ++counter;
|
||||
log.debug(format.apply(null, [label + ':', counter]));
|
||||
getLogger().debug(format.apply(null, [label + ':', counter]));
|
||||
}
|
||||
},
|
||||
|
||||
debug: function () {
|
||||
log.debug(format.apply(null, arguments));
|
||||
getLogger().debug(format.apply(null, arguments));
|
||||
},
|
||||
|
||||
info: function () {
|
||||
log.info(format.apply(null, arguments));
|
||||
getLogger().info(format.apply(null, arguments));
|
||||
},
|
||||
|
||||
log: function () {
|
||||
log.info(format.apply(null, arguments));
|
||||
getLogger().info(format.apply(null, arguments));
|
||||
},
|
||||
|
||||
warn: function () {
|
||||
log.warn(format.apply(null, arguments));
|
||||
getLogger().warn(format.apply(null, arguments));
|
||||
},
|
||||
|
||||
error: function () {
|
||||
log.error(format.apply(null, arguments));
|
||||
getLogger().error(format.apply(null, arguments));
|
||||
},
|
||||
|
||||
trace: function () {
|
||||
log.trace(new Error(format.apply(null, arguments)).stack.replace(/^Error: /, ''));
|
||||
getLogger().trace(new Error(format.apply(null, arguments)).stack.replace(/^Error: /, ''));
|
||||
},
|
||||
|
||||
time: function (label) {
|
||||
@ -140,10 +156,10 @@
|
||||
if (label) {
|
||||
const now = System.currentTimeMillis();
|
||||
if (timers.hasOwnProperty(label)) {
|
||||
log.info(format.apply(null, [label + ':', (now - timers[label]) + 'ms']));
|
||||
getLogger().info(format.apply(null, [label + ':', (now - timers[label]) + 'ms']));
|
||||
delete timers[label];
|
||||
} else {
|
||||
log.info(format.apply(null, [label + ':', '<no timer>']));
|
||||
getLogger().info(format.apply(null, [label + ':', '<no timer>']));
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -151,23 +167,25 @@
|
||||
// Allow user customizable logging names
|
||||
// Be aware that a log4j2 required a logger defined for the logger name, otherwise messages won't be logged!
|
||||
set loggerName (name) {
|
||||
log = createLogger(name);
|
||||
getLogger(name);
|
||||
this._loggerName = name;
|
||||
ThreadsafeTimers.setIdentifier(name);
|
||||
},
|
||||
|
||||
get loggerName () {
|
||||
return this._loggerName || defaultIdentifier;
|
||||
return this._loggerName || getDefaultIdentifier();
|
||||
}
|
||||
};
|
||||
|
||||
// Polyfill common NodeJS functions onto the global object
|
||||
globalThis.console = console;
|
||||
globalThis.setTimeout = function (functionRef, delay, ...args) {
|
||||
ThreadsafeTimers.setIdentifier(console.loggerName);
|
||||
return ThreadsafeTimers.setTimeout(() => functionRef(...args), delay);
|
||||
};
|
||||
globalThis.clearTimeout = ThreadsafeTimers.clearTimeout;
|
||||
globalThis.setInterval = function (functionRef, delay, ...args) {
|
||||
ThreadsafeTimers.setIdentifier(console.loggerName);
|
||||
return ThreadsafeTimers.setInterval(() => functionRef(...args), delay);
|
||||
};
|
||||
globalThis.clearInterval = ThreadsafeTimers.clearInterval;
|
||||
|
@ -1,2 +1,3 @@
|
||||
# Please check here how to add suppressions https://maven.apache.org/plugins/maven-pmd-plugin/examples/violation-exclusions.html
|
||||
org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable=AvoidThrowingNullPointerException,AvoidCatchingNPE
|
||||
org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine=UnusedPrivateField
|
||||
org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable=AvoidThrowingNullPointerException,AvoidCatchingNPE
|
||||
|
@ -21,7 +21,7 @@ If you create an empty file called `test.nashornjs`, you will see a log line wit
|
||||
|
||||
To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to enable debug logging for the automation functionality:
|
||||
|
||||
```text
|
||||
```shell
|
||||
log:set DEBUG org.openhab.core.automation
|
||||
```
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.jsscriptingnashorn</artifactId>
|
||||
|
@ -1,23 +1,16 @@
|
||||
# Jython Scripting (DEPRECATED)
|
||||
# Jython Scripting
|
||||
|
||||
::: tip Note:
|
||||
Currently, the development of Jython stopped at version 2.7 with no definite timeline to support Python 3.x.
|
||||
The 3rd party openHAB helper library for Jython is also no longer maintained.
|
||||
We would not recommend using Jython scripting at this point in time.
|
||||
For alternatives, check out the list of other supported [automation add-ons](https://www.openhab.org/addons/#automation).
|
||||
:::
|
||||
|
||||
This add-on provides [Jython](https://www.jython.org/) 2.7.2 that can be used as a scripting language within automation rules and which eliminates the need to download Jython and create `EXTRA_JAVA_OPTS` entries for `bootclasspath`, `python.home` and `python.path`.
|
||||
This add-on provides [Jython](https://www.jython.org/) 2.7 that can be used as a scripting language within automation rules and which eliminates the need to download Jython and create `EXTRA_JAVA_OPTS` entries for `bootclasspath`, `python.home` and `python.path`.
|
||||
|
||||
The `python.home` system property is set to the path of the add-on.
|
||||
|
||||
The `python.path` system property is set to `$OPENHAB_CONF/automation/lib/python`, but any existing `python.path` will be appended to it.
|
||||
The `python.path` system property is set to `$OPENHAB_CONF/automation/jython/lib`, but any existing `python.path` will be appended to it.
|
||||
|
||||
## Creating Jython Scripts
|
||||
|
||||
When this add-on is installed, you can select Jython as a scripting language when creating a script action within the rule editor of the UI.
|
||||
|
||||
Alternatively, you can create scripts in the `automation/jsr223` configuration directory.
|
||||
Alternatively, you can create scripts in the `automation/jython` configuration directory.
|
||||
If you create an empty file called `test.py`, you will see a log line with information similar to:
|
||||
|
||||
```text
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.jythonscripting</artifactId>
|
||||
@ -16,14 +16,14 @@
|
||||
|
||||
<properties>
|
||||
<bnd.fixupmessages><![CDATA["Classes found in the wrong directory","The default package '.' is not permitted by the Import-Package syntax"; restrict:=error; is:=warning]]></bnd.fixupmessages>
|
||||
<bnd.importpackage>*blockhound*;resolution:=optional,com.cloudius.util;resolution:=optional,com.github.luben.zstd;resolution:=optional,com.informix.jdbc;resolution:=optional,com.jcraft.jzlib;resolution:=optional,com.ning.compress.*;resolution:=optional,com.oracle.svm.core.annotate;resolution:=optional,com.sun.management;resolution:=optional,custom_proxymaker.tests;resolution:=optional,jnr.*;resolution;resolution:=optional,*jpountz*;resolution:=optional,junit.framework;resolution:=optional,lzma.sdk.*;resolution:=optional,oracle.*;resolution:=optional,org.antlr.stringtemplate;resolution:=optional,org.apache.tools.*;resolution:=optional,org.brotli.dec;resolution:=optional,org.checkerframework.*;resolution:=optional,org.conscrypt;resolution:=optional,org.eclipse.jetty.*;resolution:=optional,org.hamcrest;resolution:=optional,org.jboss.marshalling;resolution:=optional,org.junit.*;resolution:=optional,org.python.apache.xml.resolver.*;resolution:=optional,org.python.google.*;resolution:=optional,org.python.netty.internal.tcnative;resolution:=optional,org.python.objectweb.asm.tree.*;resolution:=optional,org.python.proxies;resolution:=optional,org.tukaani.xz;resolution:=optional,sun.*;resolution:=optional</bnd.importpackage>
|
||||
<bnd.importpackage>*blockhound*;resolution:=optional,com.cloudius.util;resolution:=optional,com.github.luben.zstd;resolution:=optional,com.informix.jdbc;resolution:=optional,com.jcraft.jzlib;resolution:=optional,com.ning.compress.*;resolution:=optional,com.oracle.svm.core.annotate;resolution:=optional,com.sun.management;resolution:=optional,custom_proxymaker.tests;resolution:=optional,jnr.*;resolution;resolution:=optional,*jpountz*;resolution:=optional,junit.framework;resolution:=optional,lzma.sdk.*;resolution:=optional,oracle.*;resolution:=optional,org.antlr.stringtemplate;resolution:=optional,org.apache.tools.*;resolution:=optional,org.brotli.dec;resolution:=optional,org.checkerframework.*;resolution:=optional,org.conscrypt;resolution:=optional,org.eclipse.jetty.*;resolution:=optional,org.hamcrest;resolution:=optional,org.jboss.marshalling;resolution:=optional,org.junit.*;resolution:=optional,org.python.apache.xml.resolver.*;resolution:=optional,org.python.google.*;resolution:=optional,org.python.netty.internal.tcnative;resolution:=optional,org.python.objectweb.asm.tree.*;resolution:=optional,org.python.proxies;resolution:=optional,org.tukaani.xz;resolution:=optional,sun.*;resolution:=optional,com.aayushatharva.brotli4j.*;resolution:=optional,javax.annotation.meta;resolution:=optional,org.python.bouncycastle.*;resolution:=optional</bnd.importpackage>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.python</groupId>
|
||||
<artifactId>jython-standalone</artifactId>
|
||||
<version>2.7.2</version>
|
||||
<version>2.7.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -10,15 +10,15 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.automation.jythonscripting;
|
||||
package org.openhab.automation.jythonscripting.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
|
||||
@ -36,93 +36,84 @@ import org.osgi.service.component.annotations.Deactivate;
|
||||
*
|
||||
* @author Scott Rushworth - Initial contribution
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Holger Hees - Further development
|
||||
*/
|
||||
@Component(service = ScriptEngineFactory.class)
|
||||
@NonNullByDefault
|
||||
public class JythonScriptEngineFactory extends AbstractScriptEngineFactory {
|
||||
|
||||
private static final String PYTHON_CACHEDIR = "python.cachedir";
|
||||
private static final String PYTHON_HOME = "python.home";
|
||||
private static final String PYTHON_HOME_PATH = JythonScriptEngineFactory.class.getProtectionDomain().getCodeSource()
|
||||
.getLocation().toString().replace("file:", "");
|
||||
|
||||
private static final String PYTHON_PATH = "python.path";
|
||||
private static final String PYTHON_DEFAULT_PATH = Paths
|
||||
.get(OpenHAB.getConfigFolder(), "automation", "jython", "lib").toString();
|
||||
|
||||
private static final String DEFAULT_PYTHON_PATH = Paths
|
||||
.get(OpenHAB.getConfigFolder(), "automation", "lib", "python").toString();
|
||||
private static final String PYTHON_CACHEDIR = "python.cachedir";
|
||||
private static final String PYTHON_CACHEDIR_PATH = Paths
|
||||
.get(OpenHAB.getUserDataFolder(), "cache", JythonScriptEngineFactory.class.getPackageName(), "cachedir")
|
||||
.toString();
|
||||
|
||||
private static final String SCRIPT_TYPE = "py";
|
||||
private static final javax.script.ScriptEngineManager ENGINE_MANAGER = new javax.script.ScriptEngineManager();
|
||||
private static final org.python.jsr223.PyScriptEngineFactory factory = new org.python.jsr223.PyScriptEngineFactory();
|
||||
|
||||
private final List<String> scriptTypes = (List<String>) Stream.of(factory.getExtensions(), factory.getMimeTypes())
|
||||
.flatMap(List::stream) //
|
||||
.toList();
|
||||
|
||||
@Activate
|
||||
public JythonScriptEngineFactory() {
|
||||
logger.debug("Loading JythonScriptEngineFactory");
|
||||
|
||||
String pythonHome = JythonScriptEngineFactory.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toString().replace("file:", "");
|
||||
System.setProperty(PYTHON_HOME, pythonHome);
|
||||
System.setProperty(PYTHON_HOME, PYTHON_HOME_PATH);
|
||||
|
||||
Set<String> pythonPathList = new TreeSet<>(Arrays.asList(PYTHON_DEFAULT_PATH));
|
||||
String existingPythonPath = System.getProperty(PYTHON_PATH);
|
||||
if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
|
||||
pythonPathList.addAll(Arrays.asList(existingPythonPath.split(File.pathSeparator)));
|
||||
}
|
||||
System.setProperty(PYTHON_PATH, String.join(File.pathSeparator, pythonPathList));
|
||||
|
||||
System.setProperty(PYTHON_CACHEDIR, PYTHON_CACHEDIR_PATH);
|
||||
|
||||
logPythonPaths();
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void cleanup() {
|
||||
logger.debug("Unloading JythonScriptEngineFactory");
|
||||
|
||||
System.clearProperty(PYTHON_HOME);
|
||||
|
||||
String existingPythonPath = System.getProperty(PYTHON_PATH);
|
||||
if (existingPythonPath == null || existingPythonPath.isEmpty()) {
|
||||
System.setProperty(PYTHON_PATH, DEFAULT_PYTHON_PATH);
|
||||
} else if (!existingPythonPath.contains(DEFAULT_PYTHON_PATH)) {
|
||||
if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
|
||||
Set<String> newPythonPathList = new TreeSet<>(Arrays.asList(existingPythonPath.split(File.pathSeparator)));
|
||||
newPythonPathList.add(DEFAULT_PYTHON_PATH);
|
||||
newPythonPathList.remove(PYTHON_DEFAULT_PATH);
|
||||
System.setProperty(PYTHON_PATH, String.join(File.pathSeparator, newPythonPathList));
|
||||
}
|
||||
|
||||
System.setProperty(PYTHON_CACHEDIR, Paths
|
||||
.get(OpenHAB.getUserDataFolder(), "cache", JythonScriptEngineFactory.class.getPackageName(), "cachedir")
|
||||
.toString());
|
||||
System.clearProperty(PYTHON_CACHEDIR);
|
||||
|
||||
logPythonPaths();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getScriptTypes() {
|
||||
return scriptTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
|
||||
if (!scriptTypes.contains(scriptType)) {
|
||||
return null;
|
||||
}
|
||||
return factory.getScriptEngine();
|
||||
}
|
||||
|
||||
private void logPythonPaths() {
|
||||
logger.trace("{}: {}, {}: {}, {}: {}", //
|
||||
PYTHON_HOME, System.getProperty(PYTHON_HOME), //
|
||||
PYTHON_PATH, System.getProperty(PYTHON_PATH), //
|
||||
PYTHON_CACHEDIR, System.getProperty(PYTHON_CACHEDIR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getScriptTypes() {
|
||||
List<String> scriptTypes = new ArrayList<>();
|
||||
|
||||
for (javax.script.ScriptEngineFactory factory : ENGINE_MANAGER.getEngineFactories()) {
|
||||
List<String> extensions = factory.getExtensions();
|
||||
|
||||
if (extensions.contains(SCRIPT_TYPE)) {
|
||||
scriptTypes.addAll(extensions);
|
||||
scriptTypes.addAll(factory.getMimeTypes());
|
||||
}
|
||||
}
|
||||
return scriptTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
|
||||
ScriptEngine scriptEngine = ENGINE_MANAGER.getEngineByExtension(scriptType);
|
||||
if (scriptEngine == null) {
|
||||
scriptEngine = ENGINE_MANAGER.getEngineByMimeType(scriptType);
|
||||
}
|
||||
if (scriptEngine == null) {
|
||||
scriptEngine = ENGINE_MANAGER.getEngineByName(scriptType);
|
||||
}
|
||||
return scriptEngine;
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void removePythonPath() {
|
||||
logger.debug("Unloading JythonScriptEngineFactory");
|
||||
|
||||
String existingPythonPath = System.getProperty(PYTHON_PATH);
|
||||
if (existingPythonPath != null && existingPythonPath.contains(DEFAULT_PYTHON_PATH)) {
|
||||
Set<String> newPythonPathList = new TreeSet<>(Arrays.asList(existingPythonPath.split(File.pathSeparator)));
|
||||
newPythonPathList.remove(DEFAULT_PYTHON_PATH);
|
||||
System.setProperty(PYTHON_PATH, String.join(File.pathSeparator, newPythonPathList));
|
||||
}
|
||||
|
||||
System.clearProperty(PYTHON_HOME);
|
||||
System.clearProperty(PYTHON_CACHEDIR);
|
||||
|
||||
logPythonPaths();
|
||||
}
|
||||
}
|
@ -11,10 +11,11 @@
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
@org.osgi.annotation.bundle.Header(name = org.osgi.framework.Constants.DYNAMICIMPORT_PACKAGE, value = "*")
|
||||
package org.openhab.automation.jythonscripting;
|
||||
package org.openhab.automation.jythonscripting.internal;
|
||||
|
||||
/**
|
||||
* Additional information for the Jython Scripting package
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Holger Hees - Further development
|
||||
*/
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.automation.jythonscripting.internal.watch;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
|
||||
import org.openhab.core.automation.module.script.ScriptEngineManager;
|
||||
import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher;
|
||||
import org.openhab.core.automation.module.script.rulesupport.loader.ScriptFileWatcher;
|
||||
import org.openhab.core.service.ReadyService;
|
||||
import org.openhab.core.service.StartLevelService;
|
||||
import org.openhab.core.service.WatchService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Monitors {@code <openHAB-conf>/automation/jython} for Jython files, but not libraries
|
||||
*
|
||||
* @author Holger Hees - Initial contribution
|
||||
*/
|
||||
@Component(immediate = true, service = { ScriptFileWatcher.class, ScriptDependencyTracker.Listener.class })
|
||||
@NonNullByDefault
|
||||
public class JythonScriptFileWatcher extends AbstractScriptFileWatcher {
|
||||
private static final String FILE_DIRECTORY = "automation" + File.separator + "jython";
|
||||
|
||||
@Activate
|
||||
public JythonScriptFileWatcher(
|
||||
final @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService,
|
||||
final @Reference ScriptEngineManager manager, final @Reference ReadyService readyService,
|
||||
final @Reference StartLevelService startLevelService) {
|
||||
super(watchService, manager, readyService, startLevelService, FILE_DIRECTORY, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<String> getScriptType(Path scriptFilePath) {
|
||||
Optional<String> scriptType = super.getScriptType(scriptFilePath);
|
||||
if (scriptType.isPresent() && !scriptFilePath.startsWith(getWatchPath().resolve("lib"))
|
||||
&& ("py".equals(scriptType.get()))) {
|
||||
return scriptType;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="pidcontroller" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<addon:addon id="jythonscripting" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>automation</type>
|
||||
<name>Jython Scripting (DEPRECATED)</name>
|
||||
<name>Jython Scripting</name>
|
||||
<description>This adds a Jython script engine.</description>
|
||||
<connection>none</connection>
|
||||
|
||||
<service-id>org.openhab.automation.jythonscripting</service-id>
|
||||
<config-description-ref uri="automation:jythonscripting"/>
|
||||
|
||||
</addon:addon>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.pidcontroller</artifactId>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.automation.pwm</artifactId>
|
||||
|
@ -73,7 +73,7 @@ Remember that the host and port parameter are not needed in most cases.
|
||||
As discussed above care is taken that the brightness channel only allows values from 1 to 100 by specifying a min and max value in the sitemap for the dimmers.
|
||||
For this example to run on an openHAB version older than 2.5 Bedroom 1's Slider must be removed in the sitemap since older versions don't support the min/max setting.
|
||||
|
||||
## demo.things
|
||||
## `demo.things` Example
|
||||
|
||||
```java
|
||||
Bridge adorne:hub:home "Adorne Hub" [host="192.160.1.111", port=2113] {
|
||||
@ -83,7 +83,7 @@ Bridge adorne:hub:home "Adorne Hub" [host="192.160.1.111", port=2113] {
|
||||
}
|
||||
```
|
||||
|
||||
## demo.items
|
||||
## `demo.items` Example
|
||||
|
||||
```java
|
||||
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
|
||||
@ -93,7 +93,7 @@ Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
|
||||
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}
|
||||
```
|
||||
|
||||
## demo.sitemap
|
||||
## `demo.sitemap` Example
|
||||
|
||||
```perl
|
||||
sitemap demo label="Adorne Binding Demo"
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.adorne</artifactId>
|
||||
|
@ -55,7 +55,7 @@ The thing **aha Waste Collection Schedule** provides four channels for the upcom
|
||||
wasteCollection.things
|
||||
|
||||
```java
|
||||
Thing ahawastecollection:collectionSchedule:wasteCollectionSchedule "aha Abfuhrkalender" [ commune="Isernhagen", street="67269@Rosmarinweg+/+Kirchhorst@Kirchhorst", houseNumber="10", houseNumberAddon="", collectionPlace="67269-0010+" ]
|
||||
Thing ahawastecollection:collectionSchedule:wasteCollectionSchedule "aha Abfuhrkalender" [ commune="Isernhagen", street="67269@Rosmarinweg+/+Kirchhorst@Kirchhorst", houseNumber="10", houseNumberAddon="", collectionPlace="67269-0010+" ]
|
||||
```
|
||||
|
||||
wasteCollection.items
|
||||
@ -99,7 +99,6 @@ actions:
|
||||
var papierDate = items['collectionDay_paper'].getZonedDateTime();
|
||||
var restmuellDate = items['collectionDay_generalWaste'].getZonedDateTime();
|
||||
|
||||
|
||||
// Check which waste types are collected on the next day
|
||||
var biomuellCollection = biomuellDate.equals(tomorrow);
|
||||
var leichtverpackungCollection = leichtverpackungDate.equals(tomorrow);
|
||||
@ -125,7 +124,6 @@ actions:
|
||||
toBeCollected.push('general waste');
|
||||
}
|
||||
|
||||
|
||||
// Send message (or something else) if at least one waste type is collected
|
||||
if (toBeCollected.length > 0) {
|
||||
var message = "Tomorrow the following waste will be collected:\n" + toBeCollected.join(', ');
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.ahawastecollection</artifactId>
|
||||
|
@ -14,7 +14,6 @@ package org.openhab.binding.ahawastecollection.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
@ -26,7 +25,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.scheduler.CronScheduler;
|
||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||
@ -57,7 +55,6 @@ public class AhaWasteCollectionHandler extends BaseThingHandler {
|
||||
private final Lock monitor = new ReentrantLock();
|
||||
private final ExpiringCache<Map<WasteType, CollectionDate>> cache;
|
||||
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
private final Logger logger = LoggerFactory.getLogger(AhaWasteCollectionHandler.class);
|
||||
|
||||
private @Nullable AhaCollectionSchedule collectionSchedule;
|
||||
@ -69,11 +66,10 @@ public class AhaWasteCollectionHandler extends BaseThingHandler {
|
||||
private final ScheduledExecutorService executorService;
|
||||
|
||||
public AhaWasteCollectionHandler(final Thing thing, final CronScheduler scheduler,
|
||||
final TimeZoneProvider timeZoneProvider, final AhaCollectionScheduleFactory scheduleFactory,
|
||||
final AhaCollectionScheduleFactory scheduleFactory,
|
||||
@Nullable final ScheduledExecutorService executorService) {
|
||||
super(thing);
|
||||
this.cronScheduler = scheduler;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
this.scheduleFactory = scheduleFactory;
|
||||
this.cache = new ExpiringCache<>(Duration.ofMinutes(5), this::loadCollectionDates);
|
||||
this.executorService = executorService == null ? this.scheduler : executorService;
|
||||
@ -190,9 +186,7 @@ public class AhaWasteCollectionHandler extends BaseThingHandler {
|
||||
|
||||
final Date nextCollectionDate = collectionDate.getDates().get(0);
|
||||
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(nextCollectionDate.toInstant(),
|
||||
this.timeZoneProvider.getTimeZone());
|
||||
this.updateState(channel.getUID(), new DateTimeType(zonedDateTime));
|
||||
this.updateState(channel.getUID(), new DateTimeType(nextCollectionDate.toInstant()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,6 @@ import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.scheduler.CronScheduler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
@ -40,7 +39,6 @@ public class AhaWasteCollectionHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SCHEDULE);
|
||||
private final CronScheduler scheduler;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
|
||||
@ -48,10 +46,8 @@ public class AhaWasteCollectionHandlerFactory extends BaseThingHandlerFactory {
|
||||
}
|
||||
|
||||
@Activate
|
||||
public AhaWasteCollectionHandlerFactory(final @Reference CronScheduler scheduler,
|
||||
final @Reference TimeZoneProvider timeZoneProvider) {
|
||||
public AhaWasteCollectionHandlerFactory(final @Reference CronScheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -59,8 +55,7 @@ public class AhaWasteCollectionHandlerFactory extends BaseThingHandlerFactory {
|
||||
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_SCHEDULE.equals(thingTypeUID)) {
|
||||
return new AhaWasteCollectionHandler(thing, this.scheduler, this.timeZoneProvider,
|
||||
AhaCollectionScheduleImpl::new, null);
|
||||
return new AhaWasteCollectionHandler(thing, this.scheduler, AhaCollectionScheduleImpl::new, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -15,8 +15,6 @@ package org.openhab.binding.ahawastecollection.internal;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
@ -136,15 +134,14 @@ public class AhaWasteCollectionHandlerTest {
|
||||
}).when(executorStub).execute(any(Runnable.class));
|
||||
|
||||
final AhaWasteCollectionHandler handler = new AhaWasteCollectionHandler(thing, createStubScheduler(),
|
||||
ZoneId::systemDefault, new AhaCollectionScheduleStubFactory(), executorStub);
|
||||
new AhaCollectionScheduleStubFactory(), executorStub);
|
||||
handler.setCallback(callback);
|
||||
handler.initialize();
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static State getDateTime(final Date day) {
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault());
|
||||
return new DateTimeType(zonedDateTime);
|
||||
return new DateTimeType(day.toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
119
bundles/org.openhab.binding.airgradient/README.md
Normal file
119
bundles/org.openhab.binding.airgradient/README.md
Normal file
@ -0,0 +1,119 @@
|
||||
# AirGradient Binding
|
||||
|
||||
AirGradient provides open source and open hardware air quality monitors.
|
||||
|
||||
This binding reads air quality data from the AirGradient (https://www.airgradient.com/) API.
|
||||
|
||||
This API is documented at https://api.airgradient.com/public/docs/api/v1/
|
||||
|
||||
## Supported Things
|
||||
|
||||
![AirGradient sensors](doc/airgradient_sensors.png)
|
||||
|
||||
This binding supports all the different AirGradient sensors, providing most of the sensor data.
|
||||
|
||||
- `bridge`: Connection to the API
|
||||
- `location`: Location in the API to read values for
|
||||
|
||||
## Discovery
|
||||
|
||||
Autodiscovery of locations is implemented.
|
||||
Start by adding an AirGradient API thing.
|
||||
When that is added and online, run a scan for new things in the AirGradient binding.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
This binding supports reading data both directly from AirGradient sensors and from the AirGradient API.
|
||||
If you don't specify any path on the server, the binding will behave as if the hostname is the hostname of the AirGradient API server, and append paths and tokens for it.
|
||||
|
||||
The binding will adapt to the content type of the returned content to support different formats for getting data both from local and cloud installations.
|
||||
|
||||
| Name | Hostname | Content-Type | Parser |
|
||||
|-------------------|-----------------------------------------------------------------|------------------------------|--------|
|
||||
| API | Hostnames without any path (e.g., https://api.airgradient.com/) | application/json | JSON parser for the AirGradient API, correct paths will be appended to the calls |
|
||||
| Local OpenMetrics | Hostnames with path (e.g., http://192.168.x.x/metrics) | application/openmetrics-text | OpenMetrics parser |
|
||||
| Local Web | Hostnames with path (e.g., http://192.168.x.x/measures/current) | application/json | JSON parser for the AirGradient API, as if you returned the value of sendToServer() payload |
|
||||
| Local Prometheus | Hostnames with path (e.g., http://192.168.x.x/measures) | text/plain | Prometheus parser for [Prometheus format](https://prometheus.io/docs/instrumenting/exposition_formats/) |
|
||||
|
||||
### AirGradient API
|
||||
|
||||
The connection to the API needs setup and configuration
|
||||
|
||||
1. Log in to the AirGradient Dashboard: https://app.airgradient.com/dashboard
|
||||
2. Navigate to Place->Connectivity Settings from the upper left hamburger menu.
|
||||
3. Enable API access, and take a copy of the Token, which will be used in the token setting to configure the connection to the API.
|
||||
|
||||
To add a location, you need to know the location ID. To get the location ID, you
|
||||
|
||||
1. Log in to the AirGradient Dashboard: https://app.airgradient.com/dashboard
|
||||
2. Navigate to Locations from the upper left hamburger menu.
|
||||
3. Here you will find a list of all of your sensors, with a location ID in the left column. Use that id when you add new Location things.
|
||||
|
||||
### `API` Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------|---------|---------------------------------------|------------------------------|----------|----------|
|
||||
| token | text | Token to access the device | N/A | yes | no |
|
||||
| hostname | text | Hostname or IP address of the API | https://api.airgradient.com/ | no | yes |
|
||||
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
|
||||
|
||||
### `Location` Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------|---------|-------------------------------------------------------------------|---------|----------|----------|
|
||||
| location | text | A number identifying the location id in the AirGradient Dashboard | N/A | yes | no |
|
||||
|
||||
## Channels
|
||||
|
||||
For more information about the data in the channels, please refer to the models in https://api.airgradient.com/public/docs/api/v1/
|
||||
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|--------------------|----------------------|------------|----------------------------------------------------------------------------------|
|
||||
| pm01 | Number:Density | Read | Particulate Matter 1 (0.001mm) |
|
||||
| pm02 | Number:Density | Read | Particulate Matter 2 (0.002mm) |
|
||||
| pm10 | Number:Density | Read | Particulate Matter 10 (0.01mm) |
|
||||
| pm003-count | Number:Dimensionless | Read | The number of particles with a diameter beyond 0.3 microns in 1 deciliter of air |
|
||||
| rco2 | Number:Density | Read | Carbon dioxide PPM |
|
||||
| tvoc | Number:Density | Read | Total Volatile Organic Compounds |
|
||||
| atmp | Number:Temperature | Read | Ambient Temperature |
|
||||
| rhum | Number:Dimensionless | Read | Relative Humidity Percentage |
|
||||
| wifi | Number:Power | Read | Received signal strength indicator |
|
||||
| uploads-since-boot | Number:Dimensionless | Read | Number of measure uploads since last reboot (boot) |
|
||||
| leds | String | Read/Write | Sets the leds mode (off/co2/pm) |
|
||||
| calibration | String | Write | Triggers co2 calibration on the device |
|
||||
|
||||
Some configuration channels are only available for local devices (for cloud devices use the AirGradient dashboard to configure these instead).
|
||||
These configuration settings needs AirGradient firmware on the sensor of version 3.1.1 or later.
|
||||
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|-----------------------|----------------------|------------|----------------------------------------------------------------------------------|
|
||||
| country-code | String | Read/Write | The ALPHA-2 Country code used for the device |
|
||||
| pm-standard | String | Read/Write | Standard used for Parts per Million measurements (us-aqi or ugm3) |
|
||||
| abc-days | Number:Days | Read/Write | Co2 calibration automatic baseline calibration days |
|
||||
| tvoc-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
|
||||
| nox-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
|
||||
| mqtt-broker-url | String | Read/Write | MQTT Broker URL |
|
||||
| temperature-unit | String | Read/Write | Temperature unit used on the display |
|
||||
| configuration-control | String | Read/Write | Where the unit is configured from (local/cloud/both) |
|
||||
| post-to-cloud | Switch | Read/Write | Send data to the AirGradient cloud |
|
||||
| led-bar-brightness | Number:Dimensionless | Read/Write | Brightness of the LED bar |
|
||||
| display-brightness | Number:Dimensionless | Read/Write | Brightness of the display |
|
||||
| model | String | Read/Write | The model of the device (can be changed e.g. if you change sensors) |
|
||||
| led-bar-test | String | Write | Trigger test of LED bar |
|
||||
|
||||
## Full Example
|
||||
|
||||
### Thing Configuration
|
||||
|
||||
```java
|
||||
Bridge airgradient:airgradient-api:home "My Home" [ token="abc123...." ] {
|
||||
Thing location "654321" "Outside" [ location="654321" ]
|
||||
}
|
||||
```
|
||||
|
||||
### Item Configuration
|
||||
|
||||
```java
|
||||
Number:Density AirGradient_Location_PM2 "%.0f kg/m³" <density> {channel="airgradient:location:654321:pm2"}"
|
||||
Number:Temperature AirGradient_Location_PM2 "Temperature [%.1f °C]" <temperature> {channel="airgradient:location:654321:atmp"}"
|
||||
```
|
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
@ -7,11 +7,11 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.electroluxair</artifactId>
|
||||
<artifactId>org.openhab.binding.airgradient</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: ElectroluxAir Binding</name>
|
||||
<name>openHAB Add-ons :: Bundles :: AirGradient Binding</name>
|
||||
|
||||
</project>
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.meteoalerte-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<features name="org.openhab.binding.airgradient-${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-meteoalerte" description="Meteo Alerte Binding" version="${project.version}">
|
||||
<feature name="openhab-binding-airgradient" description="AirGradient Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.meteoalerte/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airgradient/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "airgradient";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "airgradient-api");
|
||||
public static final ThingTypeUID THING_TYPE_LOCAL = new ThingTypeUID(BINDING_ID, "airgradient-local");
|
||||
public static final ThingTypeUID THING_TYPE_LOCATION = new ThingTypeUID(BINDING_ID, "location");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_PM_01 = "pm01";
|
||||
public static final String CHANNEL_PM_02 = "pm02";
|
||||
public static final String CHANNEL_PM_10 = "pm10";
|
||||
public static final String CHANNEL_PM_003_COUNT = "pm003-count";
|
||||
public static final String CHANNEL_ATMP = "atmp";
|
||||
public static final String CHANNEL_RHUM = "rhum";
|
||||
public static final String CHANNEL_WIFI = "wifi";
|
||||
public static final String CHANNEL_RCO2 = "rco2";
|
||||
public static final String CHANNEL_TVOC = "tvoc";
|
||||
public static final String CHANNEL_LEDS_MODE = "leds";
|
||||
public static final String CHANNEL_CALIBRATION = "calibration";
|
||||
public static final String CHANNEL_UPLOADS_SINCE_BOOT = "uploads-since-boot";
|
||||
public static final String CHANNEL_COUNTRY_CODE = "country-code";
|
||||
public static final String CHANNEL_PM_STANDARD = "pm-standard";
|
||||
public static final String CHANNEL_ABC_DAYS = "abc-days";
|
||||
public static final String CHANNEL_TVOC_LEARNING_OFFSET = "tvoc-learning-offset";
|
||||
public static final String CHANNEL_NOX_LEARNING_OFFSET = "nox-learning-offset";
|
||||
public static final String CHANNEL_MQTT_BROKER_URL = "mqtt-broker-url";
|
||||
public static final String CHANNEL_TEMPERATURE_UNIT = "temperature-unit";
|
||||
public static final String CHANNEL_CONFIGURATION_CONTROL = "configuration-control";
|
||||
public static final String CHANNEL_POST_TO_CLOUD = "post-to-cloud";
|
||||
public static final String CHANNEL_LED_BAR_BRIGHTNESS = "led-bar-brightness";
|
||||
public static final String CHANNEL_DISPLAY_BRIGHTNESS = "display-brightness";
|
||||
public static final String CHANNEL_MODEL = "model";
|
||||
public static final String CHANNEL_LED_BAR_TEST = "led-bar-test";
|
||||
|
||||
// List of all properties
|
||||
public static final String PROPERTY_NAME = "name";
|
||||
|
||||
// All configurations
|
||||
public static final String CONFIG_LOCATION = "location";
|
||||
public static final String CONFIG_API_TOKEN = "token";
|
||||
public static final String CONFIG_API_HOST_NAME = "hostname";
|
||||
public static final String CONFIG_API_REFRESH_INTERVAL = "refreshInterval";
|
||||
|
||||
// URLs for API
|
||||
public static final String CURRENT_MEASURES_PATH = "/public/api/v1/locations/measures/current?token=%s";
|
||||
public static final String CURRENT_MEASURES_LOCAL_PATH = "/measures/current";
|
||||
public static final String LOCAL_CONFIG_PATH = "/config";
|
||||
public static final String LEDS_MODE_PATH = "/public/api/v1/sensors/%s/config/leds/mode?token=%s";
|
||||
public static final String CALIBRATE_CO2_PATH = "/public/api/v1/sensors/%s/co2/calibration?token=%s";
|
||||
|
||||
// Discovery
|
||||
public static final Duration SEARCH_TIME = Duration.ofSeconds(15);
|
||||
public static final boolean BACKGROUND_DISCOVERY = true;
|
||||
public static final Duration DEFAULT_POLL_INTERVAL_LOCAL = Duration.ofSeconds(10);
|
||||
|
||||
// Media types
|
||||
public static final String CONTENTTYPE_JSON = "application/json";
|
||||
public static final String CONTENTTYPE_TEXT = "text/plain";
|
||||
public static final String CONTENTTYPE_OPENMETRICS = "application/openmetrics-text";
|
||||
|
||||
// Communication
|
||||
public static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.airgradient.internal.handler.AirGradientAPIHandler;
|
||||
import org.openhab.binding.airgradient.internal.handler.AirGradientLocalHandler;
|
||||
import org.openhab.binding.airgradient.internal.handler.AirGradientLocationHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.airgradient", service = ThingHandlerFactory.class)
|
||||
public class AirGradientHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_API, THING_TYPE_LOCATION,
|
||||
THING_TYPE_LOCAL);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientHandlerFactory.class);
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Activate
|
||||
public AirGradientHandlerFactory(final @Reference HttpClientFactory factory) {
|
||||
logger.debug("Activating factory for: {}", SUPPORTED_THING_TYPES_UIDS);
|
||||
this.httpClient = factory.getCommonHttpClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
logger.debug("We support: {}", SUPPORTED_THING_TYPES_UIDS);
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_API.equals(thingTypeUID)) {
|
||||
logger.debug("Creating Bridge Handler for {}", thingTypeUID);
|
||||
return new AirGradientAPIHandler((Bridge) thing, httpClient);
|
||||
}
|
||||
|
||||
if (THING_TYPE_LOCATION.equals(thingTypeUID)) {
|
||||
logger.debug("Creating Location Handler for {}", thingTypeUID);
|
||||
return new AirGradientLocationHandler(thing);
|
||||
}
|
||||
|
||||
if (THING_TYPE_LOCAL.equals(thingTypeUID)) {
|
||||
logger.debug("Creating Local Handler for {}", thingTypeUID);
|
||||
return new AirGradientLocalHandler(thing, httpClient);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.communication;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception for communication errors against AirGradient API or sensors.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientCommunicationException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AirGradientCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AirGradientCommunicationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.communication;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Helper for parsing JSON.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonParserHelper {
|
||||
|
||||
public static List<Measure> parseJson(Gson gson, String stringResponse) {
|
||||
List<@Nullable Measure> measures = null;
|
||||
if (stringResponse.startsWith("[")) {
|
||||
// Array of measures, like returned from the AirGradients API
|
||||
Type measuresType = new TypeToken<List<@Nullable Measure>>() {
|
||||
}.getType();
|
||||
measures = gson.fromJson(stringResponse, measuresType);
|
||||
} else if (stringResponse.startsWith("{")) {
|
||||
// Single measure e.g. if you read directly from the device
|
||||
Type measureType = new TypeToken<Measure>() {
|
||||
}.getType();
|
||||
Measure measure = gson.fromJson(stringResponse, measureType);
|
||||
measures = new ArrayList<>(1);
|
||||
measures.add(measure);
|
||||
}
|
||||
|
||||
if (measures != null) {
|
||||
List<@Nullable Measure> nullableMeasuresWithoutNulls = measures.stream().filter(Objects::nonNull).toList();
|
||||
List<Measure> measuresWithoutNulls = new ArrayList<>(nullableMeasuresWithoutNulls.size());
|
||||
for (@Nullable
|
||||
Measure m : nullableMeasuresWithoutNulls) {
|
||||
if (m != null) {
|
||||
measuresWithoutNulls.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
return measuresWithoutNulls;
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.communication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.openhab.binding.airgradient.internal.prometheus.PrometheusMetric;
|
||||
import org.openhab.binding.airgradient.internal.prometheus.PrometheusTextParser;
|
||||
|
||||
/**
|
||||
* Helper for parsing Prometheus data.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PrometheusParserHelper {
|
||||
|
||||
public static List<Measure> parsePrometheus(String stringResponse) {
|
||||
List<PrometheusMetric> metrics = PrometheusTextParser.parse(stringResponse);
|
||||
Measure measure = new Measure();
|
||||
|
||||
for (PrometheusMetric metric : metrics) {
|
||||
if (metric.getMetricName().equals("pm01")) {
|
||||
measure.pm01 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("pm02")) {
|
||||
measure.pm02 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("pm10")) {
|
||||
measure.pm10 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("rco2")) {
|
||||
measure.rco2 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("atmp")) {
|
||||
measure.atmp = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("rhum")) {
|
||||
measure.rhum = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("tvoc")) {
|
||||
measure.tvoc = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("nox")) {
|
||||
measure.noxIndex = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_wifi_rssi_dbm")) {
|
||||
measure.wifi = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_co2_ppm")) {
|
||||
measure.rco2 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_pm1_ugm3")) {
|
||||
measure.pm01 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_pm2d5_ugm3")) {
|
||||
measure.pm02 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_pm10_ugm3")) {
|
||||
measure.pm10 = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_pm0d3_p100ml")) {
|
||||
measure.pm003Count = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_tvoc_index")) {
|
||||
measure.tvoc = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_tvoc_raw_index")) {
|
||||
measure.tvocIndex = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_nox_index")) {
|
||||
measure.noxIndex = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_temperature_degc")) {
|
||||
measure.atmp = metric.getValue();
|
||||
} else if (metric.getMetricName().equals("airgradient_humidity_percent")) {
|
||||
measure.rhum = metric.getValue();
|
||||
}
|
||||
|
||||
if (metric.getLabels().containsKey("id")) {
|
||||
String id = metric.getLabels().get("id");
|
||||
measure.serialno = id;
|
||||
measure.locationId = id;
|
||||
measure.locationName = id;
|
||||
}
|
||||
|
||||
if (metric.getLabels().containsKey("airgradient_serial_number")) {
|
||||
String id = metric.getLabels().get("airgradient_serial_number");
|
||||
measure.serialno = id;
|
||||
measure.locationId = id;
|
||||
measure.locationName = id;
|
||||
}
|
||||
}
|
||||
|
||||
return Arrays.asList(measure);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.communication;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CALIBRATE_CO2_PATH;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CURRENT_MEASURES_PATH;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.LEDS_MODE_PATH;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.LOCAL_CONFIG_PATH;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.REQUEST_TIMEOUT;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
|
||||
/**
|
||||
* Helper for doing rest calls to the API.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RESTHelper {
|
||||
|
||||
public static @Nullable String generateMeasuresUrl(AirGradientAPIConfiguration apiConfig) {
|
||||
if (apiConfig.hasCloudUrl()) {
|
||||
return apiConfig.hostname + String.format(CURRENT_MEASURES_PATH, apiConfig.token);
|
||||
} else {
|
||||
return apiConfig.hostname;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String generateConfigUrl(AirGradientAPIConfiguration apiConfig) {
|
||||
URI uri = URI.create(apiConfig.hostname);
|
||||
URI configUri = uri.resolve(LOCAL_CONFIG_PATH);
|
||||
return configUri.toString();
|
||||
}
|
||||
|
||||
public static @Nullable String generateCalibrationCo2Url(AirGradientAPIConfiguration apiConfig, String serialNo) {
|
||||
if (apiConfig.hasCloudUrl()) {
|
||||
return apiConfig.hostname + String.format(CALIBRATE_CO2_PATH, serialNo, apiConfig.token);
|
||||
} else {
|
||||
return generateConfigUrl(apiConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String generateGetLedsModeUrl(AirGradientAPIConfiguration apiConfig, String serialNo) {
|
||||
if (apiConfig.hasCloudUrl()) {
|
||||
return apiConfig.hostname + String.format(LEDS_MODE_PATH, serialNo, apiConfig.token);
|
||||
} else {
|
||||
return generateConfigUrl(apiConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Request generateRequest(HttpClient httpClient, @Nullable String url) {
|
||||
return generateRequest(httpClient, url, HttpMethod.GET);
|
||||
}
|
||||
|
||||
public static @Nullable Request generateRequest(HttpClient httpClient, @Nullable String url, HttpMethod method) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Request request = httpClient.newRequest(url);
|
||||
request.timeout(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
|
||||
request.method(method);
|
||||
return request;
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.communication;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONTENTTYPE_JSON;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONTENTTYPE_OPENMETRICS;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONTENTTYPE_TEXT;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.REQUEST_TIMEOUT;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.model.LedMode;
|
||||
import org.openhab.binding.airgradient.internal.model.LocalConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Helper for doing rest calls to the AirGradient API.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoteAPIController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RemoteAPIController.class);
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final AirGradientAPIConfiguration apiConfig;
|
||||
|
||||
public RemoteAPIController(HttpClient httpClient, Gson gson, AirGradientAPIConfiguration apiConfig) {
|
||||
this.httpClient = httpClient;
|
||||
this.gson = gson;
|
||||
this.apiConfig = apiConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of measures from AirGradient API.
|
||||
*
|
||||
* @return list of measures
|
||||
* @throws AirGradientCommunicationException if unable to communicate with sensor or API.
|
||||
*/
|
||||
public List<Measure> getMeasures() throws AirGradientCommunicationException {
|
||||
ContentResponse response = sendRequest(
|
||||
RESTHelper.generateRequest(httpClient, RESTHelper.generateMeasuresUrl(apiConfig)));
|
||||
if (response != null) {
|
||||
String contentType = response.getMediaType();
|
||||
logger.trace("Got measurements with status {}: {} ({})", response.getStatus(),
|
||||
response.getContentAsString(), contentType);
|
||||
|
||||
if (HttpStatus.isSuccess(response.getStatus())) {
|
||||
String stringResponse = response.getContentAsString().trim();
|
||||
|
||||
if (null != contentType) {
|
||||
switch (contentType) {
|
||||
case CONTENTTYPE_JSON:
|
||||
return JsonParserHelper.parseJson(gson, stringResponse);
|
||||
case CONTENTTYPE_TEXT:
|
||||
return PrometheusParserHelper.parsePrometheus(stringResponse);
|
||||
case CONTENTTYPE_OPENMETRICS:
|
||||
return PrometheusParserHelper.parsePrometheus(stringResponse);
|
||||
default:
|
||||
logger.debug("Unhandled content type returned: {}", contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public @Nullable LocalConfiguration getConfig() throws AirGradientCommunicationException {
|
||||
ContentResponse response = sendRequest(
|
||||
RESTHelper.generateRequest(httpClient, RESTHelper.generateConfigUrl(apiConfig)));
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.trace("Got configuration with status {}: {}", response.getStatus(), response.getContentAsString());
|
||||
|
||||
Type configType = new TypeToken<LocalConfiguration>() {
|
||||
}.getType();
|
||||
return gson.fromJson(response.getContentAsString(), configType);
|
||||
}
|
||||
|
||||
public void setConfig(LocalConfiguration config) throws AirGradientCommunicationException {
|
||||
Request request = httpClient.newRequest(RESTHelper.generateConfigUrl(apiConfig));
|
||||
request.timeout(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
|
||||
request.method(HttpMethod.PUT);
|
||||
request.header(HttpHeader.CONTENT_TYPE, CONTENTTYPE_JSON);
|
||||
String configJson = gson.toJson(config);
|
||||
logger.debug("Setting configuration: {}", configJson);
|
||||
request.content(new StringContentProvider(CONTENTTYPE_JSON, configJson, StandardCharsets.UTF_8));
|
||||
sendRequest(request);
|
||||
}
|
||||
|
||||
public void setLedMode(String serialNo, String mode) throws AirGradientCommunicationException {
|
||||
Request request = httpClient.newRequest(RESTHelper.generateGetLedsModeUrl(apiConfig, serialNo));
|
||||
request.timeout(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
|
||||
request.method(HttpMethod.PUT);
|
||||
request.header(HttpHeader.CONTENT_TYPE, CONTENTTYPE_JSON);
|
||||
LedMode ledMode = new LedMode();
|
||||
ledMode.mode = mode;
|
||||
String modeJson = gson.toJson(ledMode);
|
||||
logger.debug("Setting LEDS mode for {}: {}", serialNo, modeJson);
|
||||
request.content(new StringContentProvider(CONTENTTYPE_JSON, modeJson, StandardCharsets.UTF_8));
|
||||
sendRequest(request);
|
||||
}
|
||||
|
||||
public void calibrateCo2(String serialNo) throws AirGradientCommunicationException {
|
||||
logger.debug("Triggering CO2 calibration for {}", serialNo);
|
||||
sendRequest(RESTHelper.generateRequest(httpClient, RESTHelper.generateCalibrationCo2Url(apiConfig, serialNo),
|
||||
HttpMethod.POST));
|
||||
}
|
||||
|
||||
private @Nullable ContentResponse sendRequest(@Nullable final Request request)
|
||||
throws AirGradientCommunicationException {
|
||||
if (request == null) {
|
||||
throw new AirGradientCommunicationException("Unable to generate request");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ContentResponse response = null;
|
||||
try {
|
||||
response = request.send();
|
||||
if (response != null) {
|
||||
logger.trace("Response from {} ({}): {}", request.getURI(), response.getStatus(),
|
||||
response.getContentAsString());
|
||||
if (!HttpStatus.isSuccess(response.getStatus())) {
|
||||
throw new AirGradientCommunicationException("Returned status code: " + response.getStatus());
|
||||
}
|
||||
} else {
|
||||
throw new AirGradientCommunicationException("No response");
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
String message = e.getMessage();
|
||||
if (message == null) {
|
||||
message = "Communication error";
|
||||
}
|
||||
throw new AirGradientCommunicationException(message, e);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.config;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientAPIConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientAPIConfiguration {
|
||||
|
||||
public String hostname = "";
|
||||
public String token = "";
|
||||
public int refreshInterval = 600;
|
||||
|
||||
public boolean isValid() {
|
||||
// hostname must be entered and be a URI
|
||||
if ("".equals(hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
URI.create(hostname);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// token is optional
|
||||
|
||||
// refresh interval is positive integer
|
||||
return (refreshInterval > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a URL against the cloud.
|
||||
*
|
||||
* @return true if this is a URL against the cloud API
|
||||
*/
|
||||
public boolean hasCloudUrl() {
|
||||
URI url = URI.create(hostname);
|
||||
return url.getPath().equals("/");
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientLocationConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientLocationConfiguration {
|
||||
|
||||
public String location = "";
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.BACKGROUND_DISCOVERY;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONFIG_LOCATION;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.PROPERTY_NAME;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.SEARCH_TIME;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.THING_TYPE_LOCATION;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
|
||||
import org.openhab.binding.airgradient.internal.handler.AirGradientAPIHandler;
|
||||
import org.openhab.binding.airgradient.internal.handler.PollEventListener;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientLocationDiscoveryService} is responsible for discovering new locations
|
||||
* that are not bound to any items.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = AirGradientLocationDiscoveryService.class)
|
||||
@NonNullByDefault
|
||||
public class AirGradientLocationDiscoveryService extends AbstractThingHandlerDiscoveryService<AirGradientAPIHandler>
|
||||
implements PollEventListener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientLocationDiscoveryService.class);
|
||||
|
||||
public AirGradientLocationDiscoveryService() {
|
||||
super(AirGradientAPIHandler.class, Set.of(THING_TYPE_LOCATION), (int) SEARCH_TIME.getSeconds(),
|
||||
BACKGROUND_DISCOVERY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Start AirGradient background discovery");
|
||||
getApiHandler().addPollEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping AirGradient background discovery");
|
||||
getApiHandler().removePollEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pollEvent(List<Measure> measures) {
|
||||
BridgeHandler bridge = getApiHandler().getThing().getHandler();
|
||||
if (bridge == null) {
|
||||
logger.debug("Missing bridge, can't discover sensors for unknown bridge.");
|
||||
return;
|
||||
}
|
||||
|
||||
ThingUID bridgeUid = bridge.getThing().getUID();
|
||||
|
||||
Set<String> registeredLocationIds = new HashSet<>(getApiHandler().getRegisteredLocationIds());
|
||||
for (Measure measure : measures) {
|
||||
String id = measure.getLocationId();
|
||||
if (id.isEmpty()) {
|
||||
// Local devices don't have location ID.
|
||||
id = measure.getSerialNo();
|
||||
}
|
||||
|
||||
String name = measure.getLocationName();
|
||||
if (name.isEmpty()) {
|
||||
name = "Sensor_" + measure.getSerialNo();
|
||||
}
|
||||
|
||||
if (!registeredLocationIds.contains(id)) {
|
||||
Map<String, Object> properties = new HashMap<>(5);
|
||||
properties.put(PROPERTY_NAME, name);
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, measure.getFirmwareVersion());
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, measure.getSerialNo());
|
||||
String model = measure.getModel();
|
||||
if (model != null) {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
}
|
||||
properties.put(CONFIG_LOCATION, id);
|
||||
|
||||
ThingUID thingUID = new ThingUID(THING_TYPE_LOCATION, bridgeUid, id);
|
||||
|
||||
logger.debug("Adding location {} with id {} to bridge {} with location id {}", name, thingUID,
|
||||
bridgeUid, measure.getLocationId());
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withBridge(bridgeUid).withLabel(name).withRepresentationProperty(CONFIG_LOCATION).build();
|
||||
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
try {
|
||||
List<Measure> measures = getApiHandler().getApiController().getMeasures();
|
||||
pollEvent(measures);
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
logger.warn("Failed discovery due to communication exception: {}", agce.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private AirGradientAPIHandler getApiHandler() {
|
||||
return (@NonNull AirGradientAPIHandler) getThingHandler();
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONFIG_API_HOST_NAME;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONFIG_API_REFRESH_INTERVAL;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONFIG_API_TOKEN;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CURRENT_MEASURES_LOCAL_PATH;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.DEFAULT_POLL_INTERVAL_LOCAL;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.THING_TYPE_LOCAL;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientMDNSDiscoveryParticipant} is responsible for discovering new and removed AirGradient sensors.
|
||||
* It uses the
|
||||
* central {@link org.openhab.core.config.discovery.mdns.internal.MDNSDiscoveryService}.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@Component(configurationPid = "discovery.airgradient")
|
||||
@NonNullByDefault
|
||||
public class AirGradientMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private static final String SERVICE_TYPE = "_airgradient._tcp.local.";
|
||||
private static final String MDNS_PROPERTY_SERIALNO = "serialno";
|
||||
private static final String MDNS_PROPERTY_MODEL = "model";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientMDNSDiscoveryParticipant.class);
|
||||
protected final ThingRegistry thingRegistry;
|
||||
|
||||
@Activate
|
||||
public AirGradientMDNSDiscoveryParticipant(final @Reference ThingRegistry thingRegistry) {
|
||||
this.thingRegistry = thingRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Set.of(THING_TYPE_LOCAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return SERVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(ServiceInfo si) {
|
||||
logger.debug("Discovered {} at {}: {}", si.getQualifiedName(), si.getURLs(), si.getNiceTextString());
|
||||
|
||||
String urls[] = si.getURLs();
|
||||
if (urls == null || urls.length < 1) {
|
||||
logger.debug("Not able to find URLs for {}, not autodetecting", si.getQualifiedName());
|
||||
return null;
|
||||
}
|
||||
|
||||
String hostName = urls[0] + CURRENT_MEASURES_LOCAL_PATH;
|
||||
String model = si.getPropertyString(MDNS_PROPERTY_MODEL);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>(4);
|
||||
properties.put(CONFIG_API_TOKEN, "");
|
||||
properties.put(CONFIG_API_HOST_NAME, hostName);
|
||||
properties.put(CONFIG_API_REFRESH_INTERVAL, DEFAULT_POLL_INTERVAL_LOCAL.getSeconds());
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
|
||||
ThingUID thingUID = getThingUID(si);
|
||||
if (thingUID == null) {
|
||||
logger.debug("Failed creating thing as we couldn't create a UID for it (missing serialno)");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Autodiscovered API {} with id {} with host name {}. It is a {}", si.getName(), thingUID, hostName,
|
||||
model);
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withLabel(si.getName()).withRepresentationProperty(CONFIG_API_HOST_NAME).build();
|
||||
|
||||
return discoveryResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(ServiceInfo si) {
|
||||
logger.debug("Getting thing ID for: App: {} Host: {} Name: {} Port: {} Serial: {}", si.getApplication(),
|
||||
si.getHostAddresses(), si.getName(), si.getPort(), si.getPropertyString("serialno"));
|
||||
|
||||
String serialNo = si.getPropertyString(MDNS_PROPERTY_SERIALNO);
|
||||
if (serialNo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ThingUID(THING_TYPE_LOCAL, serialNo);
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
|
||||
import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.discovery.AirGradientLocationDiscoveryService;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientAPIHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientAPIHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientAPIHandler.class);
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final Set<PollEventListener> pollListeners = new HashSet<>(1);
|
||||
|
||||
private @NonNullByDefault({}) RemoteAPIController apiController = null;
|
||||
private @NonNullByDefault({}) AirGradientAPIConfiguration apiConfig = null;
|
||||
|
||||
public AirGradientAPIHandler(Bridge bridge, HttpClient httpClient) {
|
||||
super(bridge);
|
||||
this.httpClient = httpClient;
|
||||
this.gson = new Gson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
pollingCode();
|
||||
} else {
|
||||
// This is read only
|
||||
logger.warn("Received command {} for channel {}, but the API is read only", command.toString(),
|
||||
channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
apiConfig = getConfigAs(AirGradientAPIConfiguration.class);
|
||||
if (!apiConfig.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Need to set hostname to a valid URL. Refresh interval needs to be a positive integer.");
|
||||
return;
|
||||
}
|
||||
|
||||
apiController = new RemoteAPIController(httpClient, gson, apiConfig);
|
||||
|
||||
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
|
||||
// the framework is then able to reuse the resources from the thing handler initialization.
|
||||
// we set this upfront to reliably check status updates in unit tests.
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, apiConfig.refreshInterval,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static String getMeasureId(Measure measure) {
|
||||
String id = measure.getLocationId();
|
||||
if (id.isEmpty()) {
|
||||
// Local devices don't have location ID.
|
||||
id = measure.getSerialNo();
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
protected void pollingCode() {
|
||||
try {
|
||||
List<Measure> measures = apiController.getMeasures();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
triggerPollEvent(measures);
|
||||
|
||||
Map<String, Measure> measureMap = measures.stream()
|
||||
.collect(Collectors.toMap((m) -> getMeasureId(m), (m) -> m));
|
||||
|
||||
for (Thing t : getThing().getThings()) {
|
||||
if (t.getHandler() instanceof AirGradientLocationHandler handler) {
|
||||
String locationId = handler.getLocationId();
|
||||
@Nullable
|
||||
Measure measure = measureMap.get(locationId);
|
||||
if (measure != null) {
|
||||
handler.setMeasurment(measure);
|
||||
} else {
|
||||
logger.debug("Could not find measures for location {}", locationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return location ids we already have things for.
|
||||
*
|
||||
* @return location ids we already have things for.
|
||||
*/
|
||||
public List<String> getRegisteredLocationIds() {
|
||||
List<Thing> things = getThing().getThings();
|
||||
List<String> results = new ArrayList<>(things.size());
|
||||
for (Thing t : things) {
|
||||
if (t.getHandler() instanceof AirGradientLocationHandler handler) {
|
||||
results.add(handler.getLocationId());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> pollingJob = this.pollingJob;
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
this.pollingJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void setConfiguration(AirGradientAPIConfiguration config) {
|
||||
this.apiConfig = config;
|
||||
}
|
||||
|
||||
protected void setApiController(RemoteAPIController apiController) {
|
||||
this.apiController = apiController;
|
||||
}
|
||||
|
||||
public RemoteAPIController getApiController() {
|
||||
return apiController;
|
||||
}
|
||||
|
||||
// Event listening
|
||||
|
||||
public void addPollEventListener(PollEventListener listener) {
|
||||
pollListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removePollEventListener(PollEventListener listener) {
|
||||
pollListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void triggerPollEvent(List<Measure> measures) {
|
||||
for (PollEventListener listener : pollListeners) {
|
||||
listener.pollEvent(measures);
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(AirGradientLocationDiscoveryService.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
|
||||
import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.model.LocalConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
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.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientAPIHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientLocalHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientLocalHandler.class);
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
private @NonNullByDefault({}) RemoteAPIController apiController = null;
|
||||
private @NonNullByDefault({}) AirGradientAPIConfiguration apiConfig = null;
|
||||
|
||||
public AirGradientLocalHandler(Thing thing, HttpClient httpClient) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
this.gson = new Gson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Channel {}: {}", channelUID, command.toFullString());
|
||||
if (command instanceof RefreshType) {
|
||||
pollingCode();
|
||||
} else if (CHANNEL_LEDS_MODE.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.ledBarMode = stringCommand.toFullString());
|
||||
} else {
|
||||
logger.warn("Received command {} for channel {}, but it needs a string command", command.toString(),
|
||||
channelUID.getId());
|
||||
}
|
||||
} else if (CHANNEL_CALIBRATION.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
if ("co2".equals(stringCommand.toFullString())) {
|
||||
updateConfiguration((var c) -> c.co2CalibrationRequested = true);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Received unknown command {} for calibration on channel {}, which we don't know how to handle",
|
||||
command.toString(), channelUID.getId());
|
||||
}
|
||||
}
|
||||
} else if (CHANNEL_TEMPERATURE_UNIT.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.temperatureUnit = stringCommand.toFullString());
|
||||
}
|
||||
} else if (CHANNEL_PM_STANDARD.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.pmStandard = stringCommand.toFullString());
|
||||
}
|
||||
} else if (CHANNEL_ABC_DAYS.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateConfiguration((var c) -> c.abcDays = quantityCommand.longValue());
|
||||
}
|
||||
} else if (CHANNEL_TVOC_LEARNING_OFFSET.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateConfiguration((var c) -> c.tvocLearningOffset = quantityCommand.longValue());
|
||||
}
|
||||
} else if (CHANNEL_NOX_LEARNING_OFFSET.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateConfiguration((var c) -> c.noxLearningOffset = quantityCommand.longValue());
|
||||
}
|
||||
} else if (CHANNEL_MQTT_BROKER_URL.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.mqttBrokerUrl = stringCommand.toFullString());
|
||||
}
|
||||
} else if (CHANNEL_CONFIGURATION_CONTROL.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.configurationControl = stringCommand.toFullString());
|
||||
}
|
||||
} else if (CHANNEL_LED_BAR_BRIGHTNESS.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateConfiguration((var c) -> c.ledBarBrightness = quantityCommand.longValue());
|
||||
}
|
||||
} else if (CHANNEL_DISPLAY_BRIGHTNESS.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateConfiguration((var c) -> c.displayBrightness = quantityCommand.longValue());
|
||||
}
|
||||
} else if (CHANNEL_POST_TO_CLOUD.equals(channelUID.getId())) {
|
||||
if (command instanceof OnOffType onOffCommand) {
|
||||
updateConfiguration((var c) -> c.postDataToAirGradient = onOffCommand.equals(OnOffType.ON));
|
||||
}
|
||||
} else if (CHANNEL_MODEL.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
updateConfiguration((var c) -> c.model = stringCommand.toFullString());
|
||||
}
|
||||
} else if (CHANNEL_LED_BAR_TEST.equals(channelUID.getId())) {
|
||||
updateConfiguration((var c) -> c.ledBarTestRequested = true);
|
||||
} else {
|
||||
// This is read only
|
||||
logger.warn("Received command {} for channel {}, which we don't know how to handle (type: {})",
|
||||
command.toString(), channelUID.getId(), command.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
apiConfig = getConfigAs(AirGradientAPIConfiguration.class);
|
||||
if (!apiConfig.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Need to set hostname to a valid URL. Refresh interval needs to be a positive integer.");
|
||||
return;
|
||||
}
|
||||
|
||||
apiController = new RemoteAPIController(httpClient, gson, apiConfig);
|
||||
|
||||
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
|
||||
// the framework is then able to reuse the resources from the thing handler initialization.
|
||||
// we set this upfront to reliably check status updates in unit tests.
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, apiConfig.refreshInterval,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void pollingCode() {
|
||||
try {
|
||||
List<Measure> measures = apiController.getMeasures();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
if (measures.size() != 1) {
|
||||
logger.warn("Expecting single set of measures for local device, but got {} measures", measures.size());
|
||||
return;
|
||||
}
|
||||
|
||||
Measure measure = measures.get(0);
|
||||
updateProperties(MeasureHelper.createProperties(measure));
|
||||
Map<String, State> states = MeasureHelper.createStates(measure);
|
||||
for (Map.Entry<String, State> entry : states.entrySet()) {
|
||||
if (isLinked(entry.getKey())) {
|
||||
updateState(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
LocalConfiguration localConfig = apiController.getConfig();
|
||||
if (localConfig != null) {
|
||||
// If we are able to read config, we add config channels
|
||||
ThingBuilder builder = DynamicChannelHelper.updateThingWithConfigurationChannels(thing, editThing());
|
||||
updateThing(builder.build());
|
||||
|
||||
updateProperties(ConfigurationHelper.createProperties(localConfig));
|
||||
Map<String, State> configStates = ConfigurationHelper.createStates(localConfig);
|
||||
for (Map.Entry<String, State> entry : configStates.entrySet()) {
|
||||
if (isLinked(entry.getKey())) {
|
||||
updateState(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConfiguration(Consumer<LocalConfiguration> action) {
|
||||
try {
|
||||
LocalConfiguration config = new LocalConfiguration();
|
||||
action.accept(config);
|
||||
apiController.setConfig(config);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> pollingJob = this.pollingJob;
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
this.pollingJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void setConfiguration(AirGradientAPIConfiguration config) {
|
||||
this.apiConfig = config;
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientLocationConfiguration;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
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.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirGradientAPIHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirGradientLocationHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirGradientLocationHandler.class);
|
||||
|
||||
private @NonNullByDefault({}) AirGradientLocationConfiguration locationConfig = null;
|
||||
|
||||
public AirGradientLocationHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Channel {}: {}", channelUID, command.toFullString());
|
||||
if (command instanceof RefreshType) {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
if (bridge.getHandler() instanceof AirGradientAPIHandler handler) {
|
||||
handler.pollingCode();
|
||||
}
|
||||
}
|
||||
} else if (CHANNEL_LEDS_MODE.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
setLedModeOnDevice(stringCommand.toFullString());
|
||||
} else {
|
||||
logger.warn("Received command {} for channel {}, but it needs a string command", command.toString(),
|
||||
channelUID.getId());
|
||||
}
|
||||
} else if (CHANNEL_CALIBRATION.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
if ("co2".equals(stringCommand.toFullString())) {
|
||||
calibrateCo2OnDevice();
|
||||
} else {
|
||||
logger.warn(
|
||||
"Received unknown command {} for calibration on channel {}, which we don't know how to handle",
|
||||
command.toString(), channelUID.getId());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is read only
|
||||
logger.warn("Received command {} for channel {}, which we don't know how to handle", command.toString(),
|
||||
channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void setLedModeOnDevice(String mode) {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
if (bridge.getHandler() instanceof AirGradientAPIHandler handler) {
|
||||
try {
|
||||
handler.getApiController().setLedMode(getSerialNo(), mode);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void calibrateCo2OnDevice() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
if (bridge.getHandler() instanceof AirGradientAPIHandler handler) {
|
||||
try {
|
||||
handler.getApiController().calibrateCo2(getSerialNo());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AirGradientCommunicationException agce) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
|
||||
// the framework is then able to reuse the resources from the thing handler initialization.
|
||||
// we set this upfront to reliably check status updates in unit tests.
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
locationConfig = getConfigAs(AirGradientLocationConfiguration.class);
|
||||
|
||||
Bridge controller = getBridge();
|
||||
if (controller == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
} else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
|
||||
public String getLocationId() {
|
||||
return locationConfig.location;
|
||||
}
|
||||
|
||||
public void setMeasurment(Measure measure) {
|
||||
updateProperties(MeasureHelper.createProperties(measure));
|
||||
Map<String, State> states = MeasureHelper.createStates(measure);
|
||||
for (Map.Entry<String, State> entry : states.entrySet()) {
|
||||
if (isLinked(entry.getKey())) {
|
||||
updateState(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serial number of this sensor.
|
||||
*
|
||||
* @return serial number of this sensor.
|
||||
*/
|
||||
private String getSerialNo() {
|
||||
String serialNo = thing.getProperties().get(Thing.PROPERTY_SERIAL_NUMBER);
|
||||
if (serialNo == null) {
|
||||
serialNo = "";
|
||||
}
|
||||
|
||||
return serialNo;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airgradient.internal.model.LocalConfiguration;
|
||||
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.Units;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Helper class to reduce code duplication across things.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigurationHelper {
|
||||
|
||||
public static Map<String, String> createProperties(LocalConfiguration configuration) {
|
||||
Map<String, String> properties = new HashMap<>(4);
|
||||
|
||||
String model = configuration.model;
|
||||
if (model != null) {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static final String CHANNEL_POST_TO_CLOUD = "post-to-cloud";
|
||||
|
||||
public static Map<String, State> createStates(LocalConfiguration configuration) {
|
||||
Map<String, State> states = new HashMap<>(11);
|
||||
|
||||
states.put(CHANNEL_COUNTRY_CODE, toStringType(configuration.country));
|
||||
states.put(CHANNEL_PM_STANDARD, toStringType(configuration.pmStandard));
|
||||
states.put(CHANNEL_ABC_DAYS, toQuantityType(configuration.abcDays, Units.DAY));
|
||||
states.put(CHANNEL_TVOC_LEARNING_OFFSET, toQuantityType(configuration.tvocLearningOffset, Units.ONE));
|
||||
states.put(CHANNEL_NOX_LEARNING_OFFSET, toQuantityType(configuration.noxLearningOffset, Units.ONE));
|
||||
states.put(CHANNEL_MQTT_BROKER_URL, toStringType(configuration.mqttBrokerUrl));
|
||||
states.put(CHANNEL_TEMPERATURE_UNIT, toStringType(configuration.temperatureUnit));
|
||||
states.put(CHANNEL_CONFIGURATION_CONTROL, toStringType(configuration.configurationControl));
|
||||
states.put(CHANNEL_LED_BAR_BRIGHTNESS, toQuantityType(configuration.ledBarBrightness, Units.ONE));
|
||||
states.put(CHANNEL_DISPLAY_BRIGHTNESS, toQuantityType(configuration.displayBrightness, Units.ONE));
|
||||
states.put(CHANNEL_POST_TO_CLOUD, toOnOffType(configuration.postDataToAirGradient));
|
||||
states.put(CHANNEL_MODEL, toStringType(configuration.model));
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private static State toQuantityType(@Nullable Number value, Unit<?> unit) {
|
||||
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
private static State toStringType(@Nullable String value) {
|
||||
return value == null ? UnDefType.NULL : StringType.valueOf(value);
|
||||
}
|
||||
|
||||
private static State toOnOffType(@Nullable Boolean value) {
|
||||
return value == null ? UnDefType.NULL : OnOffType.from(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link DynamicChannelHelper} is responsible for creating dynamic configuration channels.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DynamicChannelHelper {
|
||||
|
||||
private record ConfigurationChannel(String id, String typeId, String itemType) {
|
||||
}
|
||||
|
||||
private static final List<ConfigurationChannel> CHANNELS = new ArrayList<ConfigurationChannel>() {
|
||||
{
|
||||
add(new ConfigurationChannel(CHANNEL_COUNTRY_CODE, CHANNEL_COUNTRY_CODE, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_PM_STANDARD, CHANNEL_PM_STANDARD, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_ABC_DAYS, CHANNEL_ABC_DAYS, "Number"));
|
||||
add(new ConfigurationChannel(CHANNEL_TVOC_LEARNING_OFFSET, CHANNEL_TVOC_LEARNING_OFFSET, "Number"));
|
||||
add(new ConfigurationChannel(CHANNEL_NOX_LEARNING_OFFSET, CHANNEL_NOX_LEARNING_OFFSET, "Number"));
|
||||
add(new ConfigurationChannel(CHANNEL_MQTT_BROKER_URL, CHANNEL_MQTT_BROKER_URL, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_TEMPERATURE_UNIT, CHANNEL_TEMPERATURE_UNIT, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_CONFIGURATION_CONTROL, CHANNEL_CONFIGURATION_CONTROL, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_POST_TO_CLOUD, CHANNEL_POST_TO_CLOUD, "Switch"));
|
||||
add(new ConfigurationChannel(CHANNEL_LED_BAR_BRIGHTNESS, CHANNEL_LED_BAR_BRIGHTNESS,
|
||||
"Number:Dimensionless"));
|
||||
add(new ConfigurationChannel(CHANNEL_DISPLAY_BRIGHTNESS, CHANNEL_DISPLAY_BRIGHTNESS,
|
||||
"Number:Dimensionless"));
|
||||
add(new ConfigurationChannel(CHANNEL_MODEL, CHANNEL_MODEL, "String"));
|
||||
add(new ConfigurationChannel(CHANNEL_LED_BAR_TEST, CHANNEL_LED_BAR_TEST, "String"));
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicChannelHelper.class);
|
||||
|
||||
public static ThingBuilder updateThingWithConfigurationChannels(Thing thing, ThingBuilder builder) {
|
||||
for (ConfigurationChannel channel : CHANNELS) {
|
||||
addLocalConfigurationChannel(thing, builder, channel);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addLocalConfigurationChannel(Thing originalThing, ThingBuilder builder,
|
||||
ConfigurationChannel toAdd) {
|
||||
ChannelUID channelId = new ChannelUID(originalThing.getUID(), toAdd.id);
|
||||
if (originalThing.getChannel(channelId) == null) {
|
||||
LOGGER.debug("Adding dynamic channel {} to {}", toAdd.id, originalThing.getUID());
|
||||
ChannelTypeUID typeId = new ChannelTypeUID(BINDING_ID, toAdd.typeId);
|
||||
Channel channel = ChannelBuilder.create(channelId, toAdd.itemType).withType(typeId).build();
|
||||
builder.withChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Helper class to reduce code duplication across things.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeasureHelper {
|
||||
|
||||
public static Map<String, String> createProperties(Measure measure) {
|
||||
Map<String, String> properties = new HashMap<>(4);
|
||||
String firmwareVersion = measure.firmwareVersion;
|
||||
if (firmwareVersion != null) {
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
|
||||
}
|
||||
|
||||
String locationName = measure.locationName;
|
||||
if (locationName != null) {
|
||||
properties.put(PROPERTY_NAME, locationName);
|
||||
}
|
||||
|
||||
String serialNo = measure.serialno;
|
||||
if (serialNo != null) {
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNo);
|
||||
}
|
||||
|
||||
String model = measure.getModel();
|
||||
if (model != null) {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static Map<String, State> createStates(Measure measure) {
|
||||
Map<String, State> states = new HashMap<>(11);
|
||||
|
||||
states.put(CHANNEL_ATMP, toQuantityType(measure.getTemperature(), SIUnits.CELSIUS));
|
||||
states.put(CHANNEL_PM_003_COUNT, toQuantityType(measure.pm003Count, Units.ONE));
|
||||
states.put(CHANNEL_PM_01, toQuantityType(measure.pm01, Units.MICROGRAM_PER_CUBICMETRE));
|
||||
states.put(CHANNEL_PM_02, toQuantityType(measure.pm02, Units.MICROGRAM_PER_CUBICMETRE));
|
||||
states.put(CHANNEL_PM_10, toQuantityType(measure.pm10, Units.MICROGRAM_PER_CUBICMETRE));
|
||||
states.put(CHANNEL_RHUM, toQuantityType(measure.getHumidity(), Units.PERCENT));
|
||||
states.put(CHANNEL_UPLOADS_SINCE_BOOT, toQuantityType(measure.getBootCount(), Units.ONE));
|
||||
|
||||
Double rco2 = measure.rco2;
|
||||
if (rco2 != null) {
|
||||
states.put(CHANNEL_RCO2, toQuantityType(rco2.longValue(), Units.PARTS_PER_MILLION));
|
||||
}
|
||||
|
||||
Double tvoc = measure.tvoc;
|
||||
if (tvoc != null) {
|
||||
states.put(CHANNEL_TVOC, toQuantityType(tvoc.longValue(), Units.PARTS_PER_BILLION));
|
||||
}
|
||||
|
||||
states.put(CHANNEL_WIFI, toQuantityType(measure.wifi, Units.DECIBEL_MILLIWATTS));
|
||||
states.put(CHANNEL_LEDS_MODE, toStringType(measure.ledMode));
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private static State toQuantityType(@Nullable Number value, Unit<?> unit) {
|
||||
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
private static State toStringType(@Nullable String value) {
|
||||
return value == null ? UnDefType.NULL : StringType.valueOf(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
|
||||
/**
|
||||
* Interface for listening to polls.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface PollEventListener {
|
||||
|
||||
/**
|
||||
* Called when a poll has happened.
|
||||
*
|
||||
* @param measures Measures that has been read in a successful poll
|
||||
*/
|
||||
public void pollEvent(List<Measure> measures);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Data model class for a single led mode from AirGradients API.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LedMode {
|
||||
@Nullable
|
||||
public String mode;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Data model class for configuration from a local sensor.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocalConfiguration {
|
||||
|
||||
@Nullable
|
||||
public String country; // ALPHA-2 Country code
|
||||
|
||||
@Nullable
|
||||
public String pmStandard; // usaqi/ugm3
|
||||
|
||||
@Nullable
|
||||
public String ledBarMode; // off, pm, co2
|
||||
|
||||
@Nullable
|
||||
public Long abcDays; // Co2 calibration automatic baseline calibration days ( 0-200)
|
||||
|
||||
@Nullable
|
||||
public Long tvocLearningOffset; // Time constant of long-term estimator for offset. Past events will be forgotten
|
||||
// after about twice the learning time. Range 1..1000 [hours]
|
||||
|
||||
@Nullable
|
||||
public Long noxLearningOffset; // Time constant of long-term estimator for offset. Past events will be forgotten
|
||||
// after about twice the learning time. Range 1..1000 [hours]
|
||||
|
||||
@Nullable
|
||||
public String mqttBrokerUrl;
|
||||
|
||||
@Nullable
|
||||
public String temperatureUnit; // c/f
|
||||
|
||||
@Nullable
|
||||
public String configurationControl; // local, cloud, both
|
||||
|
||||
@Nullable
|
||||
public Boolean postDataToAirGradient;
|
||||
|
||||
@Nullable
|
||||
public Long ledBarBrightness; // 0 - 100
|
||||
|
||||
@Nullable
|
||||
public Long displayBrightness; // 0 - 100
|
||||
|
||||
@Nullable
|
||||
public Boolean offlineMode; // Don't connect to wifi
|
||||
|
||||
@Nullable
|
||||
public String model;
|
||||
|
||||
@Nullable
|
||||
public Boolean co2CalibrationRequested; // TRIGGER: Calibration of Co2 sensor
|
||||
|
||||
@Nullable
|
||||
public Boolean ledBarTestRequested; // TRIGGER: LEDs will run test sequence
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Data model class for a single measurement from AirGradients API.
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Measure {
|
||||
|
||||
/**
|
||||
* Returns a location id that is guaranteed to not be null.
|
||||
*
|
||||
* @return A non null location id.
|
||||
*/
|
||||
public String getLocationId() {
|
||||
String loc = locationId;
|
||||
if (loc != null) {
|
||||
return loc;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a location name that is guaranteed to not be null.
|
||||
*
|
||||
* @return A non null location name.
|
||||
*/
|
||||
public String getLocationName() {
|
||||
String name = locationName;
|
||||
return (name != null) ? name : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serial number that is guaranteed to not be null.
|
||||
*
|
||||
* @return A non null serial number.
|
||||
*/
|
||||
public String getSerialNo() {
|
||||
String serial = serialno;
|
||||
if (serial != null) {
|
||||
return serial;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a firmware version that is guaranteed to not be null.
|
||||
*
|
||||
* @return A non null firmware version.
|
||||
*/
|
||||
public String getFirmwareVersion() {
|
||||
String fw = firmwareVersion;
|
||||
if (fw != null) {
|
||||
return fw;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public @Nullable String getModel() {
|
||||
// model from cloud API
|
||||
String m = model;
|
||||
if (m != null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
// model from local API
|
||||
m = fwMode;
|
||||
if (m != null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable Long getBootCount() {
|
||||
if (bootCount == null) {
|
||||
return boot;
|
||||
}
|
||||
|
||||
return bootCount;
|
||||
}
|
||||
|
||||
public @Nullable Double getTemperature() {
|
||||
if (atmpCompensated == null) {
|
||||
return atmp;
|
||||
}
|
||||
|
||||
return atmpCompensated;
|
||||
}
|
||||
|
||||
public @Nullable Double getHumidity() {
|
||||
if (rhumCompensated == null) {
|
||||
return rhum;
|
||||
}
|
||||
|
||||
return rhumCompensated;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String locationId;
|
||||
|
||||
@Nullable
|
||||
public String locationName;
|
||||
|
||||
@Nullable
|
||||
public String serialno;
|
||||
|
||||
@Nullable
|
||||
public Double pm01; // The raw PM 1 value in ug
|
||||
|
||||
@Nullable
|
||||
public Double pm02; // The raw PM 2.5 value in ug
|
||||
|
||||
@Nullable
|
||||
public Double pm10; // The raw PM 10 value in ug
|
||||
|
||||
@Nullable
|
||||
public Double pm003Count; // The number of particles with a diameter beyond 0.3 microns in 1 deciliter of air
|
||||
|
||||
@Nullable
|
||||
public Double atmp; // The ambient temperature in celsius
|
||||
|
||||
@Nullable
|
||||
public Double atmpCompensated; // The ambient temperature, compensated for sensor inaccuracies
|
||||
|
||||
@Nullable
|
||||
public Double rhum; // The relative humidity in percent
|
||||
|
||||
@Nullable
|
||||
public Double rhumCompensated; // The relative humidity in percent, compensated for sensor inaccuracies
|
||||
|
||||
@Nullable
|
||||
public Double rco2; // The CO2 value in ppm
|
||||
|
||||
@Nullable
|
||||
public Double tvoc; // The TVOC value in ppb, provided in case that the sensor delivers an absolute value
|
||||
|
||||
@Nullable
|
||||
public Double tvocIndex; // The value of the TVOC index, sensor model dependent
|
||||
|
||||
@Nullable
|
||||
public Double tvocRaw; // Raw data from TVOC senosor
|
||||
|
||||
@Nullable
|
||||
public Double noxIndex; // The value of the NOx index, sensor model dependent
|
||||
|
||||
@Nullable
|
||||
public Double noxRaw; // Raw data from NOx sensor
|
||||
|
||||
@Nullable
|
||||
public Double wifi; // The wifi signal strength in dBm
|
||||
|
||||
@Nullable
|
||||
public Integer datapoints; // The number of datapoints, present only for aggregated data
|
||||
|
||||
@Nullable
|
||||
public String timestamp; // Timestamp of the measures in ISO 8601 format with UTC offset, e.g. 2022-03-28T12:07:40Z
|
||||
|
||||
@Nullable
|
||||
public String firmwareVersion; // The firmware version running on the device, e.g. "9.2.6", not present for averages
|
||||
|
||||
@Nullable
|
||||
public String ledMode; // co2, pm, off, default
|
||||
|
||||
@Nullable
|
||||
public String ledCo2Threshold1;
|
||||
|
||||
@Nullable
|
||||
public String ledCo2Threshold2;
|
||||
|
||||
@Nullable
|
||||
public String ledCo2ThresholdEnd;
|
||||
|
||||
@Nullable
|
||||
public Long boot; // Number of times sensor has uploaded data since last reboot
|
||||
|
||||
@Nullable
|
||||
public Long bootCount; // Same as boot, in firmwares > v3
|
||||
|
||||
@Nullable
|
||||
public String fwMode; // Model of sensor from local API
|
||||
|
||||
@Nullable
|
||||
public String model; // Model of sensor from cloud API
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.prometheus;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Single metric from Prometheus.
|
||||
*
|
||||
* Based on specification in
|
||||
* https://github.com/Showmax/prometheus-docs/blob/master/content/docs/instrumenting/exposition_formats.md
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PrometheusMetric {
|
||||
private final String metricName;
|
||||
private final double value;
|
||||
private final Instant timestamp;
|
||||
private final Map<String, String> labels;
|
||||
|
||||
public PrometheusMetric(String metricName, double value, Instant timestamp, Map<String, String> labels) {
|
||||
this.metricName = metricName;
|
||||
this.value = value;
|
||||
this.timestamp = timestamp;
|
||||
this.labels = labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a prometheus line.
|
||||
*
|
||||
* @param line The line to parse
|
||||
* @return The information we are able to parse from the line
|
||||
*/
|
||||
public static @Nullable PrometheusMetric parse(String line) {
|
||||
String trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] parts = trimmedLine.split("[{}]");
|
||||
if (parts.length == 3) {
|
||||
String[] valueParts = parts[2].trim().split("[\t ]+");
|
||||
return switch (valueParts.length) {
|
||||
case 1 -> new PrometheusMetric(parts[0], Double.parseDouble(valueParts[0]), Instant.MIN,
|
||||
parseLabels(parts[1]));
|
||||
case 2 -> new PrometheusMetric(parts[0], Double.parseDouble(valueParts[0]),
|
||||
Instant.ofEpochMilli(Long.parseLong(valueParts[1])), parseLabels(parts[1]));
|
||||
default -> null;
|
||||
};
|
||||
} else if (parts.length == 2) {
|
||||
// no idea what this is
|
||||
return null;
|
||||
} else if (parts.length == 1) {
|
||||
// no properties, parse on whitespace
|
||||
parts = trimmedLine.split("[\t ]");
|
||||
return switch (parts.length) {
|
||||
case 3 -> new PrometheusMetric(parts[0], Double.parseDouble(parts[1]),
|
||||
Instant.ofEpochMilli(Long.parseLong(parts[2])), new HashMap<>());
|
||||
case 2 -> new PrometheusMetric(parts[0], Double.parseDouble(parts[1]), Instant.MIN, new HashMap<>());
|
||||
default -> null; // No idea what this is
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseLabels(String labelPart) {
|
||||
String[] labels = labelPart.split(",");
|
||||
Map<String, String> results = new HashMap<>(labels.length);
|
||||
|
||||
for (String label : labels) {
|
||||
String parts[] = label.split("=");
|
||||
if (parts.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String labelName = parts[0].trim();
|
||||
String labelValue = parts[1].trim();
|
||||
if (labelValue.startsWith("\"")) {
|
||||
labelValue = labelValue.substring(1);
|
||||
}
|
||||
if (labelValue.endsWith("\"")) {
|
||||
labelValue = labelValue.substring(0, labelValue.length() - 1);
|
||||
}
|
||||
|
||||
results.put(labelName, labelValue);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public String getMetricName() {
|
||||
return metricName;
|
||||
}
|
||||
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Instant getTimeStamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Map<String, String> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.prometheus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Prometheus text format parser.
|
||||
*
|
||||
* Based on specification in
|
||||
* https://github.com/Showmax/prometheus-docs/blob/master/content/docs/instrumenting/exposition_formats.md
|
||||
*
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PrometheusTextParser {
|
||||
|
||||
public static List<PrometheusMetric> parse(String text) {
|
||||
String[] lines = text.split("\\r?\\n");
|
||||
List<PrometheusMetric> metrics = new ArrayList<>(lines.length);
|
||||
for (String line : lines) {
|
||||
@Nullable
|
||||
PrometheusMetric metric = PrometheusMetric.parse(line);
|
||||
if (metric != null) {
|
||||
metrics.add(metric);
|
||||
}
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="airgradient" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>AirGradient Binding</name>
|
||||
<description>This is the binding for AirGradient air quality sensors.</description>
|
||||
<connection>hybrid</connection>
|
||||
<!-- <countries/> All -->
|
||||
|
||||
<discovery-methods>
|
||||
<discovery-method>
|
||||
<service-type>mdns</service-type>
|
||||
<discovery-parameters>
|
||||
<discovery-parameter>
|
||||
<name>mdnsServiceType</name>
|
||||
<value>_airgradient._tcp.local.</value>
|
||||
</discovery-parameter>
|
||||
</discovery-parameters>
|
||||
</discovery-method>
|
||||
</discovery-methods>
|
||||
|
||||
</addon:addon>
|
@ -0,0 +1,50 @@
|
||||
# add-on
|
||||
|
||||
addon.airgradient.name = AirGradient Binding
|
||||
addon.airgradient.description = This is the binding for AirGradient air quality sensors.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.airgradient.airgradient-api.label = AirGradient API
|
||||
thing-type.airgradient.airgradient-api.description = Connection to the AirGradient API
|
||||
thing-type.airgradient.location.label = AirGradient Location
|
||||
thing-type.airgradient.location.description = AirGradient Location is where measurements are made
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.airgradient.airgradient-api.hostname.label = Hostname
|
||||
thing-type.config.airgradient.airgradient-api.hostname.description = Hostname or IP address of the API
|
||||
thing-type.config.airgradient.airgradient-api.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.airgradient.airgradient-api.refreshInterval.description = Interval the device is polled in sec.
|
||||
thing-type.config.airgradient.airgradient-api.token.label = Token
|
||||
thing-type.config.airgradient.airgradient-api.token.description = Token to access the device
|
||||
thing-type.config.airgradient.location.location.label = Location ID
|
||||
thing-type.config.airgradient.location.location.description = ID of the location
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.airgradient.calibration.label = Calibration
|
||||
channel-type.airgradient.calibration.description = Calibrate Sensors
|
||||
channel-type.airgradient.calibration.command.option.co2 = co2
|
||||
channel-type.airgradient.co2.label = CO2
|
||||
channel-type.airgradient.co2.description = CarbonDioxide
|
||||
channel-type.airgradient.leds-mode.label = LEDs Mode
|
||||
channel-type.airgradient.leds-mode.description = Mode for the LEDs
|
||||
channel-type.airgradient.leds-mode.state.option.default = default
|
||||
channel-type.airgradient.leds-mode.state.option.off = off
|
||||
channel-type.airgradient.leds-mode.state.option.pm = pm
|
||||
channel-type.airgradient.leds-mode.state.option.co2 = co2
|
||||
channel-type.airgradient.particle-count.label = Particle Count
|
||||
channel-type.airgradient.particle-count.description = Count of particles in 1 decilitre of air
|
||||
channel-type.airgradient.pm1.label = PM1
|
||||
channel-type.airgradient.pm1.description = Particulate Matter 1 (0.001mm)
|
||||
channel-type.airgradient.pm10.label = PM10
|
||||
channel-type.airgradient.pm10.description = Particulate Matter 10 (0.01mm)
|
||||
channel-type.airgradient.pm2.label = PM2
|
||||
channel-type.airgradient.pm2.description = Particulate Matter 2 (0.002mm)
|
||||
channel-type.airgradient.tvoc.label = TVOC
|
||||
channel-type.airgradient.tvoc.description = Total Volatile Organic Compounds
|
||||
channel-type.airgradient.uploads-since-boot.label = Upload count
|
||||
channel-type.airgradient.uploads-since-boot.description = Number of uploads since last reboot (boot)
|
||||
channel-type.airgradient.wifi.label = RSSI
|
||||
channel-type.airgradient.wifi.description = Received signal strength indicator
|
@ -0,0 +1,328 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airgradient"
|
||||
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">
|
||||
|
||||
<!-- A connection to the Cloud API which can have several locations (sensors) connected -->
|
||||
<bridge-type id="airgradient-api">
|
||||
<label>AirGradient API</label>
|
||||
<description>Connection to the AirGradient Cloud API</description>
|
||||
|
||||
<representation-property>token</representation-property>
|
||||
<config-description>
|
||||
<parameter name="token" type="text" required="false">
|
||||
<context>password</context>
|
||||
<label>Token</label>
|
||||
<description>Token to access the device</description>
|
||||
</parameter>
|
||||
<parameter name="hostname" type="text">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<default>https://api.airgradient.com/</default>
|
||||
<description>Hostname or IP address of the API</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s" min="1">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval the device is polled in sec.</description>
|
||||
<default>600</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<!-- A sensor you communicate directly to over the local network -->
|
||||
<thing-type id="airgradient-local">
|
||||
<label>AirGradient Local Sensor</label>
|
||||
<description>Direct network connection to a local AirGradient Sensor</description>
|
||||
|
||||
<channels>
|
||||
<channel id="pm01" typeId="pm1"/>
|
||||
<channel id="pm02" typeId="pm2"/>
|
||||
<channel id="pm10" typeId="pm10"/>
|
||||
<channel id="pm003-count" typeId="particle-count"/>
|
||||
<channel id="atmp" typeId="system.outdoor-temperature"/>
|
||||
<channel id="rhum" typeId="system.atmospheric-humidity"/>
|
||||
<channel id="wifi" typeId="wifi"/>
|
||||
<channel id="rco2" typeId="co2"/>
|
||||
<channel id="tvoc" typeId="tvoc"/>
|
||||
<channel id="leds" typeId="leds-mode"/>
|
||||
<channel id="calibration" typeId="calibration"/>
|
||||
<channel id="uploads-since-boot" typeId="uploads-since-boot"/>
|
||||
<!-- These are added dynamically if the device supports them
|
||||
<channel id="country-code" typeId="country-code"/>
|
||||
<channel id="pm-standard" typeId="pm-standard"/>
|
||||
<channel id="abc-days" typeId="abc-days"/>
|
||||
<channel id="tvoc-learning-offset" typeId="tvoc-learning-offset"/>
|
||||
<channel id="nox-learning-offset" typeId="nox-learning-offset"/>
|
||||
<channel id="mqtt-broker-url" typeId="mqtt-broker-url"/>
|
||||
<channel id="temperature-unit" typeId="temperature-unit"/>
|
||||
<channel id="configuration-control" typeId="configuration-control"/>
|
||||
<channel id="post-to-cloud" typeId="post-to-cloud"/>
|
||||
<channel id="led-bar-brightness" typeId="led-bar-brightness"/>
|
||||
<channel id="display-brightness" typeId="display-brightness"/>
|
||||
<channel id="model" typeId="model"/>
|
||||
<channel id="led-bar-test" typeId="led-bar-test"/>
|
||||
-->
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="name"/>
|
||||
<property name="firmwareVersion"/>
|
||||
<property name="serialNumber"/>
|
||||
<property name="modelId"/>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
</properties>
|
||||
<representation-property>serialNumber</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<default>http://192.168.1.1:80/measures/current</default>
|
||||
<description>Hostname or IP address of the API</description>
|
||||
<advanced>false</advanced>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s" min="1">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval the device is polled in sec.</description>
|
||||
<default>10</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Sensors are called locations in the Cloud API -->
|
||||
<thing-type id="location">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="airgradient-api"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>AirGradient Location</label>
|
||||
<description>AirGradient Location for data from the AirGradient Cloud API</description>
|
||||
|
||||
<channels>
|
||||
<channel id="pm01" typeId="pm1"/>
|
||||
<channel id="pm02" typeId="pm2"/>
|
||||
<channel id="pm10" typeId="pm10"/>
|
||||
<channel id="pm003-count" typeId="particle-count"/>
|
||||
<channel id="atmp" typeId="system.outdoor-temperature"/>
|
||||
<channel id="rhum" typeId="system.atmospheric-humidity"/>
|
||||
<channel id="wifi" typeId="wifi"/>
|
||||
<channel id="rco2" typeId="co2"/>
|
||||
<channel id="tvoc" typeId="tvoc"/>
|
||||
<channel id="leds" typeId="leds-mode"/>
|
||||
<channel id="calibration" typeId="calibration"/>
|
||||
<channel id="uploads-since-boot" typeId="uploads-since-boot"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="name"/>
|
||||
<property name="firmwareVersion"/>
|
||||
<property name="serialNumber"/>
|
||||
<property name="modelId"/>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
</properties>
|
||||
<representation-property>location</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="location" type="text" required="true">
|
||||
<label>Location ID</label>
|
||||
<description>ID of the location</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="pm1">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>PM1</label>
|
||||
<description>Particulate Matter 1 (0.001mm)</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pm2">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>PM2</label>
|
||||
<description>Particulate Matter 2 (0.002mm)</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pm10">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>PM10</label>
|
||||
<description>Particulate Matter 10 (0.01mm)</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="particle-count">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Particle Count</label>
|
||||
<description>Count of particles in 1 decilitre of air</description>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wifi">
|
||||
<item-type unitHint="dBm">Number:Power</item-type>
|
||||
<label>RSSI</label>
|
||||
<description>Received signal strength indicator</description>
|
||||
<category>QualityOfService</category>
|
||||
<state readOnly="true" pattern="%d dBm"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="co2">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>CO2</label>
|
||||
<description>CarbonDioxide</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="tvoc">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>TVOC</label>
|
||||
<description>Total Volatile Organic Compounds</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="uploads-since-boot">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Upload count</label>
|
||||
<description>Number of uploads since last reboot (boot)</description>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="leds-mode">
|
||||
<item-type>String</item-type>
|
||||
<label>LEDs Mode</label>
|
||||
<description>Mode for the LEDs</description>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="default">default</option>
|
||||
<option value="off">off</option>
|
||||
<option value="pm">pm</option>
|
||||
<option value="co2">co2</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="calibration">
|
||||
<item-type>String</item-type>
|
||||
<label>Calibration</label>
|
||||
<description>Calibrate Sensors</description>
|
||||
<command>
|
||||
<options>
|
||||
<option value="co2">co2</option>
|
||||
</options>
|
||||
</command>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="country-code">
|
||||
<item-type>String</item-type>
|
||||
<label>Country code</label>
|
||||
<description>2 digit country code (ALPHA-2)</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pm-standard">
|
||||
<item-type>String</item-type>
|
||||
<label>Parts per Million Standard</label>
|
||||
<description>Standard used for Parts per Million measurements</description>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="us-aqi">USAqi</option>
|
||||
<option value="ugm3">ugm3</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="abc-days">
|
||||
<item-type>Number</item-type>
|
||||
<label>Automatic Baseline Calibration (Days)</label>
|
||||
<description>Co2 calibration automatic baseline calibration days</description>
|
||||
<state min="0" max="200" step="1" readOnly="false" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="tvoc-learning-offset">
|
||||
<item-type>Number</item-type>
|
||||
<label>TVOC learnings offset (hours)</label>
|
||||
<description>Time constant of long-term estimator for offset. Past events will be forgotten after about twice the
|
||||
learning time.</description>
|
||||
<state min="0" max="1000" step="1" readOnly="false" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="nox-learning-offset">
|
||||
<item-type>Number</item-type>
|
||||
<label>NOX learnings offset (hours)</label>
|
||||
<description>Time constant of long-term estimator for offset. Past events will be forgotten after about twice the
|
||||
learning time.</description>
|
||||
<state min="0" max="1000" step="1" readOnly="false" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="mqtt-broker-url">
|
||||
<item-type>String</item-type>
|
||||
<label>MQTT Broker URL</label>
|
||||
<description>MQTT Broker URL</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature-unit">
|
||||
<item-type>String</item-type>
|
||||
<label>Temperature Unit</label>
|
||||
<description>Temperature unit used on the display</description>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="c">Celsius</option>
|
||||
<option value="f">Fahrenheit</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="configuration-control">
|
||||
<item-type>String</item-type>
|
||||
<label>Configuration Control</label>
|
||||
<description>Where the unit is configured from</description>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="both">Both</option>
|
||||
<option value="local">Local</option>
|
||||
<option value="cloud">Cloud</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="post-to-cloud">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Send to cloud</label>
|
||||
<description>Send data to the AirGradient cloud</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="led-bar-brightness">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Led bar brightness</label>
|
||||
<description>Brightness of the LED bar.</description>
|
||||
<state min="0" max="100" step="1" readOnly="false" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="display-brightness">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Display brightness</label>
|
||||
<description>Brightness of the display.</description>
|
||||
<state min="0" max="100" step="1" readOnly="false" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="model">
|
||||
<item-type>String</item-type>
|
||||
<label>Model</label>
|
||||
<description>Model of the device</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="led-bar-test">
|
||||
<item-type>String</item-type>
|
||||
<label>LED Bar test</label>
|
||||
<description>Test LED bar</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="airgradient:airgradient-local">
|
||||
<instruction-set targetVersion="1">
|
||||
<update-channel id="wifi">
|
||||
<type>airgradient:wifi</type>
|
||||
</update-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
<thing-type uid="airgradient:airgradient-location">
|
||||
<instruction-set targetVersion="1">
|
||||
<update-channel id="wifi">
|
||||
<type>airgradient:wifi</type>
|
||||
</update-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@SuppressWarnings({ "null" })
|
||||
@NonNullByDefault
|
||||
public class AirGradientHandlerFactoryTest {
|
||||
|
||||
@Test
|
||||
public void testSupportsThingTypes() {
|
||||
HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class);
|
||||
AirGradientHandlerFactory sut = new AirGradientHandlerFactory(httpClientFactoryMock);
|
||||
|
||||
assertThat(sut.supportsThingType(AirGradientBindingConstants.THING_TYPE_API), is(true));
|
||||
assertThat(sut.supportsThingType(AirGradientBindingConstants.THING_TYPE_LOCATION), is(true));
|
||||
assertThat(sut.supportsThingType(new ThingTypeUID("unknown", "thingtype")), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanCreateAPIHandler() {
|
||||
HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class);
|
||||
AirGradientHandlerFactory sut = new AirGradientHandlerFactory(httpClientFactoryMock);
|
||||
Bridge bridgeMock = Mockito.mock(Bridge.class);
|
||||
Mockito.when(bridgeMock.getThingTypeUID()).thenReturn(AirGradientBindingConstants.THING_TYPE_API);
|
||||
|
||||
assertThat(sut.createHandler(bridgeMock), is(notNullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanCreateLocationHandler() {
|
||||
HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class);
|
||||
AirGradientHandlerFactory sut = new AirGradientHandlerFactory(httpClientFactoryMock);
|
||||
Thing thingMock = Mockito.mock(Thing.class);
|
||||
Mockito.when(thingMock.getThingTypeUID()).thenReturn(AirGradientBindingConstants.THING_TYPE_LOCATION);
|
||||
|
||||
assertThat(sut.createHandler(thingMock), is(notNullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanCreateUnknownHandler() {
|
||||
HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class);
|
||||
AirGradientHandlerFactory sut = new AirGradientHandlerFactory(httpClientFactoryMock);
|
||||
Thing thingMock = Mockito.mock(Thing.class);
|
||||
Mockito.when(thingMock.getThingTypeUID()).thenReturn(new ThingTypeUID("unknown", "thingtype"));
|
||||
|
||||
assertThat(sut.createHandler(thingMock), is(nullValue()));
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.eclipse.jdt.annotation.Checks.requireNonNull;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@SuppressWarnings({ "null" })
|
||||
@NonNullByDefault
|
||||
public class AirGradientAPIHandlerTest {
|
||||
|
||||
private static final AirGradientAPIConfiguration TEST_CONFIG = new AirGradientAPIConfiguration() {
|
||||
{
|
||||
hostname = "abc123";
|
||||
token = "def456";
|
||||
}
|
||||
};
|
||||
|
||||
private static final String MULTI_CONTENT = """
|
||||
[
|
||||
{"locationId":1234,"locationName":"Some Name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null},
|
||||
{"locationId":4321,"locationName":"Some other name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null}
|
||||
]""";
|
||||
|
||||
@Nullable
|
||||
private AirGradientAPIHandler sut;
|
||||
|
||||
@Nullable
|
||||
Bridge bridge;
|
||||
|
||||
@Nullable
|
||||
HttpClient httpClientMock;
|
||||
|
||||
@Nullable
|
||||
Request requestMock;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
bridge = Mockito.mock(Bridge.class);
|
||||
httpClientMock = Mockito.mock(HttpClient.class);
|
||||
requestMock = Mockito.mock(Request.class);
|
||||
|
||||
sut = new AirGradientAPIHandler(requireNonNull(bridge), requireNonNull(httpClientMock));
|
||||
sut.setConfiguration(TEST_CONFIG);
|
||||
sut.setApiController(new RemoteAPIController(requireNonNull(httpClientMock), new Gson(), TEST_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRegisteredNone() {
|
||||
var res = sut.getRegisteredLocationIds();
|
||||
assertThat(res, is(empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollNoData() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(500);
|
||||
ThingHandlerCallback callbackMock = Mockito.mock(ThingHandlerCallback.class);
|
||||
|
||||
sut.setCallback(callbackMock);
|
||||
sut.pollingCode();
|
||||
|
||||
verify(callbackMock).statusUpdated(requireNonNull(bridge), new ThingStatusInfo(ThingStatus.OFFLINE,
|
||||
ThingStatusDetail.COMMUNICATION_ERROR, "Returned status code: 500"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollHasData() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getContentAsString()).thenReturn(MULTI_CONTENT);
|
||||
ThingHandlerCallback callbackMock = Mockito.mock(ThingHandlerCallback.class);
|
||||
|
||||
sut.setCallback(callbackMock);
|
||||
sut.pollingCode();
|
||||
|
||||
verify(callbackMock).statusUpdated(requireNonNull(bridge),
|
||||
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.eclipse.jdt.annotation.Checks.requireNonNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.binding.airgradient.internal.model.Measure;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@SuppressWarnings({ "null" })
|
||||
@NonNullByDefault
|
||||
public class AirGradientLocationHandlerTest {
|
||||
|
||||
private static final Measure TEST_MEASURE = new Measure() {
|
||||
{
|
||||
locationId = "12345";
|
||||
locationName = "Location name";
|
||||
pm01 = 2d;
|
||||
pm02 = 3d;
|
||||
pm10 = 4d;
|
||||
pm003Count = 636d;
|
||||
atmp = 19.63;
|
||||
rhum = null;
|
||||
rco2 = 455d;
|
||||
tvoc = 51.644928;
|
||||
wifi = -59d;
|
||||
timestamp = "2024-01-07T11:28:56.000Z";
|
||||
serialno = "ecda3b1a2a50";
|
||||
firmwareVersion = "12345";
|
||||
tvocIndex = 1d;
|
||||
noxIndex = 2d;
|
||||
}
|
||||
};
|
||||
|
||||
private static final Measure TEST_MEASURE_V3_1_1 = new Measure() {
|
||||
{
|
||||
locationId = "12345";
|
||||
locationName = "Location name";
|
||||
timestamp = "2024-01-07T11:28:56.000Z";
|
||||
serialno = "ecda3b1a2a50";
|
||||
firmwareVersion = "3.1.1";
|
||||
atmpCompensated = 24.2;
|
||||
rhumCompensated = 36d;
|
||||
bootCount = 16l;
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
private AirGradientLocationHandler sut;
|
||||
|
||||
@Nullable
|
||||
private ThingHandlerCallback callbackMock;
|
||||
|
||||
@Nullable
|
||||
private Thing thing;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
callbackMock = Mockito.mock(ThingHandlerCallback.class);
|
||||
Mockito.when(callbackMock.isChannelLinked(any(ChannelUID.class))).thenReturn(true);
|
||||
thing = Mockito.mock(Thing.class);
|
||||
|
||||
sut = new AirGradientLocationHandler(requireNonNull(thing));
|
||||
sut.setCallback(callbackMock);
|
||||
|
||||
Mockito.when(thing.getUID()).thenReturn(new ThingUID(THING_TYPE_LOCATION, "1234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMeasure() {
|
||||
sut.setCallback(callbackMock);
|
||||
sut.setMeasurment(TEST_MEASURE);
|
||||
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_WIFI),
|
||||
new QuantityType<>("-59 dBm"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_PM_01),
|
||||
new QuantityType<>("2 µg/m³"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_PM_02),
|
||||
new QuantityType<>("3 µg/m³"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_PM_10),
|
||||
new QuantityType<>("4 µg/m³"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_PM_003_COUNT),
|
||||
new QuantityType<>("636"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_ATMP),
|
||||
new QuantityType<>("19.63 °C"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_RHUM), UnDefType.NULL);
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_RCO2),
|
||||
new QuantityType<>("455 ppm"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_TVOC),
|
||||
new QuantityType<>("51 ppb"));
|
||||
}
|
||||
|
||||
// Firmware Version 3.1.1 has slight changes in the Json
|
||||
@Test
|
||||
public void testSetMeasureVersion3_1_1() {
|
||||
sut.setCallback(callbackMock);
|
||||
sut.setMeasurment(TEST_MEASURE_V3_1_1);
|
||||
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_ATMP),
|
||||
new QuantityType<>("24.2 °C"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_RHUM),
|
||||
new QuantityType<>("36 %"));
|
||||
verify(callbackMock).stateUpdated(new ChannelUID(sut.getThing().getUID(), CHANNEL_UPLOADS_SINCE_BOOT),
|
||||
new QuantityType<>("16"));
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.handler;
|
||||
|
||||
import static org.eclipse.jdt.annotation.Checks.requireNonNull;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.closeTo;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
|
||||
import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
|
||||
import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@SuppressWarnings({ "null" })
|
||||
@NonNullByDefault
|
||||
public class RemoteApiControllerTest {
|
||||
|
||||
private static final AirGradientAPIConfiguration TEST_CONFIG = new AirGradientAPIConfiguration() {
|
||||
{
|
||||
hostname = "abc123";
|
||||
token = "def456";
|
||||
}
|
||||
};
|
||||
|
||||
private static final String SINGLE_CONFIG = """
|
||||
{"country":"NO","pmStandard":"ugm3","ledBarMode":"off","abcDays":8,"tvocLearningOffset":12,"noxLearningOffset":12,"mqttBrokerUrl":"https://192.168.1.1/mqtt","temperatureUnit":"c","configurationControl":"both","postDataToAirGradient":true,"ledBarBrightness":100,"displayBrightness":100,"offlineMode":false,"model":"I-9PSL"}
|
||||
""";
|
||||
|
||||
private static final String SINGLE_CONTENT = """
|
||||
{"locationId":4321,"locationName":"Some other name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":456,"tvoc":51.644928,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null}
|
||||
""";
|
||||
|
||||
private static final String MULTI_CONTENT = """
|
||||
[
|
||||
{"locationId":1234,"locationName":"Some Name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null},
|
||||
{"locationId":4321,"locationName":"Some other name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null}
|
||||
]""";
|
||||
private static final String MULTI_CONTENT2 = """
|
||||
[{"locationId":654321,"locationName":"xxxx","pm01":0,"pm02":1,"pm10":1,"pm003Count":null,"atmp":24.2,"rhum":18,"rco2":519,"tvoc":50.793266,"wifi":-62,"timestamp":"2024-02-01T19:15:37.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"ecda3b1a2a50","firmwareVersion":null,"tvocIndex":52,"noxIndex":1},{"locationId":123456,"locationName":"yyyy","pm01":0,"pm02":0,"pm10":0,"pm003Count":105,"atmp":22.33,"rhum":24,"rco2":468,"tvoc":130.95694,"wifi":-50,"timestamp":"2024-02-01T19:15:34.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"84fce612e644","firmwareVersion":null,"tvocIndex":137,"noxIndex":1}]
|
||||
""";
|
||||
|
||||
private static final String PROMETHEUS_CONTENT = """
|
||||
# HELP pm02 Particulate Matter PM2.5 value
|
||||
# TYPE pm02 gauge
|
||||
pm02{id="Airgradient"}6
|
||||
# HELP rco2 CO2 value, in ppm
|
||||
# TYPE rco2 gauge
|
||||
rco2{id="Airgradient"}862
|
||||
# HELP atmp Temperature, in degrees Celsius
|
||||
# TYPE atmp gauge
|
||||
atmp{id="Airgradient"}31.6
|
||||
# HELP rhum Relative humidity, in percent
|
||||
# TYPE rhum gauge
|
||||
rhum{id="Airgradient"}38
|
||||
# HELP tvoc Total volatile organic components, in μg/m³
|
||||
# TYPE tvoc gauge
|
||||
tvoc{id="Airgradient"}51.644928
|
||||
# HELP nox, in μg/m³
|
||||
# TYPE nox gauge
|
||||
nox{id="Airgradient"}1
|
||||
""";
|
||||
|
||||
private static final String OPEN_METRICS_CONTENT = """
|
||||
# HELP airgradient_info AirGradient device information
|
||||
# TYPE airgradient_info info
|
||||
airgradient_info{airgradient_serial_number="4XXXXXXXXXXc",airgradient_device_type="ONE_I-9PSL",airgradient_library_version="3.0.4"} 1
|
||||
# HELP airgradient_config_ok 1 if the AirGradient device was able to successfully fetch its configuration from the server
|
||||
# TYPE airgradient_config_ok gauge
|
||||
airgradient_config_ok{} 1
|
||||
# HELP airgradient_post_ok 1 if the AirGradient device was able to successfully send to the server
|
||||
# TYPE airgradient_post_ok gauge
|
||||
airgradient_post_ok{} 1
|
||||
# HELP airgradient_wifi_rssi_dbm WiFi signal strength from the AirGradient device perspective, in dBm
|
||||
# TYPE airgradient_wifi_rssi_dbm gauge
|
||||
# UNIT airgradient_wifi_rssi_dbm dbm
|
||||
airgradient_wifi_rssi_dbm{} -51
|
||||
# HELP airgradient_co2_ppm Carbon dioxide concentration as measured by the AirGradient S8 sensor, in parts per million
|
||||
# TYPE airgradient_co2_ppm gauge
|
||||
# UNIT airgradient_co2_ppm ppm
|
||||
airgradient_co2_ppm{} 589
|
||||
# HELP airgradient_pm1_ugm3 PM1.0 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
|
||||
# TYPE airgradient_pm1_ugm3 gauge
|
||||
# UNIT airgradient_pm1_ugm3 ugm3
|
||||
airgradient_pm1_ugm3{} 3
|
||||
# HELP airgradient_pm2d5_ugm3 PM2.5 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
|
||||
# TYPE airgradient_pm2d5_ugm3 gauge
|
||||
# UNIT airgradient_pm2d5_ugm3 ugm3
|
||||
airgradient_pm2d5_ugm3{} 3
|
||||
# HELP airgradient_pm10_ugm3 PM10 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
|
||||
# TYPE airgradient_pm10_ugm3 gauge
|
||||
# UNIT airgradient_pm10_ugm3 ugm3
|
||||
airgradient_pm10_ugm3{} 3
|
||||
# HELP airgradient_pm0d3_p100ml PM0.3 concentration as measured by the AirGradient PMS sensor, in number of particules per 100 milliliters
|
||||
# TYPE airgradient_pm0d3_p100ml gauge
|
||||
# UNIT airgradient_pm0d3_p100ml p100ml
|
||||
airgradient_pm0d3_p100ml{} 594
|
||||
# HELP airgradient_tvoc_index The processed Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor
|
||||
# TYPE airgradient_tvoc_index gauge
|
||||
airgradient_tvoc_index{} 220
|
||||
# HELP airgradient_tvoc_raw_index The raw input value to the Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor
|
||||
# TYPE airgradient_tvoc_raw_index gauge
|
||||
airgradient_tvoc_raw_index{} 30801
|
||||
# HELP airgradient_nox_index The processed Nitrous Oxide (NOx) index as measured by the AirGradient SGP sensor
|
||||
# TYPE airgradient_nox_index gauge
|
||||
airgradient_nox_index{} 1
|
||||
# HELP airgradient_temperature_degc The ambient temperature as measured by the AirGradient SHT sensor, in degrees Celsius
|
||||
# TYPE airgradient_temperature_degc gauge
|
||||
# UNIT airgradient_temperature_degc degc
|
||||
airgradient_temperature_degc{} 23.69
|
||||
# HELP airgradient_humidity_percent The relative humidity as measured by the AirGradient SHT sensor
|
||||
# TYPE airgradient_humidity_percent gauge
|
||||
# UNIT airgradient_humidity_percent percent
|
||||
airgradient_humidity_percent{} 39
|
||||
# EOF
|
||||
""";
|
||||
|
||||
@Nullable
|
||||
private RemoteAPIController sut;
|
||||
|
||||
@Nullable
|
||||
HttpClient httpClientMock;
|
||||
|
||||
@Nullable
|
||||
Request requestMock;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
httpClientMock = Mockito.mock(HttpClient.class);
|
||||
requestMock = Mockito.mock(Request.class);
|
||||
|
||||
sut = new RemoteAPIController(requireNonNull(httpClientMock), new Gson(), TEST_CONFIG);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresNone() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/json");
|
||||
Mockito.when(response.getStatus()).thenReturn(500);
|
||||
|
||||
AirGradientCommunicationException agce = Assertions.assertThrows(AirGradientCommunicationException.class,
|
||||
() -> sut.getMeasures());
|
||||
assertThat(agce.getMessage(), is("Returned status code: 500"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresSingle() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/json");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(SINGLE_CONTENT);
|
||||
|
||||
var res = sut.getMeasures();
|
||||
assertThat(res, is(not(empty())));
|
||||
assertThat(res.size(), is(1));
|
||||
assertThat(res.get(0).locationName, is("Some other name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresMulti() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/json");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(MULTI_CONTENT);
|
||||
|
||||
var res = sut.getMeasures();
|
||||
assertThat(res, is(not(empty())));
|
||||
assertThat(res.size(), is(2));
|
||||
assertThat(res.get(0).locationName, is("Some Name"));
|
||||
assertThat(res.get(1).locationName, is("Some other name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresMulti2() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/json");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(MULTI_CONTENT2);
|
||||
|
||||
var res = sut.getMeasures();
|
||||
assertThat(res, is(not(empty())));
|
||||
assertThat(res.size(), is(2));
|
||||
assertThat(res.get(0).locationName, is("xxxx"));
|
||||
assertThat(res.get(1).locationName, is("yyyy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresPrometheus() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("text/plain");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(PROMETHEUS_CONTENT);
|
||||
|
||||
var res = sut.getMeasures();
|
||||
assertThat(res, is(not(empty())));
|
||||
assertThat(res.size(), is(1));
|
||||
assertThat(res.get(0).pm02, closeTo(6, 0.1));
|
||||
assertThat(res.get(0).rco2, closeTo(862, 0.1));
|
||||
assertThat(res.get(0).atmp, closeTo(31.6, 0.1));
|
||||
assertThat(res.get(0).rhum, closeTo(38, 0.1));
|
||||
assertThat(res.get(0).tvoc, closeTo(51.644928, 0.1));
|
||||
assertThat(res.get(0).noxIndex, closeTo(1, 0.1));
|
||||
assertThat(res.get(0).locationId, is("Airgradient"));
|
||||
assertThat(res.get(0).locationName, is("Airgradient"));
|
||||
assertThat(res.get(0).serialno, is("Airgradient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMeasuresOpenMetrics() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/openmetrics-text");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(OPEN_METRICS_CONTENT);
|
||||
|
||||
var res = sut.getMeasures();
|
||||
assertThat(res, is(not(empty())));
|
||||
assertThat(res.size(), is(1));
|
||||
assertThat(res.get(0).pm01, closeTo(3, 0.1));
|
||||
assertThat(res.get(0).pm02, closeTo(3, 0.1));
|
||||
assertThat(res.get(0).pm10, closeTo(3, 0.1));
|
||||
assertThat(res.get(0).rco2, closeTo(589, 0.1));
|
||||
assertThat(res.get(0).atmp, closeTo(23.69, 0.1));
|
||||
assertThat(res.get(0).rhum, closeTo(39, 0.1));
|
||||
assertThat(res.get(0).tvoc, closeTo(220, 0.1));
|
||||
assertThat(res.get(0).noxIndex, closeTo(1, 0.1));
|
||||
assertThat(res.get(0).serialno, is("4XXXXXXXXXXc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfig() throws Exception {
|
||||
ContentResponse response = Mockito.mock(ContentResponse.class);
|
||||
Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
|
||||
Mockito.when(requestMock.send()).thenReturn(response);
|
||||
Mockito.when(response.getStatus()).thenReturn(200);
|
||||
Mockito.when(response.getMediaType()).thenReturn("application/json");
|
||||
Mockito.when(response.getContentAsString()).thenReturn(SINGLE_CONFIG);
|
||||
|
||||
var res = sut.getConfig();
|
||||
assertThat(res.abcDays, is(8L));
|
||||
assertThat(res.configurationControl, is("both"));
|
||||
assertThat(res.country, is("NO"));
|
||||
assertThat(res.displayBrightness, is(100L));
|
||||
assertThat(res.ledBarBrightness, is(100L));
|
||||
assertThat(res.ledBarMode, is("off"));
|
||||
assertThat(res.model, is("I-9PSL"));
|
||||
assertThat(res.mqttBrokerUrl, is("https://192.168.1.1/mqtt"));
|
||||
assertThat(res.noxLearningOffset, is(12L));
|
||||
assertThat(res.pmStandard, is("ugm3"));
|
||||
assertThat(res.postDataToAirGradient, is(true));
|
||||
assertThat(res.temperatureUnit, is("c"));
|
||||
assertThat(res.tvocLearningOffset, is(12L));
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airgradient.internal.prometheus;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.closeTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Jørgen Austvik - Initial contribution
|
||||
*/
|
||||
@SuppressWarnings({ "null" })
|
||||
@NonNullByDefault
|
||||
public class PrometheusMetricTest {
|
||||
|
||||
@Test
|
||||
public void testParseEmpty() {
|
||||
var res = PrometheusMetric.parse("");
|
||||
assertThat(res, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseComment() {
|
||||
var res = PrometheusMetric.parse("# Comment");
|
||||
assertThat(res, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseAirGradient() {
|
||||
var res = PrometheusMetric.parse("atmp{id=\"Airgradient\"}31.6");
|
||||
assertThat(res.getMetricName(), is("atmp"));
|
||||
assertThat(res.getValue(), closeTo(31.6, 0.1));
|
||||
assertThat(res.getLabels().get("id"), is("Airgradient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNoLables() {
|
||||
var res = PrometheusMetric.parse("http_request_duration_seconds_count 144320");
|
||||
assertThat(res.getMetricName(), is("http_request_duration_seconds_count"));
|
||||
assertThat(res.getValue(), closeTo(144320, 0.1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseWithTimestamp() {
|
||||
var res = PrometheusMetric.parse("http_requests_total{method=\"post\",code=\"200\"} 1027 1395066363000");
|
||||
assertThat(res.getMetricName(), is("http_requests_total"));
|
||||
assertThat(res.getValue(), closeTo(1027, 0.1));
|
||||
assertThat(res.getTimeStamp(), is(Instant.ofEpochMilli(1395066363000L)));
|
||||
assertThat(res.getLabels().get("method"), is("post"));
|
||||
assertThat(res.getLabels().get("code"), is("200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNegativeEpoch() {
|
||||
var res = PrometheusMetric.parse("something_weird{problem=\"division by zero\"} 123 -3982045");
|
||||
assertThat(res.getMetricName(), is("something_weird"));
|
||||
assertThat(res.getTimeStamp(), is(Instant.ofEpochMilli(-3982045)));
|
||||
assertThat(res.getValue(), closeTo(123, 0.1));
|
||||
assertThat(res.getLabels().get("problem"), is("division by zero"));
|
||||
}
|
||||
}
|
@ -53,25 +53,28 @@ The rw column is empty if the channel is only readable, w if the channel can be
|
||||
| fineDustCnt02_5 | Number:Dimensionless | | Fine Dust >2,5 µm |
|
||||
| fineDustCnt05 | Number:Dimensionless | | Fine Dust >5 µm |
|
||||
| fineDustCnt10 | Number:Dimensionless | | Fine Dust >10 µm |
|
||||
| co | Number | | CO concentration |
|
||||
| co2 | Number:Dimensionless | | CO₂ concentration |
|
||||
| co | Number:Density | | Carbon monoxide (CO) concentration |
|
||||
| co2 | Number:Dimensionless | | Carbon dioxide (CO₂) concentration |
|
||||
| dCO2dt | Number | | Change of CO₂ concentration |
|
||||
| dHdt | Number | | Change of Humidity |
|
||||
| dewpt | Number:Temperature | | Dew Point |
|
||||
| doorEvent | Number | | Door Event (experimental, might not work reliably) |
|
||||
| h2s | Number:Density | | Hydrogen sulfide (H₂S) |
|
||||
| healthIndex | Number:Dimensionless | | Health Index in percent |
|
||||
| health | Number:Dimensionless | | Health Index (0 to 1000, -200 for gas alarm, -800 for fire alarm) |
|
||||
| humidityRelative | Number:Dimensionless | | Humidity in percent |
|
||||
| humidityAbsolute | Number | | Absolute Humidity |
|
||||
| humidityAbsolute | Number:Density | | Absolute Humidity |
|
||||
| measureTime | Number:Time | | Milliseconds needed for measurement |
|
||||
| no2 | Number | | NO₂ concentration |
|
||||
| o3 | Number | | Ozone (O₃) concentration |
|
||||
| no2 | Number:Density | | Nitrogen Dioxide (NO₂) concentration |
|
||||
| o3 | Number:Density | | Ozone (O₃) concentration |
|
||||
| o2 | Number:Dimensionless | | Oxygen (O₂) concentration |
|
||||
| performanceIndex | Number:Dimensionless | | Performance Index in percent |
|
||||
| performance | Number:Dimensionless | | Performance Index (0 to 1000) |
|
||||
| fineDustConc01 | Number | | Fine Dust concentration >1 µm |
|
||||
| fineDustConc02_5 | Number | | Fine Dust concentration >2.5 µm |
|
||||
| fineDustConc10 | Number | | Fine Dust concentration >10 µm fni |
|
||||
| pressure | Number:Pressure | | Pressure |
|
||||
| so2 | Number | | SO₂ concentration |
|
||||
| fineDustConc10 | Number | | Fine Dust concentration >10 µm |
|
||||
| pressure | Number:Pressure | | Barometric Pressure |
|
||||
| so2 | Number | | Sulfur dioxide (SO₂) concentration |
|
||||
| sound | Number:Dimensionless | | Noise |
|
||||
| temperature | Number:Temperature | | Temperature |
|
||||
| timestamp | DateTime | | Timestamp of measurement |
|
||||
@ -116,6 +119,12 @@ The rw column is empty if the channel is only readable, w if the channel can be
|
||||
| errorBars | Switch | rw | Calculate Maximum Errors |
|
||||
| warmupPhase | Switch | rw | Output data as Warmup Phase |
|
||||
|
||||
## Usage with Docker
|
||||
|
||||
This binding requires the JVM cryptographic strength policy to be set to "unlimited".
|
||||
Otherwise the connection to the device will fail.
|
||||
See the [openHAB Docker image documentation](https://github.com/openhab/openhab-docker/blob/main/README.md#java-cryptographic-strength-policy) for details.
|
||||
|
||||
## Example
|
||||
|
||||
### air-Q.things
|
||||
|
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.airq</artifactId>
|
||||
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airq.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception for handling an empty response.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirqEmptyResonseException extends AirqException {
|
||||
private static final long serialVersionUID = 1423144673651821622L;
|
||||
|
||||
public AirqEmptyResonseException() {
|
||||
super("Device sent an empty response");
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airq.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* General exception for this binding.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirqException extends Exception {
|
||||
private static final long serialVersionUID = 8255154215873928896L;
|
||||
|
||||
public AirqException() {
|
||||
// nothing
|
||||
}
|
||||
|
||||
public AirqException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AirqException(Exception exception) {
|
||||
super(exception);
|
||||
}
|
||||
|
||||
public AirqException(String message, Exception exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
}
|
@ -65,16 +65,17 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link AirqHandler} is responsible for retrieving all information from the air-Q device
|
||||
* and change properties and channels accordingly.
|
||||
*
|
||||
* @author Aurelio Caliaro - Initial contribution
|
||||
* @author Fabian Wolter - Improve error handling
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirqHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirqHandler.class);
|
||||
private final Gson gson = new Gson();
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
@ -100,19 +101,23 @@ public class AirqHandler extends BaseThingHandler {
|
||||
* Expects a string consisting of two values as sent by the air-Q device
|
||||
* and returns a corresponding object
|
||||
*
|
||||
* @param input string formed as this: [1234,56,789,012] (including the brackets)
|
||||
* @param input element as JSON array
|
||||
* @return ResultPair object with the two values
|
||||
* @throws AirqException when parsing fails
|
||||
*/
|
||||
public ResultPair(String input) {
|
||||
value = Float.parseFloat(input.substring(1, input.indexOf(',')));
|
||||
maxdev = Float.parseFloat(input.substring(input.indexOf(',') + 1, input.length() - 1));
|
||||
public ResultPair(JsonElement input) throws JsonSyntaxException {
|
||||
if (input instanceof JsonArray pair && pair.size() == 2) {
|
||||
value = pair.get(0).getAsFloat();
|
||||
maxdev = pair.get(1).getAsFloat();
|
||||
} else {
|
||||
throw new JsonSyntaxException("Failed to parse pair: " + input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AirqHandler(Thing thing, HttpClient httpClient) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
logger.warn("air-Q - airqHandler - constructor: httpClient={}", httpClient);
|
||||
}
|
||||
|
||||
private boolean isTimeFormat(String str) {
|
||||
@ -309,17 +314,7 @@ public class AirqHandler extends BaseThingHandler {
|
||||
public void initialize() {
|
||||
config = getThing().getConfiguration().as(AirqConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
// We don't have to test if ipAddress and password have been set because we have defined them
|
||||
// as being 'required' in thing-types.xml and OpenHAB will only initialize the handler if both are set.
|
||||
String data = getDecryptedContentString("http://" + config.ipAddress + "/data", "GET", null);
|
||||
// we try if the device is reachable and the password is correct. Otherwise a corresponding message is
|
||||
// thrown in Thing manager.
|
||||
if (data == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Unable to retrieve get data from air-Q device. Probable cause: invalid password.");
|
||||
} else {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::pollData, 0, POLLING_PERIOD_DATA_MSEC,
|
||||
TimeUnit.MILLISECONDS);
|
||||
getConfigDataJob = scheduler.scheduleWithFixedDelay(this::getConfigData, 0, POLLING_PERIOD_CONFIG,
|
||||
@ -327,9 +322,8 @@ public class AirqHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
// AES decoding based on this tutorial: https://www.javainterviewpoint.com/aes-256-encryption-and-decryption/
|
||||
public @Nullable String decrypt(byte[] base64text, String password) {
|
||||
public String decrypt(byte[] base64text, String password) throws AirqException {
|
||||
String content = "";
|
||||
logger.trace("air-Q - airqHandler - decrypt(): content to decrypt: {}", base64text);
|
||||
byte[] encodedtextwithIV = Base64.getDecoder().decode(base64text);
|
||||
byte[] ciphertext = Arrays.copyOfRange(encodedtextwithIV, 16, encodedtextwithIV.length);
|
||||
byte[] passkey = Arrays.copyOf(password.getBytes(), 32);
|
||||
@ -344,18 +338,16 @@ public class AirqHandler extends BaseThingHandler {
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
byte[] decryptedText = cipher.doFinal(ciphertext);
|
||||
content = new String(decryptedText, StandardCharsets.UTF_8);
|
||||
logger.trace("air-Q - airqHandler - decrypt(): Text decoded as String: {}", content);
|
||||
} catch (BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||
return content;
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
|
||||
logger.warn("Error while decrypting. Probably the provided password is wrong.");
|
||||
return null;
|
||||
throw new AirqException(exc);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AirqPasswordIncorrectException();
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public String encrypt(byte[] toencode, String password) {
|
||||
String content = "";
|
||||
logger.trace("air-Q - airqHandler - encrypt(): text to encode: {}", new String(toencode));
|
||||
public String encrypt(byte[] toencode, String password) throws AirqException {
|
||||
byte[] passkey = Arrays.copyOf(password.getBytes(StandardCharsets.UTF_8), 32);
|
||||
if (password.length() < 32) {
|
||||
Arrays.fill(passkey, password.length(), 32, (byte) '0');
|
||||
@ -374,47 +366,32 @@ public class AirqHandler extends BaseThingHandler {
|
||||
System.arraycopy(iv, 0, totaltext, 0, 16);
|
||||
System.arraycopy(encryptedText, 0, totaltext, 16, encryptedText.length);
|
||||
byte[] encodedcontent = Base64.getEncoder().encode(totaltext);
|
||||
logger.trace("air-Q - airqHandler - encrypt(): encrypted text: {}", encodedcontent);
|
||||
content = new String(encodedcontent);
|
||||
} catch (Exception e) {
|
||||
logger.warn("air-Q - airqHandler - encrypt(): Error while encrypting: {}", e.toString());
|
||||
return new String(encodedcontent);
|
||||
} catch (BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
|
||||
throw new AirqException("Failed to encrypt data", exc);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// gets the data after online/offline management and does the JSON work, or at least the first step.
|
||||
protected @Nullable String getDecryptedContentString(String url, String requestMethod, @Nullable String body) {
|
||||
Result res = null;
|
||||
String jsonAnswer = null;
|
||||
res = getData(url, "GET", null);
|
||||
if (res != null) {
|
||||
String jsontext = res.getBody();
|
||||
logger.trace("air-Q - airqHandler - getDecryptedContentString(): Result from getData() is {} with body={}",
|
||||
res, res.getBody());
|
||||
// Gson code based on https://riptutorial.com/de/gson
|
||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||
if (ans != null) {
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
if (jsonAnswer == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Decryption not possible, probably wrong password");
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"air-Q - airqHandler - getDecryptedContentString(): The air-Q data could not be extracted from this string: {}",
|
||||
ans);
|
||||
}
|
||||
protected String getDecryptedContentString(String url, String requestMethod, @Nullable String body)
|
||||
throws AirqException, InterruptedException {
|
||||
Result res = getData(url, "GET", null);
|
||||
String jsontext = res.getBody();
|
||||
// Gson code based on https://riptutorial.com/de/gson
|
||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||
if (ans == null) {
|
||||
throw new AirqEmptyResonseException();
|
||||
}
|
||||
return jsonAnswer;
|
||||
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
return decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
}
|
||||
|
||||
// calls the networking job and in addition does additional tests for online/offline management
|
||||
protected @Nullable Result getData(String address, String requestMethod, @Nullable String body) {
|
||||
Result res = null;
|
||||
protected Result getData(String address, String requestMethod, @Nullable String body)
|
||||
throws AirqException, InterruptedException {
|
||||
int timeout = 10;
|
||||
logger.debug("air-Q - airqHandler - getData(): connecting to {} with method {} and body {}", address,
|
||||
requestMethod, body);
|
||||
Request request = httpClient.newRequest(address).timeout(timeout, TimeUnit.SECONDS).method(requestMethod);
|
||||
if (body != null) {
|
||||
request = request.content(new StringContentProvider(body)).header(HttpHeader.CONTENT_TYPE,
|
||||
@ -422,22 +399,12 @@ public class AirqHandler extends BaseThingHandler {
|
||||
}
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
res = new Result(response.getContentAsString(), response.getStatus());
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
|
||||
logger.warn("air-Q - airqHandler - doNetwork(): Error while accessing air-Q: {}", exc.toString());
|
||||
return new Result(response.getContentAsString(), response.getStatus());
|
||||
} catch (ExecutionException e) {
|
||||
throw new AirqException("Connection failed: " + e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
throw new AirqException("Timeout while connecting");
|
||||
}
|
||||
if (res == null) {
|
||||
if (getThing().getStatus() != ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "air-Q device not reachable");
|
||||
} else {
|
||||
logger.warn("air-Q - airqHandler - getData(): retried but still cannot reach the air-Q device.");
|
||||
}
|
||||
} else {
|
||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
@ -460,151 +427,161 @@ public class AirqHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
ScheduledFuture<?> localPollingJob = pollingJob;
|
||||
if (localPollingJob != null) {
|
||||
localPollingJob.cancel(true);
|
||||
}
|
||||
if (getConfigDataJob != null) {
|
||||
getConfigDataJob.cancel(true);
|
||||
|
||||
ScheduledFuture<?> localGetConfigDataJob = getConfigDataJob;
|
||||
if (localGetConfigDataJob != null) {
|
||||
localGetConfigDataJob.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void pollData() {
|
||||
logger.trace("air-Q - airqHandler - run(): starting polled data handler");
|
||||
try {
|
||||
String url = "http://" + config.ipAddress + "/data";
|
||||
String jsonAnswer = getDecryptedContentString(url, "GET", null);
|
||||
if (jsonAnswer != null) {
|
||||
|
||||
try {
|
||||
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
||||
if (decEl != null) {
|
||||
JsonObject decObj = decEl.getAsJsonObject();
|
||||
logger.debug("air-Q - airqHandler - run(): decObj={}, jsonAnswer={}", decObj, jsonAnswer);
|
||||
// 'bat' is a field that is already delivered by air-Q but as
|
||||
// there are no air-Q devices which are powered with batteries
|
||||
// it is obsolete at this moment. We implemented the code anyway
|
||||
// to make it easier to add afterwords, but for the moment it is not applicable.
|
||||
// processType(decObj, "bat", "battery", "pair");
|
||||
processType(decObj, "cnt0_3", "fineDustCnt00_3", "pair");
|
||||
processType(decObj, "cnt0_5", "fineDustCnt00_5", "pair");
|
||||
processType(decObj, "cnt1", "fineDustCnt01", "pair");
|
||||
processType(decObj, "cnt2_5", "fineDustCnt02_5", "pair");
|
||||
processType(decObj, "cnt5", "fineDustCnt05", "pair");
|
||||
processType(decObj, "cnt10", "fineDustCnt10", "pair");
|
||||
processType(decObj, "co", "co", "pair");
|
||||
processType(decObj, "co2", "co2", "pairPPM");
|
||||
processType(decObj, "dewpt", "dewpt", "pair");
|
||||
processType(decObj, "humidity", "humidityRelative", "pair");
|
||||
processType(decObj, "humidity_abs", "humidityAbsolute", "pair");
|
||||
processType(decObj, "no2", "no2", "pair");
|
||||
processType(decObj, "o3", "o3", "pair");
|
||||
processType(decObj, "oxygen", "o2", "pair");
|
||||
processType(decObj, "pm1", "fineDustConc01", "pair");
|
||||
processType(decObj, "pm2_5", "fineDustConc02_5", "pair");
|
||||
processType(decObj, "pm10", "fineDustConc10", "pair");
|
||||
processType(decObj, "pressure", "pressure", "pair");
|
||||
processType(decObj, "so2", "so2", "pair");
|
||||
processType(decObj, "sound", "sound", "pairDB");
|
||||
processType(decObj, "temperature", "temperature", "pair");
|
||||
// We have two places where the Device ID is delivered: with the measurement data and
|
||||
// with the configuration.
|
||||
// We take the info from the configuration and show it as a property, so we don't need
|
||||
// something like processType(decObj, "DeviceID", "DeviceID", "string") at this moment. We leave
|
||||
// this as a reminder in case for some reason it will be needed in future, e.g. when an air-Q
|
||||
// device also sends data from other devices (then with another Device ID)
|
||||
processType(decObj, "Status", "status", "string");
|
||||
processType(decObj, "TypPS", "avgFineDustSize", "number");
|
||||
processType(decObj, "dCO2dt", "dCO2dt", "number");
|
||||
processType(decObj, "dHdt", "dHdt", "number");
|
||||
processType(decObj, "door_event", "doorEvent", "number");
|
||||
processType(decObj, "health", "health", "number");
|
||||
processType(decObj, "measuretime", "measureTime", "number");
|
||||
processType(decObj, "performance", "performance", "number");
|
||||
processType(decObj, "timestamp", "timestamp", "datetime");
|
||||
processType(decObj, "uptime", "uptime", "numberTimePeriod");
|
||||
processType(decObj, "tvoc", "tvoc", "pairPPB");
|
||||
} else {
|
||||
logger.warn("The air-Q data could not be extracted from this string: {}", decEl);
|
||||
if (decEl == null) {
|
||||
throw new AirqEmptyResonseException();
|
||||
}
|
||||
|
||||
JsonObject decObj = decEl.getAsJsonObject();
|
||||
// 'bat' is a field that is already delivered by air-Q but as
|
||||
// there are no air-Q devices which are powered with batteries
|
||||
// it is obsolete at this moment. We implemented the code anyway
|
||||
// to make it easier to add afterwords, but for the moment it is not applicable.
|
||||
// processType(decObj, "bat", "battery", "pair");
|
||||
processType(decObj, "cnt0_3", "fineDustCnt00_3", "pair");
|
||||
processType(decObj, "cnt0_5", "fineDustCnt00_5", "pair");
|
||||
processType(decObj, "cnt1", "fineDustCnt01", "pair");
|
||||
processType(decObj, "cnt2_5", "fineDustCnt02_5", "pair");
|
||||
processType(decObj, "cnt5", "fineDustCnt05", "pair");
|
||||
processType(decObj, "cnt10", "fineDustCnt10", "pair");
|
||||
processType(decObj, "co", "co", "pair");
|
||||
processType(decObj, "co2", "co2", "pairPPM");
|
||||
processType(decObj, "dewpt", "dewpt", "pair");
|
||||
processType(decObj, "h2s", "h2s", "pair");
|
||||
processType(decObj, "humidity", "humidityRelative", "pair");
|
||||
processType(decObj, "humidity_abs", "humidityAbsolute", "pair");
|
||||
processType(decObj, "no2", "no2", "pair");
|
||||
processType(decObj, "o3", "o3", "pair");
|
||||
processType(decObj, "oxygen", "o2", "pair");
|
||||
processType(decObj, "pm1", "fineDustConc01", "pair");
|
||||
processType(decObj, "pm2_5", "fineDustConc02_5", "pair");
|
||||
processType(decObj, "pm10", "fineDustConc10", "pair");
|
||||
processType(decObj, "pressure", "pressure", "pair");
|
||||
processType(decObj, "so2", "so2", "pair");
|
||||
processType(decObj, "sound", "sound", "pairDB");
|
||||
processType(decObj, "temperature", "temperature", "pair");
|
||||
// We have two places where the Device ID is delivered: with the measurement data and
|
||||
// with the configuration.
|
||||
// We take the info from the configuration and show it as a property, so we don't need
|
||||
// something like processType(decObj, "DeviceID", "DeviceID", "string") at this moment. We leave
|
||||
// this as a reminder in case for some reason it will be needed in future, e.g. when an air-Q
|
||||
// device also sends data from other devices (then with another Device ID)
|
||||
processType(decObj, "Status", "status", "string");
|
||||
processType(decObj, "TypPS", "avgFineDustSize", "number");
|
||||
processType(decObj, "dCO2dt", "dCO2dt", "number");
|
||||
processType(decObj, "dHdt", "dHdt", "number");
|
||||
processType(decObj, "door_event", "doorEvent", "number");
|
||||
processType(decObj, "health", "healthIndex", "index");
|
||||
processType(decObj, "health", "health", "number");
|
||||
processType(decObj, "measuretime", "measureTime", "number");
|
||||
processType(decObj, "performance", "performanceIndex", "index");
|
||||
processType(decObj, "performance", "performance", "number");
|
||||
processType(decObj, "timestamp", "timestamp", "datetime");
|
||||
processType(decObj, "uptime", "uptime", "numberTimePeriod");
|
||||
processType(decObj, "tvoc", "tvoc", "pairPPB");
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (JsonSyntaxException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Syntax error while parsing response from device: " + e.getMessage());
|
||||
logger.trace("Parse error in response: {}", jsonAnswer);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("air-Q - airqHandler - polldata.run(): Error while retrieving air-Q data: {}", toString());
|
||||
} catch (AirqPasswordIncorrectException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device password incorrect");
|
||||
} catch (AirqException e) {
|
||||
String causeMessage = "";
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null) {
|
||||
causeMessage = cause.getClass().getSimpleName() + ": " + cause.getMessage() + ": ";
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, causeMessage + e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
public void getConfigData() {
|
||||
Result res = null;
|
||||
logger.trace("air-Q - airqHandler - getConfigData(): starting processing data");
|
||||
try {
|
||||
String url = "http://" + config.ipAddress + "/config";
|
||||
res = getData(url, "GET", null);
|
||||
if (res != null) {
|
||||
String jsontext = res.getBody();
|
||||
logger.trace("air-Q - airqHandler - getConfigData(): Result from getBody() is {} with body={}", res,
|
||||
res.getBody());
|
||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||
if (ans != null) {
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
if (jsonAnswer != null) {
|
||||
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
||||
if (decEl != null) {
|
||||
JsonObject decObj = decEl.getAsJsonObject();
|
||||
logger.debug("air-Q - airqHandler - getConfigData(): decObj={}", decObj);
|
||||
processType(decObj, "Wifi", "wifi", "boolean");
|
||||
processType(decObj, "WLANssid", "ssid", "arr");
|
||||
processType(decObj, "pass", "password", "string");
|
||||
processType(decObj, "WifiInfo", "wifiInfo", "boolean");
|
||||
processType(decObj, "TimeServer", "timeServer", "string");
|
||||
processType(decObj, "geopos", "location", "coord");
|
||||
processType(decObj, "NightMode", "", "nightmode");
|
||||
processType(decObj, "devicename", "deviceName", "string");
|
||||
processType(decObj, "RoomType", "roomType", "string");
|
||||
processType(decObj, "logging", "logLevel", "string");
|
||||
processType(decObj, "DeleteKey", "deleteKey", "string");
|
||||
processType(decObj, "FireAlarm", "fireAlarm", "boolean");
|
||||
processType(decObj, "air-Q-Hardware-Version", "hardwareVersion", "property");
|
||||
processType(decObj, "WLAN config", "", "wlan");
|
||||
processType(decObj, "cloudUpload", "cloudUpload", "boolean");
|
||||
processType(decObj, "SecondsMeasurementDelay", "averagingRhythm", "number");
|
||||
processType(decObj, "Rejection", "powerFreqSuppression", "string");
|
||||
processType(decObj, "air-Q-Software-Version", "softwareVersion", "property");
|
||||
processType(decObj, "sensors", "sensorList", "proparr");
|
||||
processType(decObj, "AutoDriftCompensation", "autoDriftCompensation", "boolean");
|
||||
processType(decObj, "AutoUpdate", "autoUpdate", "boolean");
|
||||
processType(decObj, "AdvancedDataProcessing", "advancedDataProcessing", "boolean");
|
||||
processType(decObj, "Industry", "Industry", "property");
|
||||
processType(decObj, "ppm&ppb", "ppm_and_ppb", "boolean");
|
||||
processType(decObj, "GasAlarm", "gasAlarm", "boolean");
|
||||
processType(decObj, "id", "id", "property");
|
||||
processType(decObj, "SoundInfo", "soundPressure", "boolean");
|
||||
processType(decObj, "AlarmForwarding", "alarmForwarding", "boolean");
|
||||
processType(decObj, "usercalib", "userCalib", "calib");
|
||||
processType(decObj, "InitialCalFinished", "initialCalFinished", "boolean");
|
||||
processType(decObj, "Averaging", "averaging", "boolean");
|
||||
processType(decObj, "SensorInfo", "sensorInfo", "property");
|
||||
processType(decObj, "ErrorBars", "errorBars", "boolean");
|
||||
processType(decObj, "warmup-phase", "warmupPhase", "boolean");
|
||||
} else {
|
||||
logger.warn(
|
||||
"air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
|
||||
decEl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
|
||||
ans);
|
||||
}
|
||||
String jsontext = res.getBody();
|
||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||
if (ans == null) {
|
||||
throw new AirqEmptyResonseException();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("air-Q - airqHandler - getConfigData(): Error in processConfigData(): {}", e.toString());
|
||||
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
||||
if (decEl == null) {
|
||||
throw new AirqEmptyResonseException();
|
||||
}
|
||||
|
||||
JsonObject decObj = decEl.getAsJsonObject();
|
||||
processType(decObj, "Wifi", "wifi", "boolean");
|
||||
processType(decObj, "WLANssid", "ssid", "arr");
|
||||
processType(decObj, "pass", "password", "string");
|
||||
processType(decObj, "WifiInfo", "wifiInfo", "boolean");
|
||||
processType(decObj, "TimeServer", "timeServer", "string");
|
||||
processType(decObj, "geopos", "location", "coord");
|
||||
processType(decObj, "NightMode", "", "nightmode");
|
||||
processType(decObj, "devicename", "deviceName", "string");
|
||||
processType(decObj, "RoomType", "roomType", "string");
|
||||
processType(decObj, "logging", "logLevel", "string");
|
||||
processType(decObj, "DeleteKey", "deleteKey", "string");
|
||||
processType(decObj, "FireAlarm", "fireAlarm", "boolean");
|
||||
processType(decObj, "air-Q-Hardware-Version", "hardwareVersion", "property");
|
||||
processType(decObj, "WLAN config", "", "wlan");
|
||||
processType(decObj, "cloudUpload", "cloudUpload", "boolean");
|
||||
processType(decObj, "SecondsMeasurementDelay", "averagingRhythm", "number");
|
||||
processType(decObj, "Rejection", "powerFreqSuppression", "string");
|
||||
processType(decObj, "air-Q-Software-Version", "softwareVersion", "property");
|
||||
processType(decObj, "sensors", "sensorList", "proparr");
|
||||
processType(decObj, "AutoDriftCompensation", "autoDriftCompensation", "boolean");
|
||||
processType(decObj, "AutoUpdate", "autoUpdate", "boolean");
|
||||
processType(decObj, "AdvancedDataProcessing", "advancedDataProcessing", "boolean");
|
||||
processType(decObj, "Industry", "Industry", "property");
|
||||
processType(decObj, "ppm&ppb", "ppm_and_ppb", "boolean");
|
||||
processType(decObj, "GasAlarm", "gasAlarm", "boolean");
|
||||
processType(decObj, "id", "id", "property");
|
||||
processType(decObj, "SoundInfo", "soundPressure", "boolean");
|
||||
processType(decObj, "AlarmForwarding", "alarmForwarding", "boolean");
|
||||
processType(decObj, "usercalib", "userCalib", "calib");
|
||||
processType(decObj, "InitialCalFinished", "initialCalFinished", "boolean");
|
||||
processType(decObj, "Averaging", "averaging", "boolean");
|
||||
processType(decObj, "SensorInfo", "sensorInfo", "property");
|
||||
processType(decObj, "ErrorBars", "errorBars", "boolean");
|
||||
processType(decObj, "warmup-phase", "warmupPhase", "boolean");
|
||||
} catch (AirqException | JsonSyntaxException e) {
|
||||
logger.warn("Failed to retrieve configuration: {}", e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
private void processType(JsonObject dec, String airqName, String channelName, String type) {
|
||||
logger.trace("air-Q - airqHandler - processType(): airqName={}, channelName={}, type={}", airqName, channelName,
|
||||
type);
|
||||
if (dec.get(airqName) == null) {
|
||||
logger.trace("air-Q - airqHandler - processType(): get({}) is null", airqName);
|
||||
// If a device variant does not have a specific sensor type, the value is not present in the JSON data.
|
||||
// Under rare conditions an existing sensor has a null value in the JSON data on a single event.
|
||||
if (dec.get(airqName) == null || dec.get(airqName).isJsonNull()) {
|
||||
updateState(channelName, UnDefType.UNDEF);
|
||||
if (type.contentEquals("pair")) {
|
||||
updateState(channelName + "_maxerr", UnDefType.UNDEF);
|
||||
@ -631,27 +608,29 @@ public class AirqHandler extends BaseThingHandler {
|
||||
updateState(channelName, new QuantityType<>(dec.get(airqName).getAsBigInteger(), Units.SECOND));
|
||||
break;
|
||||
case "pair":
|
||||
ResultPair pair = new ResultPair(dec.get(airqName).toString());
|
||||
ResultPair pair = new ResultPair(dec.get(airqName));
|
||||
updateState(channelName, new DecimalType(pair.getValue()));
|
||||
updateState(channelName + "_maxerr", new DecimalType(pair.getMaxdev()));
|
||||
break;
|
||||
case "pairPPM":
|
||||
ResultPair pairPPM = new ResultPair(dec.get(airqName).toString());
|
||||
ResultPair pairPPM = new ResultPair(dec.get(airqName));
|
||||
updateState(channelName, new QuantityType<>(pairPPM.getValue(), Units.PARTS_PER_MILLION));
|
||||
updateState(channelName + "_maxerr", new DecimalType(pairPPM.getMaxdev()));
|
||||
break;
|
||||
case "pairPPB":
|
||||
ResultPair pairPPB = new ResultPair(dec.get(airqName).toString());
|
||||
ResultPair pairPPB = new ResultPair(dec.get(airqName));
|
||||
updateState(channelName, new QuantityType<>(pairPPB.getValue(), Units.PARTS_PER_BILLION));
|
||||
updateState(channelName + "_maxerr", new DecimalType(pairPPB.getMaxdev()));
|
||||
break;
|
||||
case "pairDB":
|
||||
ResultPair pairDB = new ResultPair(dec.get(airqName).toString());
|
||||
logger.trace("air-Q - airqHandler - processType(): db transmitted as {} with unit {}",
|
||||
pairDB.getValue(), Units.DECIBEL);
|
||||
ResultPair pairDB = new ResultPair(dec.get(airqName));
|
||||
updateState(channelName, new QuantityType<>(pairDB.getValue(), Units.DECIBEL));
|
||||
updateState(channelName + "_maxerr", new DecimalType(pairDB.getMaxdev()));
|
||||
break;
|
||||
case "index":
|
||||
double rawValue = Double.parseDouble(dec.get(airqName).toString());
|
||||
updateState(channelName, new QuantityType<>(rawValue / 10, Units.PERCENT));
|
||||
break;
|
||||
case "datetime":
|
||||
Long timest = Long.valueOf(dec.get(airqName).toString());
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
@ -761,8 +740,6 @@ public class AirqHandler extends BaseThingHandler {
|
||||
arrstr = arrstr + el.getAsString() + ", ";
|
||||
}
|
||||
if (arrstr.length() >= 2) {
|
||||
logger.trace("air-Q - airqHandler - processType(): property array {} set to {}",
|
||||
channelName, arrstr.substring(0, arrstr.length() - 2));
|
||||
getThing().setProperty(channelName, arrstr.substring(0, arrstr.length() - 2));
|
||||
} else {
|
||||
logger.trace("air-Q - airqHandler - processType(): cannot handle this as an array: {}",
|
||||
@ -782,24 +759,25 @@ public class AirqHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
private void changeSettings(JsonObject jsonchange) {
|
||||
String jsoncmd = jsonchange.toString();
|
||||
logger.trace("air-Q - airqHandler - changeSettings(): called with jsoncmd={}", jsoncmd);
|
||||
Result res = null;
|
||||
String url = "http://" + config.ipAddress + "/config";
|
||||
String jsonbody = encrypt(jsoncmd.getBytes(StandardCharsets.UTF_8), config.password);
|
||||
String fullbody = "request=" + jsonbody;
|
||||
logger.trace("air-Q - airqHandler - changeSettings(): doing call to url={}, method=POST, body={}", url,
|
||||
fullbody);
|
||||
res = getData(url, "POST", fullbody);
|
||||
if (res != null) {
|
||||
try {
|
||||
String jsoncmd = jsonchange.toString();
|
||||
Result res;
|
||||
String url = "http://" + config.ipAddress + "/config";
|
||||
String jsonbody = encrypt(jsoncmd.getBytes(StandardCharsets.UTF_8), config.password);
|
||||
String fullbody = "request=" + jsonbody;
|
||||
res = getData(url, "POST", fullbody);
|
||||
JsonElement ans = gson.fromJson(res.getBody(), JsonElement.class);
|
||||
if (ans != null) {
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
logger.trace("air-Q - airqHandler - changeSettings(): call returned {}", jsonAnswer);
|
||||
} else {
|
||||
logger.warn("The air-Q data could not be extracted from this string: {}", ans);
|
||||
|
||||
if (ans == null) {
|
||||
throw new AirqEmptyResonseException();
|
||||
}
|
||||
|
||||
JsonObject jsonObj = ans.getAsJsonObject();
|
||||
decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||
} catch (AirqException e) {
|
||||
logger.warn("Failed to change settings", e);
|
||||
} catch (InterruptedException e) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user