diff --git a/CODEOWNERS b/CODEOWNERS index 1865229aed998..aacebb8efb0c3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -289,6 +289,7 @@ /bundles/org.openhab.binding.omnilink/ @ecdye /bundles/org.openhab.binding.ondilo/ @MikeTheTux /bundles/org.openhab.binding.onebusaway/ @sdwilsh +/bundles/org.openhab.binding.onecta/ @adr001db /bundles/org.openhab.binding.onewire/ @J-N-K /bundles/org.openhab.binding.onewiregpio/ @aogorek /bundles/org.openhab.binding.onkyo/ @pail23 @paulianttila diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 1ce231440bb18..4bfe07a3927b7 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1431,6 +1431,11 @@ org.openhab.binding.onewiregpio ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.onecta + ${project.version} + org.openhab.addons.bundles org.openhab.binding.onkyo diff --git a/bundles/org.openhab.binding.onecta/NOTICE b/bundles/org.openhab.binding.onecta/NOTICE new file mode 100644 index 0000000000000..f8cd7cc4f5ed9 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/NOTICE @@ -0,0 +1,25 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +== Third-party Content + +jackson +* License: Apache 2.0 License +* Project: https://github.com/FasterXML/jackson +* Source: https://github.com/FasterXML/jackson + +gson +* License: Apache 2.0 License +* Project: https://github.com/google/gson +* Source: https://github.com/google/gson \ No newline at end of file diff --git a/bundles/org.openhab.binding.onecta/README.md b/bundles/org.openhab.binding.onecta/README.md new file mode 100644 index 0000000000000..9a2fc5125eb74 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/README.md @@ -0,0 +1,365 @@ +# Onecta Binding + +This binding allows you to control Daikin units connected to Onecta. +Onecta is a Daikin cloud platform that allows user's to control their Daikin units via the internet. +The Daikin Onecta app can be used to control the units and to register the units in the Daikin Onecta cloud. +After the initial setup in de Daikin app, the binding will recognize all units connected to the Daikin cloud and create the corresponding things. + +## Supported Things + +Basically all devices connected to Daikin Onecta cloud could be connected with the binding. + +- `account`: Acts as bridge and ensures the connection to Onecta cloud and the recognition of connected units +- `climate-control`: The unit itself. With this items like climate can controled. +- `gateway`: The network controller of the unit. +- `IndoorUnit`: Gives information about the indoor unit. +- `domestic-hot-water-tank`: Is the thing to control the Hot water tank. + +## Discovery + +Please take the following steps prior to using the binding. +Create a Onecta cloud account in the Onecta app for [Android](https://play.google.com/store/apps/details?id=com.daikineurope.online.controller&hl=en_US) or [iOS](https://apps.apple.com/de/app/onecta/id1474811586?l=en) (if not already done). +Afterwards, pair your Daikin units in the Onecta App. + +There is no auto discovery for the Onecta cloud account. +The account is paired using OAuth2 with your Onecta login and the developer credentials obtained from the Onecta Developer Portal. +To pair the account go to the binding's configuration UI at https:///onecta. For a standard openHABian Pi installation the address is https://openhabianpi:8443/onecta or https://pi-adres:8443/onecta. +Note that your browser will file a warning that the certificate is self-signed. +This is fine and you can safely continue. +It is NOT possible to use an unsecured connection (http://) for pairing.
+Allowed pairing formats are: +- https:///onecta +- https://:/onecta (standaard port 8443) +- https://home.myopenhab.org/onecta +- https://openhabianpi:8443/onecta + +It is **not** allowed to use localhost (Onecta will not accept this) +- https://localhost:8443 + +Once a Onecta account is paired, all supported appliances are automatically discovered as individual things and placed in the inbox. +They can then be paired with your favorite management UI. +As an alternative, the binding configuration UI provides a things-file template per paired account that can be used to pair the appliances. + +For a detailed walk through the account configuration, see [Account Configuration Example](#account-configuration-example). + +## Account Bridge Configuration + +| Name | Type | Description | Default | Required | Advanced | +|------------------|---------|----------------------------------------------------------------|---------|----------|----------| +| `refreshInterval` | integer | Interval the device is polled in sec. | 600 | yes | no | + + +## Discovered Things Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|----------------------------------------------------------------------------------------------------------|---------|----------|----------| +| `unitID` | text | UID Unique Identifier.
If this thing is created in a thing-file this UID can be found in the logging | N/A | yes | no | +| `refreshDelay` | integer | Refresh Delay in sec.
Only available for thing 'device' and 'domestic-hot-water-tank' | 15 | yes | no | + +### Explanation Refresh Delay: +If a command is sent from the binding to OnectaCloud, it needs time to be processed by Daikin. +This can cause items to flip-flop.
+For example: You switch a Unit 'On' with the binding. +Daikin will process this command and control the unit, this processing can take 15 seconds. +During this time, the binding may have requested a data refresh from OnectaCloud. +If this 'On' command has not yet been processed by Daikin, this will result in the OH item returning to 'Off'. +After a while, when Daikin has processed it and another data refresh is performed by the binding, the OH item will return to 'On'.
+The Refresh Delay prevents an item from being refreshed (for x seconds) after a command has been issued from this item. +Other items will be updated during this time with a data refresh. + + + +## Full Example + +### Thing Configuration + +```java +Bridge onecta:account:bridge "Daikin Onecta Bridge" [refreshInterval=600] { + Thing climate-control livingRoom "Onecta living room Unit" [unitID="80100dc5-a289-47c1-bbdb-****************", refreshDelay=15] + Thing gateway livingRoom "Onecta living room Gateway" [unitID="80100dc5-a289-47c1-bbdb-****************", refreshDelay=15] + Thing domestic-hot-water-tank livingRoom "Onecta living room Watertank" [unitID="80100dc5-a289-47c1-bbdb-****************", refreshDelay=15] + Thing indoor-unit livingRoom "Onecta Woonkamer living room" [unitID="80100dc5-a289-47c1-bbdb-****************", refreshDelay=15] + } +``` + +### Item Configuration climate-control + +```java +Switch Power "Power for the AC unit" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#power"} +Number:Temperature SetPoint "SetTemp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#settemp"} +Number:Temperature SetPointMin "SetTempMin [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#settemp-min", readOnly="true"} +Number:Temperature SetPointMax "SetTempMax [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#settemp-max", readOnly="true"} +Number:Temperature SetPointStep "SetTempStep [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#settemp-step", readOnly="true"} +Number:Temperature IndoorTemp "Indoor temp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#indoor-temp", readOnly="true"} +Number:Temperature OutdoorTemp "Outdoor temp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#outdoor-temp" , readOnly="true"} +Number:Temperature LeavingWaterTemp "LeavingWaterTemp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#leaving-water-temp"} +Number:Dimensionless Humidity "The indoor humidity" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#humidity"} + +String OperationMode "Operation mode" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#operation-mode"} +String FanSpeed "Fan Speed" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#fan-speed"} +String FanDirHor "Fan Swing Horizontal" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#fan-dir-hor"} +String FanDirVer "Fan Swing Vertical" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#fan-dir-ver"} +String FanDir "Fan Direction" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#fan-dir"} + +Switch Ecomode "Ecomode" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#econo-mode"} +Switch Streamer "Streamer mode" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#streamer"} +Switch Powerful "Powerful mode" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#powerful-mode"} +Switch Holiday "Holiday mode" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#holiday-mode", readOnly="true"} + +Number:Temperature TargetTemp "TargetTemp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#target-temp"} +Number:Temperature TargetTempMin "TargetTempMin [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#target-temp-min", readOnly="true"} +Number:Temperature TargetTempMax "TargetTempMax [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#target-temp-max", readOnly="true"} +Number:Temperature TargetTempStep "TargetTempStep [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#target-temp-step", readOnly="true"} + +Number:Temperature SetPointLeavingWaterOffset "SetPointLeavingWaterOffset [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#set-leaving-water-offset", readOnly="true"} +Number:Temperature SetPointLeavingWaterTemp "SetPointLeavingWaterTemp [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#set-leaving-water-temp", readOnly="true"} + +Number:Temperature TargetTempStep "TargetTempStep [%.1f °C]" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#target-temp-step", readOnly="true"} +DateTime TimeStamp "Time stamp" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:basic#timestamp"} + +String DemandControl "Demand Controle" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:demandcontrol#demand-control"} +Number DemandControlFixedValue "Demand Control (fixed)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:demandcontrol#demand-control-fixed-value"} +Number DemandControlFixedMinValue "Demand Control fixed Min value" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:demandcontrol#demand-control-fixed-min-value"} +Number DemandControlFixedMaxValue "Demand Control fixed Max value" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:demandcontrol#demand-control-fixed-max-value"} +Number DemandControlFixedStepValue "Demand Control fixed step value" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:demandcontrol#demand-control-fixed-step-value"} + +Number:Energy EnergyCoolingCurrentYear "The energy usage for cooling current year total" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-current-year"} +Number:Energy EnergyCoolingCurrentDay "The energy usage for cooling current day total" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-current-day"} +Number:Energy EnergyHeatingCurrentYear "The energy usage for heating current year total" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-current-year"} +Number:Energy EnergyHeatingCurrentDay "The energy usage for heating current day total" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-current-day"} + +Number:Energy EnergyCoolingDay0 "Energy Cooling yesterday (00:00 - 02:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-0"} +Number:Energy EnergyCoolingDay1 "Energy Cooling yesterday (02:00 - 04:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-1"} +Number:Energy EnergyCoolingDay2 "Energy Cooling yesterday (04:00 - 06:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-2"} +Number:Energy EnergyCoolingDay3 "Energy Cooling yesterday (06:00 - 08:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-3"} +Number:Energy EnergyCoolingDay4 "Energy Cooling yesterday (08:00 - 10:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-4"} +Number:Energy EnergyCoolingDay5 "Energy Cooling yesterday (10:00 - 12:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-5"} +Number:Energy EnergyCoolingDay6 "Energy Cooling yesterday (12:00 - 14:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-6"} +Number:Energy EnergyCoolingDay7 "Energy Cooling yesterday (14:00 - 16:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-7"} +Number:Energy EnergyCoolingDay8 "Energy Cooling yesterday (16:00 - 18:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-8"} +Number:Energy EnergyCoolingDay9 "Energy Cooling yesterday (18:00 - 20:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-9"} +Number:Energy EnergyCoolingDay10 "Energy Cooling yesterday (20:00 - 22:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-10"} +Number:Energy EnergyCoolingDay11 "Energy Cooling yesterday (22:00 - 24:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-11"} +Number:Energy EnergyCoolingDay12 "Energy Cooling today (00:00 - 02:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-12"} +Number:Energy EnergyCoolingDay13 "Energy Cooling today (02:00 - 04:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-13"} +Number:Energy EnergyCoolingDay14 "Energy Cooling today (04:00 - 06:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-14"} +Number:Energy EnergyCoolingDay15 "Energy Cooling today (06:00 - 08:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-15"} +Number:Energy EnergyCoolingDay16 "Energy Cooling today (08:00 - 10:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-16"} +Number:Energy EnergyCoolingDay17 "Energy Cooling today (10:00 - 12:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-17"} +Number:Energy EnergyCoolingDay18 "Energy Cooling today (12:00 - 14:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-18"} +Number:Energy EnergyCoolingDay19 "Energy Cooling today (14:00 - 16:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-19"} +Number:Energy EnergyCoolingDay20 "Energy Cooling today (16:00 - 18:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-20"} +Number:Energy EnergyCoolingDay21 "Energy Cooling today (18:00 - 20:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-21"} +Number:Energy EnergyCoolingDay22 "Energy Cooling today (20:00 - 22:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-22"} +Number:Energy EnergyCoolingDay23 "Energy Cooling today (22:00 - 24:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-day-23"} + +Number:Energy EnergyCoolingWeek0 "Energy Cooling last week Monday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-0"} +Number:Energy EnergyCoolingWeek1 "Energy Cooling last week Tuesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-1"} +Number:Energy EnergyCoolingWeek2 "Energy Cooling last week Wednesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-2"} +Number:Energy EnergyCoolingWeek3 "Energy Cooling last week Thursday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-3"} +Number:Energy EnergyCoolingWeek4 "Energy Cooling last week Friday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-4"} +Number:Energy EnergyCoolingWeek5 "Energy Cooling last week Saturday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-5"} +Number:Energy EnergyCoolingWeek6 "Energy Cooling last week Sunday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-6"} +Number:Energy EnergyCoolingWeek7 "Energy Cooling current week Monday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-7"} +Number:Energy EnergyCoolingWeek8 "Energy Cooling current week Tuesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-8"} +Number:Energy EnergyCoolingWeek9 "Energy Cooling current week Wednesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-9"} +Number:Energy EnergyCoolingWeek10 "Energy Cooling current week Thursday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-10"} +Number:Energy EnergyCoolingWeek11 "Energy Cooling current week Friday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-11"} +Number:Energy EnergyCoolingWeek12 "Energy Cooling current week Saturday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-12"} +Number:Energy EnergyCoolingWeek13 "Energy Cooling current week Sunday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-week-13"} + +Number:Energy EnergyCoolingMonth0 "Energy Cooling last year January" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-0"} +Number:Energy EnergyCoolingMonth1 "Energy Cooling last year February" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-1"} +Number:Energy EnergyCoolingMonth2 "Energy Cooling last year March" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-2"} +Number:Energy EnergyCoolingMonth3 "Energy Cooling last year April" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-3"} +Number:Energy EnergyCoolingMonth4 "Energy Cooling last year May" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-4"} +Number:Energy EnergyCoolingMonth5 "Energy Cooling last year June" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-5"} +Number:Energy EnergyCoolingMonth6 "Energy Cooling last year July" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-6"} +Number:Energy EnergyCoolingMonth7 "Energy Cooling last year August" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-7"} +Number:Energy EnergyCoolingMonth8 "Energy Cooling last year September" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-8"} +Number:Energy EnergyCoolingMonth9 "Energy Cooling last year October" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-9"} +Number:Energy EnergyCoolingMonth10 "Energy Cooling last year November" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-10"} +Number:Energy EnergyCoolingMonth11 "Energy Cooling last year December" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-11"} +Number:Energy EnergyCoolingMonth12 "Energy Cooling current year January" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-12"} +Number:Energy EnergyCoolingMonth13 "Energy Cooling current year February" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-13"} +Number:Energy EnergyCoolingMonth14 "Energy Cooling current year March" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-14"} +Number:Energy EnergyCoolingMonth15 "Energy Cooling current year April" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-15"} +Number:Energy EnergyCoolingMonth16 "Energy Cooling current year May" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-16"} +Number:Energy EnergyCoolingMonth17 "Energy Cooling current year June" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-17"} +Number:Energy EnergyCoolingMonth18 "Energy Cooling current year July" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-18"} +Number:Energy EnergyCoolingMonth19 "Energy Cooling current year August" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-19"} +Number:Energy EnergyCoolingMonth20 "Energy Cooling current year September" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-20"} +Number:Energy EnergyCoolingMonth21 "Energy Cooling current year October" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-21"} +Number:Energy EnergyCoolingMonth22 "Energy Cooling current year November" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-22"} +Number:Energy EnergyCoolingMonth23 "Energy Cooling current year December" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-cooling#energy-cooling-month-23"} + +Number:Energy EnergyHeatingDay0 "Energy Heating yesterday (00:00 - 02:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-0"} +Number:Energy EnergyHeatingDay1 "Energy Heating yesterday (02:00 - 04:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-1"} +Number:Energy EnergyHeatingDay2 "Energy Heating yesterday (04:00 - 06:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-2"} +Number:Energy EnergyHeatingDay3 "Energy Heating yesterday (06:00 - 08:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-3"} +Number:Energy EnergyHeatingDay4 "Energy Heating yesterday (08:00 - 10:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-4"} +Number:Energy EnergyHeatingDay5 "Energy Heating yesterday (10:00 - 12:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-5"} +Number:Energy EnergyHeatingDay6 "Energy Heating yesterday (12:00 - 14:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-6"} +Number:Energy EnergyHeatingDay7 "Energy Heating yesterday (14:00 - 16:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-7"} +Number:Energy EnergyHeatingDay8 "Energy Heating yesterday (16:00 - 18:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-8"} +Number:Energy EnergyHeatingDay9 "Energy Heating yesterday (18:00 - 20:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-9"} +Number:Energy EnergyHeatingDay10 "Energy Heating yesterday (20:00 - 22:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-10"} +Number:Energy EnergyHeatingDay11 "Energy Heating yesterday (22:00 - 24:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-11"} +Number:Energy EnergyHeatingDay12 "Energy Heating today (00:00 - 02:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-12"} +Number:Energy EnergyHeatingDay13 "Energy Heating today (02:00 - 04:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-13"} +Number:Energy EnergyHeatingDay14 "Energy Heating today (04:00 - 06:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-14"} +Number:Energy EnergyHeatingDay15 "Energy Heating today (06:00 - 08:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-15"} +Number:Energy EnergyHeatingDay16 "Energy Heating today (08:00 - 10:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-16"} +Number:Energy EnergyHeatingDay17 "Energy Heating today (10:00 - 12:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-17"} +Number:Energy EnergyHeatingDay18 "Energy Heating today (12:00 - 14:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-18"} +Number:Energy EnergyHeatingDay19 "Energy Heating today (14:00 - 16:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-19"} +Number:Energy EnergyHeatingDay20 "Energy Heating today (16:00 - 18:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-20"} +Number:Energy EnergyHeatingDay21 "Energy Heating today (18:00 - 20:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-21"} +Number:Energy EnergyHeatingDay22 "Energy Heating today (20:00 - 22:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-22"} +Number:Energy EnergyHeatingDay23 "Energy Heating today (22:00 - 24:00)" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-day-23"} + +Number:Energy EnergyHeatingWeek0 "Energy Heating last week Monday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-0"} +Number:Energy EnergyHeatingWeek1 "Energy Heating last week Tuesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-1"} +Number:Energy EnergyHeatingWeek2 "Energy Heating last week Wednesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-2"} +Number:Energy EnergyHeatingWeek3 "Energy Heating last week Thursday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-3"} +Number:Energy EnergyHeatingWeek4 "Energy Heating last week Friday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-4"} +Number:Energy EnergyHeatingWeek5 "Energy Heating last week Saturday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-5"} +Number:Energy EnergyHeatingWeek6 "Energy Heating last week Sunday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-6"} +Number:Energy EnergyHeatingWeek7 "Energy Heating current week Monday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-7"} +Number:Energy EnergyHeatingWeek8 "Energy Heating current week Tuesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-8"} +Number:Energy EnergyHeatingWeek9 "Energy Heating current week Wednesday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-9"} +Number:Energy EnergyHeatingWeek10 "Energy Heating current week Thursday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-10"} +Number:Energy EnergyHeatingWeek11 "Energy Heating current week Friday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-11"} +Number:Energy EnergyHeatingWeek12 "Energy Heating current week Saturday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-12"} +Number:Energy EnergyHeatingWeek13 "Energy Heating current week Sunday" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-week-13"} + +Number:Energy EnergyHeatingMonth0 "Energy Heating last year January" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-0"} +Number:Energy EnergyHeatingMonth1 "Energy Heating last year February" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-1"} +Number:Energy EnergyHeatingMonth2 "Energy Heating last year March" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-2"} +Number:Energy EnergyHeatingMonth3 "Energy Heating last year April" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-3"} +Number:Energy EnergyHeatingMonth4 "Energy Heating last year May" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-4"} +Number:Energy EnergyHeatingMonth5 "Energy Heating last year June" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-5"} +Number:Energy EnergyHeatingMonth6 "Energy Heating last year July" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-6"} +Number:Energy EnergyHeatingMonth7 "Energy Heating last year August" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-7"} +Number:Energy EnergyHeatingMonth8 "Energy Heating last year September" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-8"} +Number:Energy EnergyHeatingMonth9 "Energy Heating last year October" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-9"} +Number:Energy EnergyHeatingMonth10 "Energy Heating last year November" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-10"} +Number:Energy EnergyHeatingMonth11 "Energy Heating last year December" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-11"} +Number:Energy EnergyHeatingMonth12 "Energy Heating current year January" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-12"} +Number:Energy EnergyHeatingMonth13 "Energy Heating current year February" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-13"} +Number:Energy EnergyHeatingMonth14 "Energy Heating current year March" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-14"} +Number:Energy EnergyHeatingMonth15 "Energy Heating current year April" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-15"} +Number:Energy EnergyHeatingMonth16 "Energy Heating current year May" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-16"} +Number:Energy EnergyHeatingMonth17 "Energy Heating current year June" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-17"} +Number:Energy EnergyHeatingMonth18 "Energy Heating current year July" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-18"} +Number:Energy EnergyHeatingMonth19 "Energy Heating current year August" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-19"} +Number:Energy EnergyHeatingMonth20 "Energy Heating current year September" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-20"} +Number:Energy EnergyHeatingMonth21 "Energy Heating current year October" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-21"} +Number:Energy EnergyHeatingMonth22 "Energy Heating current year November" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-22"} +Number:Energy EnergyHeatingMonth23 "Energy Heating current year December" ["Point"] {channel="onecta:climate-control:bridge:livingRoom:consumption-data-heating#energy-heating-month-23"} +``` + +### Item Configuration gateway + +```java +Switch DaylightSavingTime "Daylight saving time enabled" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#daylight-savingtime-enabled", readOnly="true"} +Switch FirmwareUpdateSupported "Is firmware update supported" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#is-firmware-update-supported", readOnly="true"} +String FirmwaweVersion "firmware version" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#firmware-version", readOnly="true"} +Switch IsInErrorState "Is in error state" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#is-in-error-state", readOnly="true"} +Switch LedEnabled "Led enabled " ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#led-enabled", readOnly="true"} +String RegionCode "Region code" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#region-code", readOnly="true"} +String Serialnumber "Serialnumber" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#serial-number" , readOnly="true"} +String SSID "SSID " ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#ssid", readOnly="true"} +String TimeZone "Time zone" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#timezone", readOnly="true"} +String WiFiConnectionSSID "WiFi Connection SSID" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#wifi-connection-ssid", readOnly="true"} +Number WifiConnectionStrength "Wifi connection strength" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#wifi-connection-power", readOnly="true"} +String ModelInfo "Model info" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#model-info", readOnly="true"} +String IPAddress "IP Address" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#ip-address", readOnly="true"} +String MacAddress "MAC Address" ["Point"] {channel="onecta:gateway:bridge:livingRoom:basic#mac-address", readOnly="true"} +``` +### Item Configuration domestic-hot-water-tank + +```java +Switch Power "Power for the AC unit" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#power"} +Switch IsInErrorState "Is in error state" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#isinerrorstate", readOnly="true"} +Switch IsInWarningState "Is in warning state" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#isinwarningstate", readOnly="true"} +Switch IsInInstallerState "Is in installer state" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#isininstallerstate", readOnly="true"} +Switch IsInEmergencyState "Is in imergency state" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#isinemergencystate", readOnly="true"} +Switch IsHolidayModeActive "Is holiday mode active" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#isholidaymodeactive", readOnly="true"} +Switch Powerfulmode "Powerful mode" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#powerfulmode", readOnly="true"} +String HeatupMode "Heatup mode" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#heatupmode", readOnly="true"} + +String ErrorCode "Error code" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#errorcode", readOnly="true"} +String OperationMode "Operation mode" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#operationmode"} +String SetPointMode "Setpoint mode" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#setpointmode"} + +Number:Temperature SetPoint "SetTemp [%.1f °C]" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#settemp"} +Number:Temperature SetPointMin "SetTempMin [%.1f °C]" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#settempmin", readOnly="true"} +Number:Temperature SetPointMax "SetTempMax [%.1f °C]" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#settempmax", readOnly="true"} +Number:Temperature SetPointStep "SetTempStep [%.1f °C]" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#settempstep", readOnly="true"} + +Number:Temperature TankTemperature "Tank temperature [%.1f °C]" ["Point"] {channel="onecta:domestic-hot-water-tank:bridge:livingroom:basic#tanktemperature" , readOnly="true"} +``` + +### Item Configuration indoor-unit + +```java +String ModelInformation "Model information" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#modelinfo", readOnly="true"} +String SoftwareVersion "Software version" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#softwareversion", readOnly="true"} +String EepromVersion "Eeprom version" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#eepromversion", readOnly="true"} + +Switch DrykeepSetting "Dry keep setting" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#isdrykeepsetting", readOnly="true"} +Number:Temperature DeltaDvalue "DeltaD temperature [%.1f °C]" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#deltadvalue"} +Number:Frequency FanMotorratationSpeed "Fanmotor rotation speed [%.1f RPM]" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#fanmotorratationspeed", readOnly="true"} + +Number:Temperature HeatExchangerTemp "Heatexchanger temperature [%.1f °C]" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#heatexchangertemp", readOnly="true"} +Number:Temperature SuctionTemp "Suction temperature [%.1f °C]" ["Point"] {channel="onecta:indoor-unit:bridge:livingroom:basic#suctiontemp", readOnly="true"} +``` + + +## Account Configuration Example + +The best way to perform the following steps is in an incognito browser to prevent historical settings and accounts from causing problems. + +The configuration UI is accessible at `https:///onecta`. +See [Discovery](#discovery) for a detailed description of how to open the configuration UI in a browser. + +When first opening the configuration UI no account will be paired. + +![Empty Account Overview](doc/CloudBindingConfigEmpty.png) + +We strongly recommend to use a secure connection for pairing, details on this topic can also be found in the [Discovery](#discovery) section. +Click `Pair Account` to start the pairing process. +If not already done, go to the [Daikin Developer Portal](https://developer.cloud.daikineurope.com/login), register there and wait for the confirmation e-mail. +Obtain your client ID and client secret according to the instructions presented there. (**Open the Daikin Developer portal in a incognito browser.**) + +Once you obtained your client ID and client secret continue pairing by filling in your client ID, client secret and Redirect URI. +
**Important** : +- The redirect URI must be set to `https:///onecta/result`. +- It has to be https, otherwise the pairing will fail. +- Localhost is not supported, you have to use the actual hostname of your openHAB installation. +![img.png](doc/CloudBindingSettings.png) + + + +A click on `Pair Account` will take you to the Daikin cloud service login form where you need to log in with the same account as you used for the Onecta@mobile app. + +![Onecta Login Form](doc/onecta-login.png) +When this is the first time you pair an account, you will need to allow openHAB to access your account. +![Onecta Permissions](doc/onecta-permision.png) + +When everything worked, you are presented with a page stating that pairing was successful. +Select the locale which should be used to display localized texts in openHAB channels. +From here, you have two options: +Either let the binding automatically configure a bridge instance or copy the presented things-file template to a things-file and return to the overview page. + +![Pairing Successful](doc/pairing-success.png) +Once the bridge instance is `ONLINE`, you can either pair things for all appliances via your favorite management UI or use a things-file. +The account overview provides a things-file template that is shown when you expand the account. +This can serve as a starting point for your own things-file. +In the properties of the Onecta account thing, you can also find the available units and their details of your system. +Sometimes you need to Disable and then re-Enable the account thing to see the updated list of units. + +![Account Overview With Bridge](doc/account-overview-with-bridge.png) + + diff --git a/bundles/org.openhab.binding.onecta/doc/CloudBindingConfigEmpty.png b/bundles/org.openhab.binding.onecta/doc/CloudBindingConfigEmpty.png new file mode 100644 index 0000000000000..555792e0bc6d4 Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/CloudBindingConfigEmpty.png differ diff --git a/bundles/org.openhab.binding.onecta/doc/CloudBindingSettings.png b/bundles/org.openhab.binding.onecta/doc/CloudBindingSettings.png new file mode 100644 index 0000000000000..d0c4c8ab19943 Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/CloudBindingSettings.png differ diff --git a/bundles/org.openhab.binding.onecta/doc/account-overview-with-bridge.png b/bundles/org.openhab.binding.onecta/doc/account-overview-with-bridge.png new file mode 100644 index 0000000000000..c6cedf4285309 Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/account-overview-with-bridge.png differ diff --git a/bundles/org.openhab.binding.onecta/doc/onecta-login.png b/bundles/org.openhab.binding.onecta/doc/onecta-login.png new file mode 100644 index 0000000000000..d7051143c6c9e Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/onecta-login.png differ diff --git a/bundles/org.openhab.binding.onecta/doc/onecta-permision.png b/bundles/org.openhab.binding.onecta/doc/onecta-permision.png new file mode 100644 index 0000000000000..cf420bef9a87d Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/onecta-permision.png differ diff --git a/bundles/org.openhab.binding.onecta/doc/pairing-success.png b/bundles/org.openhab.binding.onecta/doc/pairing-success.png new file mode 100644 index 0000000000000..b3c241605fb82 Binary files /dev/null and b/bundles/org.openhab.binding.onecta/doc/pairing-success.png differ diff --git a/bundles/org.openhab.binding.onecta/pom.xml b/bundles/org.openhab.binding.onecta/pom.xml new file mode 100644 index 0000000000000..4e561a18a0813 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.1.0-SNAPSHOT + + + org.openhab.binding.onecta + + openHAB Add-ons :: Bundles :: Onecta Binding + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.google.code.gson + gson + ${gson.version} + + + diff --git a/bundles/org.openhab.binding.onecta/src/main/feature/feature.xml b/bundles/org.openhab.binding.onecta/src/main/feature/feature.xml new file mode 100644 index 0000000000000..8180465b6f91b --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.onecta/${project.version} + + diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeConstants.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeConstants.java new file mode 100644 index 0000000000000..66807bb533658 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeConstants.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link OnectaBridgeConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaBridgeConstants { + + private static final String BINDING_ID = "onecta"; + public static final String BRIDGE = "account"; + // List of all Device Types + public static final String DEVICE_TYPE_GATEWAY = "gateway"; + public static final String DEVICE_TYPE_CLIMATECONTROL = "climate-control"; + public static final String DEVICE_TYPE_WATERTANK = "domestic-hot-water-tank"; + public static final String DEVICE_TYPE_INDOORUNIT = "indoor-unit"; + + // List of config parameters + public static final String CONFIG_PAR_REFRESHINTERVAL = "refreshInterval"; + public static final String CONFIG_PAR_UNITID = "unitID"; + public static final String CHANNEL_OPENHAB_HOST = "openhabHost"; + + // List of all Bridge Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE); + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_CLIMATECONTROL = new ThingTypeUID(BINDING_ID, + DEVICE_TYPE_CLIMATECONTROL); + public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, DEVICE_TYPE_GATEWAY); + public static final ThingTypeUID THING_TYPE_WATERTANK = new ThingTypeUID(BINDING_ID, DEVICE_TYPE_WATERTANK); + public static final ThingTypeUID THING_TYPE_INDOORUNIT = new ThingTypeUID(BINDING_ID, DEVICE_TYPE_INDOORUNIT); + + // The supported thing types. + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE, THING_TYPE_CLIMATECONTROL, + THING_TYPE_WATERTANK, THING_TYPE_GATEWAY, THING_TYPE_INDOORUNIT); + + public static final String THIRD_PARTY_ENDPOINTS_BASENAME = "https://idp.onecta.daikineurope.com/v1/oidc"; + public static final String OAUTH2_SERVICE_HANDLE = BINDING_ID + ":" + BRIDGE; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeHandlerFactory.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeHandlerFactory.java new file mode 100644 index 0000000000000..0d2b57922f618 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaBridgeHandlerFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import static org.openhab.binding.onecta.internal.OnectaBridgeConstants.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.handler.*; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthTokenRefresher; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link OnectaBridgeHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.onecta", service = ThingHandlerFactory.class) +public class OnectaBridgeHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, + THING_TYPE_CLIMATECONTROL, THING_TYPE_GATEWAY, THING_TYPE_WATERTANK, THING_TYPE_INDOORUNIT); + + private Map> discoveryServiceRegs = new HashMap<>(); + private final OnectaTranslationProvider translation; + + @Activate + public OnectaBridgeHandlerFactory(@Reference HttpClientFactory httpClientFactory, + @Reference OAuthTokenRefresher openHabOAuthTokenRefresher, + @Reference OnectaTranslationProvider translation) { + this.translation = translation; + OnectaConfiguration.setTranslation(translation); + OnectaConfiguration.setHttpClientFactory(httpClientFactory); + OnectaConfiguration.setOAuthTokenRefresher(openHabOAuthTokenRefresher); + OnectaConfiguration.getOnectaConnectionClient().openConnecttion(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals((THING_TYPE_BRIDGE))) { + OnectaBridgeHandler bridgeHandler = new OnectaBridgeHandler((Bridge) thing); + OnectaConfiguration.setBridgeThing((Bridge) thing); + return bridgeHandler; + } else if (thingTypeUID.equals(THING_TYPE_CLIMATECONTROL)) { + return new OnectaDeviceHandler(thing); + } else if (thingTypeUID.equals((THING_TYPE_GATEWAY))) { + return new OnectaGatewayHandler(thing); + } else if (thingTypeUID.equals((THING_TYPE_WATERTANK))) { + return new OnectaWaterTankHandler(thing); + } else if (thingTypeUID.equals((THING_TYPE_INDOORUNIT))) { + return new OnectaIndoorUnitHandler(thing); + } + return null; + } + + @Override + protected void removeHandler(ThingHandler handler) { + if (handler.getThing().getThingTypeUID().equals(THING_TYPE_BRIDGE)) { + ServiceRegistration serviceReg = this.discoveryServiceRegs.get(handler.getThing().getUID()); + if (serviceReg != null) { + serviceReg.unregister(); + discoveryServiceRegs.remove(handler.getThing().getUID()); + } + } + super.removeHandler(handler); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaClimateControlConstants.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaClimateControlConstants.java new file mode 100644 index 0000000000000..75bdad47ab8e0 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaClimateControlConstants.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link OnectaClimateControlConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaClimateControlConstants { + + // List of all Channel ids + public static final String CHANNEL_AC_TEMP = "basic#settemp"; + public static final String CHANNEL_AC_TEMPMIN = "basic#settemp-min"; + public static final String CHANNEL_AC_TEMPMAX = "basic#settemp-max"; + public static final String CHANNEL_AC_TEMPSTEP = "basic#settemp-step"; + public static final String CHANNEL_AC_TARGETTEMP = "basic#target-temp"; + public static final String CHANNEL_AC_TARGETTEMPMIN = "basic#target-temp-min"; + public static final String CHANNEL_AC_TARGETTEMPMAX = "basic#target-temp-max"; + public static final String CHANNEL_AC_TARGETTEMPSTEP = "basic#target-temp-step"; + public static final String CHANNEL_INDOOR_TEMP = "basic#indoor-temp"; + public static final String CHANNEL_LEAVINGWATER_TEMP = "basic#leaving-water-temp"; + public static final String CHANNEL_OUTDOOR_TEMP = "basic#outdoor-temp"; + public static final String CHANNEL_INDOOR_HUMIDITY = "basic#humidity"; + public static final String CHANNEL_AC_POWER = "basic#power"; + public static final String CHANNEL_AC_POWERFULMODE = "basic#powerful-mode"; + public static final String CHANNEL_AC_RAWDATA = "extra#rawdata"; + public static final String CHANNEL_AC_OPERATIONMODE = "basic#operation-mode"; + public static final String CHANNEL_AC_NAME = "basic#name"; + public static final String CHANNEL_AC_FANSPEED = "basic#fan-speed"; + public static final String CHANNEL_AC_FANMOVEMENT_HOR = "basic#fan-dir-hor"; + public static final String CHANNEL_AC_FANMOVEMENT_VER = "basic#fan-dir-ver"; + public static final String CHANNEL_AC_FANMOVEMENT = "basic#fan-dir"; + public static final String CHANNEL_AC_ECONOMODE = "basic#econo-mode"; + public static final String CHANNEL_AC_STREAMER = "basic#streamer"; + public static final String CHANNEL_AC_HOLIDAYMODE = "basic#holiday-mode"; + public static final String CHANNEL_AC_SETPOINT_LEAVINGWATER_OFFSET = "basic#set-leaving-water-offset"; + public static final String CHANNEL_AC_SETPOINT_LEAVINGWATER_TEMP = "basic#set-leaving-water-temp"; + public static final String CHANNEL_AC_TIMESTAMP = "basic#timestamp"; + public static final String CHANNEL_AC_DEMANDCONTROL = "demandcontrol#demand-control"; + public static final String CHANNEL_AC_DEMANDCONTROLFIXEDVALUE = "demandcontrol#demand-control-fixed-value"; + public static final String CHANNEL_AC_DEMANDCONTROLFIXEDSTEPVALUE = "demandcontrol#demand-control-fixed-step-value"; + public static final String CHANNEL_AC_DEMANDCONTROLFIXEDMINVALUE = "demandcontrol#demand-control-fixed-min-value"; + public static final String CHANNEL_AC_DEMANDCONTROLFIXEDMAXVALUE = "demandcontrol#demand-control-fixed-max-value"; + public static final String CHANNEL_AC_ENERGY_COOLING_DAY = "consumption-data-cooling#energy-cooling-day-%s"; + public static final String CHANNEL_AC_ENERGY_COOLING_WEEK = "consumption-data-cooling#energy-cooling-week-%s"; + public static final String CHANNEL_AC_ENERGY_COOLING_MONTH = "consumption-data-cooling#energy-cooling-month-%s"; + public static final String CHANNEL_AC_ENERGY_HEATING_DAY = "consumption-data-heating#energy-heating-day-%s"; + public static final String CHANNEL_AC_ENERGY_HEATING_WEEK = "consumption-data-heating#energy-heating-week-%s"; + public static final String CHANNEL_AC_ENERGY_HEATING_MONTH = "consumption-data-heating#energy-heating-month-%s"; + public static final String CHANNEL_AC_ENERGY_HEATING_CURRENT_DAY = "consumption-data-heating#energy-heating-current-day"; + public static final String CHANNEL_AC_ENERGY_HEATING_CURRENT_YEAR = "consumption-data-heating#energy-heating-current-year"; + public static final String CHANNEL_AC_ENERGY_COOLING_CURRENT_DAY = "consumption-data-cooling#energy-cooling-current-day"; + public static final String CHANNEL_AC_ENERGY_COOLING_CURRENT_YEAR = "consumption-data-cooling#energy-cooling-current-year"; + public static final String PROPERTY_AC_NAME = "name"; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaConfiguration.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaConfiguration.java new file mode 100644 index 0000000000000..6cf1e698a821c --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.onecta.internal.api.OnectaConnectionClient; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthTokenRefresher; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Thing; + +/** + * The {@link OnectaConfiguration} class contains global static variables, which are used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaConfiguration { + + private @Nullable static Thing bridgeThing = null; + private @Nullable static HttpClientFactory httpClientFactory = null; + + private @Nullable static HttpClient httpClient = null; + private @Nullable static OAuthTokenRefresher openHabOAuthTokenRefresher = null; + + private @NonNull static OnectaConnectionClient onectaConnectionClient = new OnectaConnectionClient(); + private @Nullable static OnectaTranslationProvider translation; + + public static void setTranslation(OnectaTranslationProvider translationPar) { + translation = translationPar; + } + + public static OnectaTranslationProvider getTranslation() { + Optional optionalTranslation = Optional.ofNullable(translation); + return optionalTranslation.orElseThrow(() -> new RuntimeException("Translation provider is not available")); + } + + public static void setHttpClientFactory(HttpClientFactory httpClientFactory) { + OnectaConfiguration.httpClientFactory = httpClientFactory; + httpClient = httpClientFactory.getCommonHttpClient(); + } + + public static void setBridgeThing(Thing bridgeThing) { + OnectaConfiguration.bridgeThing = bridgeThing; + } + + public static @Nullable HttpClient getHttpClient() { + return httpClient; + } + + public static @Nullable HttpClientFactory getHttpClientFactory() { + return httpClientFactory; + } + + public static void setOAuthTokenRefresher(OAuthTokenRefresher openHabOAuthTokenRefresher) { + OnectaConfiguration.openHabOAuthTokenRefresher = openHabOAuthTokenRefresher; + } + + public static OnectaConnectionClient getOnectaConnectionClient() { + return onectaConnectionClient; + } + + public static @Nullable OAuthTokenRefresher getOAuthTokenRefresher() { + return openHabOAuthTokenRefresher; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaGatewayConstants.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaGatewayConstants.java new file mode 100644 index 0000000000000..fb969a4f015d6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaGatewayConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link OnectaGatewayConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaGatewayConstants { + public static final String PROPERTY_GW_NAME = "name"; + // List of all Channel ids + public static final String CHANNEL_GW_DAYLIGHTSAVINGENABLED = "basic#daylight-savingtime-enabled"; + public static final String CHANNEL_GW_FIRMWAREVERSION = "basic#firmware-version"; + public static final String CHANNEL_GW_IS_FIRMWAREUPDATE_SUPPORTED = "basic#is-firmware-update-supported"; + public static final String CHANNEL_GW_IS_IN_ERROR_STATE = "basic#is-in-error-state"; + public static final String CHANNEL_GW_REGION_CODE = "basic#region-code"; + public static final String CHANNEL_GW_LED_ENABLED = "basic#led-enabled"; + public static final String CHANNEL_GW_SERIAL_NUMBER = "basic#serial-number"; + public static final String CHANNEL_GW_SSID = "basic#ssid"; + public static final String CHANNEL_GW_TIME_ZONE = "basic#timezone"; + public static final String CHANNEL_GW_WIFICONNENTION_SSID = "basic#wifi-connection-ssid"; + public static final String CHANNEL_GW_WIFICONNENTION_STRENGTH = "basic#wifi-connection-strength"; + public static final String CHANNEL_GW_MODEL_INFO = "basic#model-info"; + public static final String CHANNEL_GW_IP_ADDRESS = "basic#ip-address"; + public static final String CHANNEL_GW_MAC_ADDRESS = "basic#mac-address"; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaIndoorUnitConstants.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaIndoorUnitConstants.java new file mode 100644 index 0000000000000..46ae544ab10cf --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaIndoorUnitConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link OnectaIndoorUnitConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaIndoorUnitConstants { + + public static final String PROPERTY_IDU_MODELINFO = "modelInfo"; + public static final String PROPERTY_IDU_SOFTWAREVERSION = "softwareVersion"; + public static final String PROPERTY_IDU_EEPROMVERSION = "eepromVersion"; + public static final String CHANNEL_IDU_ISKEEPDRY = "basic#is-drykeep-setting"; + public static final String CHANNEL_IDU_FANSPEED = "basic#fanmotor-rotation-speed"; + public static final String CHANNEL_IDU_DELTAD = "basic#deltad-value"; + public static final String CHANNEL_IDU_HEATEXCHANGETEMP = "basic#heat-exchanger-temp"; + public static final String CHANNEL_IDU_SUCTIONTEMP = "basic#suction-temp"; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaTranslationProvider.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaTranslationProvider.java new file mode 100644 index 0000000000000..0784649fd75c0 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaTranslationProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Provides a convenience wrapper around framework-provided {@link TranslationProvider}, pre-filling the bundle and + * locale parameters + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +@Component(service = { OnectaTranslationProvider.class }) +public class OnectaTranslationProvider { + private final TranslationProvider i18nProvider; + private final LocaleProvider localeProvider; + private final @Nullable Bundle bundle; + + @Activate + public OnectaTranslationProvider(final @Reference TranslationProvider i18nProvider, + final @Reference LocaleProvider localeProvider, final BundleContext context) { + this.bundle = context.getBundle(); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + /** + * Similar to {@link TranslationProvider#getText(Bundle, String, String, java.util.Locale, Object...)}. + * Pre-fills {@code Bundle} and {@code Locale} params to reduce boilerplate. + * + * @param key the key to be translated (can be empty) + * @param arguments the arguments to be injected into the translation (each arg can be null) + * @return the translated text (can be null or empty) + */ + public @NonNullByDefault String getText(String key, Object @Nullable... arguments) { + String text = i18nProvider.getText(bundle, key, null, localeProvider.getLocale(), arguments); + return text != null ? text : ""; + } + + /** + * Similar to {@link TranslationProvider#getText(Bundle, String, String, java.util.Locale, Object...)}. + * Pre-fills {@code Bundle} and {@code Locale} params to reduce boilerplate. + * + * @param key the key to be translated (can be empty) + * @param defaultText the default text to be used (can be null or empty) + * @param arguments the arguments to be injected into the translation (each arg can be null) + * @return the translated text or the default text (can be null or empty) + */ + public @Nullable String getTextDefault(String key, @Nullable String defaultText, Object @Nullable... arguments) { + return i18nProvider.getText(bundle, key, defaultText, localeProvider.getLocale(), arguments); + } + + /** + * Similar to {@link TranslationProvider#getText(Bundle, String, String, java.util.Locale)}. + * Pre-fills {@code Bundle} and {@code Locale} params to reduce boilerplate. + * + * @param key the key to be translated (can be empty) + * @param defaultText the default text to be used (can be null or empty) + * @return the translated text or the default text (can be null or empty) + */ + public @Nullable String getTextDefault(String key, @Nullable String defaultText) { + return i18nProvider.getText(bundle, key, defaultText, localeProvider.getLocale()); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaWaterTankConstants.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaWaterTankConstants.java new file mode 100644 index 0000000000000..2dd1779350719 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/OnectaWaterTankConstants.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link OnectaWaterTankConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaWaterTankConstants { + public static final String PROPERTY_HWT_NAME = "name"; + // List of all Channel ids + public static final String CHANNEL_HWT_POWER = "basic#power"; + public static final String CHANNEL_HWT_ERRORCODE = "basic#error-code"; + public static final String CHANNEL_HWT_IS_HOLIDAY_MODE_ACTIVE = "basic#is-holiday-mode-active"; + public static final String CHANNEL_HWT_IS_IN_ERROR_STATE = "basic#is-in-error-state"; + public static final String CHANNEL_HWT_IS_IN_WARNING_STATE = "basic#is-in-warning-state"; + public static final String CHANNEL_HWT_IS_IN_INSTALLER_STATE = "basic#is-in-installer-state"; + public static final String CHANNEL_HWT_IS_IN_EMERGENCY_STATE = "basic#is-in-emergency-state"; + public static final String CHANNEL_HWT_POWERFUL_MODE = "basic#powerful-mode"; + public static final String CHANNEL_HWT_HEATUP_MODE = "basic#heatup-mode"; + public static final String CHANNEL_HWT_TANK_TEMPERATURE = "basic#tank-temperature"; + public static final String CHANNEL_HWT_OPERATION_MODE = "basic#operation-mode"; + public static final String CHANNEL_HWT_SETPOINT_MODE = "basic#setpoint-mode"; + public static final String CHANNEL_HWT_SETTEMP = "basic#settemp"; + public static final String CHANNEL_HWT_SETTEMP_MIN = "basic#settemp-min"; + public static final String CHANNEL_HWT_SETTEMP_MAX = "basic#settemp-max"; + public static final String CHANNEL_HWT_SETTEMP_STEP = "basic#settemp-step"; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/Enums.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/Enums.java new file mode 100644 index 0000000000000..d5ab5b794d828 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/Enums.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class Enums { + public enum OnOff { + ON("on"), + OFF("off"); + + private static final Logger LOGGER = LoggerFactory.getLogger(OnOff.class); + private final String value; + + OnOff(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static OnOff fromValue(String value) { + for (OnOff m : OnOff.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + + LOGGER.debug("Unexpected OnOff value of \"{}\"", value); + + // Default to off + return OFF; + } + } + + public enum OperationMode { + UNKNOWN(""), + AUTO("auto"), + DEHUMIDIFIER("dry"), + COLD("cooling"), + HEAT("heating"), + FAN("fanOnly"); + + private static final Logger LOGGER = LoggerFactory.getLogger(OperationMode.class); + private final String value; + + OperationMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static OperationMode fromValue(String value) { + for (OperationMode m : OperationMode.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + + LOGGER.debug("Unexpected Mode value of \"{}\"", value); + + // Default to auto + return AUTO; + } + } + + public enum HeatupMode { + UNKNOWN(""), + REHEATONLY("reheatOnly"), + SCHEDULEONLY("scheduleOnly"), + REHEATSCHEDULE("reheatSchedule"); + + private static final Logger LOGGER = LoggerFactory.getLogger(HeatupMode.class); + private final String value; + + HeatupMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static HeatupMode fromValue(String value) { + for (HeatupMode m : HeatupMode.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + + LOGGER.debug("Unexpected Mode value of \"{}\"", value); + + // Default to unknown + return UNKNOWN; + } + } + + public enum SetpointMode { + UNKNOWN(""), + WEATHERDEPENDENT("weatherDependent"), + FIXED("fixed"); + + private static final Logger LOGGER = LoggerFactory.getLogger(SetpointMode.class); + private final String value; + + SetpointMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SetpointMode fromValue(String value) { + for (SetpointMode m : SetpointMode.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + + LOGGER.debug("Unexpected Mode value of \"{}\"", value); + + // Default to unknown + return UNKNOWN; + } + } + + public enum ManagementPoint { + GATEWAY("gateway"), + CLIMATECONTROL("climateControl"), + INDOORUNIT("indoorUnit"), + OUTDOORUNIT("outdoorUnit"), + WATERTANK("domesticHotWaterTank"); + + private final String value; + + ManagementPoint(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public enum SensorData { + ROOMTEMP("roomTemperature"), + ROOMHUMINITY("roomHumidity"), + OUTDOORTEMP("outdoorTemperature"), + LEAVINGWATERTEMP("leavingWaterTemperature"); + + private final String value; + + SensorData(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public enum FanSpeed { + AUTO("auto"), + SILENCE("quiet"), + LEVEL_1("fixed_1"), + LEVEL_2("fixed_2"), + LEVEL_3("fixed_3"), + LEVEL_4("fixed_4"), + LEVEL_5("fixed_5"), + NOTAVAILABLE("notavailable"); + + private static final Logger LOGGER = LoggerFactory.getLogger(FanSpeed.class); + private final String value; + private final String mode; + private final Integer speed; + + FanSpeed(String value) { + this.value = value; + this.mode = value.split("_")[0]; + if (value.contains("_")) { + this.speed = Integer.parseInt(value.split("_")[1]); + } else { + this.speed = 0; + } + } + + public String getValue() { + return value; + } + + public Integer getValueSpeed() { + return speed; + } + + public String getValueMode() { + return mode; + } + + public static FanSpeed fromValue(String value) { + for (FanSpeed m : FanSpeed.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + LOGGER.debug("Unexpected FanSpeed value of \"{}\"", value); + + // Default to auto + return AUTO; + } + } + + public enum FanSpeedMode { + AUTO("auto"), + SILENCE("quiet"), + FIXED("fixed"); + + private static final Logger LOGGER = LoggerFactory.getLogger(FanSpeedMode.class); + private final String value; + + FanSpeedMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FanSpeedMode fromValue(String value) { + for (FanSpeedMode m : FanSpeedMode.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + LOGGER.debug("Unexpected FanSpeedMode value of \"{}\"", value); + + // Default to auto + return AUTO; + } + } + + public enum FanMovementHor { + STOPPED("stop"), + SWING("swing"), + NOTAVAILABLE("notavailable"); + + private static final Logger LOGGER = LoggerFactory.getLogger(FanMovementHor.class); + private final String value; + + FanMovementHor(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FanMovementHor fromValue(@Nullable String value) { + for (FanMovementHor m : FanMovementHor.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + LOGGER.debug("Unexpected FanMovementHor value of \"{}\"", value); + + // Default to notavailable + return NOTAVAILABLE; + } + } + + public enum FanMovementVer { + STOPPED("stop"), + SWING("swing"), + WINDNICE("windNice"), + NOTAVAILABLE("notavailable"); + + private static final Logger LOGGER = LoggerFactory.getLogger(FanMovementVer.class); + private final String value; + + FanMovementVer(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FanMovementVer fromValue(@Nullable String value) { + for (FanMovementVer m : FanMovementVer.values()) { + if (m.getValue().equalsIgnoreCase(value)) { + return m; + } + } + LOGGER.debug("Unexpected FanMovementVer value of \"{}\"", value); + + // Default to notavailable + return NOTAVAILABLE; + } + } + + public enum FanMovement { + UNKNOWN(-1), + STOPPED(0), + VERTICAL(1), + VERTICAL_EXTRA(4), + HORIZONTAL(2), + VERTICAL_AND_HORIZONTAL(3), + VERTICAL_AND_HORIZONTAL_EXTRA(5); + + private static final Logger LOGGER = LoggerFactory.getLogger(FanMovement.class); + private final int value; + + FanMovement(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static FanMovement fromValue(int value) { + for (FanMovement m : FanMovement.values()) { + if (m.getValue() == value) { + return m; + } + } + LOGGER.debug("Unexpected FanMovement value of \"{}\"", value); + + // Default to stopped + return STOPPED; + } + } + + public enum DemandControl { + + OFF("off"), + AUTO("auto"), + FIXED("fixed"), + SCHEDULED("scheduled"); + + private static final Logger LOGGER = LoggerFactory.getLogger(DemandControl.class); + private final String value; + + DemandControl(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static DemandControl fromValue(String value) { + for (DemandControl m : DemandControl.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + LOGGER.debug("Unexpected DemandControl value of \"{}\"", value); + + // Default to off + return OFF; + } + } + + public enum AdvancedMode { + STREAMER("13"), + ECO("12"), + POWERFUL("2"), + POWERFUL_STREAMER("2/13"), + ECO_STREAMER("12/13"), + OFF(""), + UNKNOWN("??"); + + private static final Logger LOGGER = LoggerFactory.getLogger(AdvancedMode.class); + private final String value; + + AdvancedMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public boolean isStreamerActive() { + return this.equals(STREAMER) || this.equals(POWERFUL_STREAMER) || this.equals(ECO_STREAMER); + } + + public boolean isUndefined() { + return this.equals(UNKNOWN); + } + + public static AdvancedMode fromValue(String value) { + for (AdvancedMode m : AdvancedMode.values()) { + if (m.getValue().equals(value)) { + return m; + } + } + LOGGER.debug("Unexpected AdvancedMode value of \"{}\"", value); + + // Default to UNKNOWN + return UNKNOWN; + } + } + + public enum SpecialMode { + NORMAL("0"), + POWERFUL("1"), + ECO("2"); + + private final String value; + + SpecialMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SpecialMode fromAdvancedMode(AdvancedMode advMode) { + switch (advMode) { + case POWERFUL: + case POWERFUL_STREAMER: + return SpecialMode.POWERFUL; + case ECO: + case ECO_STREAMER: + return SpecialMode.ECO; + } + // default to normal + return NORMAL; + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaConnectionClient.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaConnectionClient.java new file mode 100644 index 0000000000000..5528c34c5e562 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaConnectionClient.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api; + +import static org.openhab.binding.onecta.internal.api.OnectaProperties.*; + +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpContentResponse; +import org.eclipse.jetty.client.api.Response; +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.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.dto.commands.CommandOnOf; +import org.openhab.binding.onecta.internal.api.dto.commands.CommandTrueFalse; +import org.openhab.binding.onecta.internal.api.dto.units.Unit; +import org.openhab.binding.onecta.internal.api.dto.units.Units; +import org.openhab.binding.onecta.internal.exception.DaikinCommunicationDataException; +import org.openhab.binding.onecta.internal.exception.DaikinCommunicationException; +import org.openhab.binding.onecta.internal.exception.DaikinCommunicationForbiddenException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.*; +import com.google.gson.JsonArray; + +/** + * @author Alexander Drent - Initial contributionF + */ +@NonNullByDefault +public class OnectaConnectionClient { + + private static final Logger logger = LoggerFactory.getLogger(OnectaConnectionClient.class); + private static final String HTTPHEADER_X_API_KEY = "x-api-key"; + private static final String HTTPHEADER_BEARER = "Bearer %s"; + private static final String USER_AGENT_VALUE = "Daikin/1.6.1.4681 CFNetwork/1209 Darwin/20.2.0"; + private static final String HTTPHEADER_X_API_KEY_VALUE = "xw6gvOtBHq5b1pyceadRp6rujSNSZdjx2AqT03iC"; + private static final Long REQUEST_TIMEOUT = 60L; // 60 seconds + + private static JsonArray onectaCompleteJsonArrayData = new JsonArray(); + private static Units onectaUnitsData = new Units(); + private static OnectaSignInClient onectaSignInClient = new OnectaSignInClient(); + + public void openConnecttion() { + try { + onectaSignInClient.SignIn(); + } catch (DaikinCommunicationException e) { + logger.error("Error in OnectaConnectionClient", e); + } + } + + public Units getUnits() { + return onectaUnitsData; + } + + public void restoreConnecton() throws DaikinCommunicationException { + this.fetchAccessToken(); + } + + public void fetchAccessToken() throws DaikinCommunicationException { + logger.debug("Refresh token."); + + try { + onectaSignInClient.refreshToken(); + } catch (Throwable e) { + throw new DaikinCommunicationException(e); + } + } + + public Boolean isOnline() { + return onectaSignInClient.isOnline(); + } + + private Response doBearerRequestGet(Boolean refreshed) throws DaikinCommunicationException { + Response response = null; + logger.debug("doBearerRequestGet : Accesstoken refreshed {}", refreshed.toString()); + try { + String testUrl = getBaseUrl(""); + response = OnectaConfiguration.getHttpClient().newRequest(testUrl).method(HttpMethod.GET) + .header(HttpHeader.AUTHORIZATION, String.format(HTTPHEADER_BEARER, onectaSignInClient.getToken())) + .header(HttpHeader.USER_AGENT, USER_AGENT_VALUE) + .header(HTTPHEADER_X_API_KEY, HTTPHEADER_X_API_KEY_VALUE).timeout(REQUEST_TIMEOUT, TimeUnit.SECONDS) + .send(); + + logger.trace("Resonse : {}", ((HttpContentResponse) response).getContentAsString()); + logger.debug("Headers : {}", response.getHeaders().toString()); + + if (response.getStatus() == HttpStatus.UNAUTHORIZED_401 && !refreshed) { + this.fetchAccessToken(); + return doBearerRequestGet(true); + } + + } catch (DaikinCommunicationException | ExecutionException | InterruptedException | TimeoutException e) { + if (!refreshed) { + try { + this.fetchAccessToken(); + return doBearerRequestGet(true); + } catch (DaikinCommunicationException ex) { + throw new DaikinCommunicationException(ex); + } + } else { + throw new DaikinCommunicationException(e); + } + } + + return response; + } + + private Response doBearerRequestPatch(String url, Object body) { + return doBearerRequestPatch(url, body, false); + } + + private Response doBearerRequestPatch(String url, Object body, Boolean refreshed) { + Response response = null; + try { + response = OnectaConfiguration.getHttpClient().newRequest(url).method(HttpMethod.PATCH) + .content(new StringContentProvider(new Gson().toJson(body)), MediaType.APPLICATION_JSON) + .header(HttpHeader.AUTHORIZATION, String.format(HTTPHEADER_BEARER, onectaSignInClient.getToken())) + .header(HttpHeader.USER_AGENT, USER_AGENT_VALUE) + .header(HTTPHEADER_X_API_KEY, HTTPHEADER_X_API_KEY_VALUE).timeout(REQUEST_TIMEOUT, TimeUnit.SECONDS) + .send(); + + logger.trace("Request : {}", response.getRequest().getURI().toString()); + logger.trace("Body : {}", new Gson().toJson(body)); + logger.trace("Resonse : {}", ((HttpContentResponse) response).getContentAsString()); + logger.debug("Headers : {}", response.getHeaders().toString()); + + if (response.getStatus() == HttpStatus.UNAUTHORIZED_401 && !refreshed) { + this.fetchAccessToken(); + return doBearerRequestPatch(url, body, true); + } + } catch (DaikinCommunicationException | ExecutionException | InterruptedException | TimeoutException e) { + if (!refreshed) { + try { + this.fetchAccessToken(); + return doBearerRequestPatch(url, body, true); + } catch (DaikinCommunicationException ex) { + throw new RuntimeException(ex); + } + } + } + if (response == null) { + throw new RuntimeException("Response is null"); + } + return response; + } + + public void refreshUnitsData() throws DaikinCommunicationException { + + Response response = doBearerRequestGet(false); + String responseString = ((HttpContentResponse) response).getContentAsString(); + + logger.trace("Response body: {}", responseString); + if (response.getStatus() == HttpStatus.OK_200) { + try { + onectaCompleteJsonArrayData = JsonParser.parseString(responseString).getAsJsonArray(); + onectaUnitsData.getAll().clear(); + for (int i = 0; i < onectaCompleteJsonArrayData.size(); i++) { + onectaUnitsData.getAll().add(Objects.requireNonNull( + new Gson().fromJson(onectaCompleteJsonArrayData.get(i).getAsJsonObject(), Unit.class))); + } + } catch (JsonSyntaxException ex) { + logger.error("Response body: {}", responseString); + throw new DaikinCommunicationDataException( + String.format("Not a valid json response from Onecta received: (%s)", ex.getMessage())); + } + } else { + throw new DaikinCommunicationForbiddenException( + String.format("refreshUnitsData resonse (%s) : (%s)", response.getStatus(), responseString)); + } + } + + public Unit getUnit(String unitId) { + return onectaUnitsData.findById(unitId); + } + + public JsonObject getRawData(String unitId) { + JsonObject jsonObject = null; + for (int i = 0; i < onectaCompleteJsonArrayData.size(); i++) { + jsonObject = onectaCompleteJsonArrayData.get(i).getAsJsonObject(); + if (jsonObject.get("id").getAsString().equals(unitId)) { + return jsonObject; + } + } + + return new JsonObject(); + } + + public void setPowerOnOff(String unitId, Enums.ManagementPoint managementPointType, Enums.OnOff value) { + logger.debug("setPowerOnOff : {}, {}, {}", unitId, managementPointType.getValue(), value); + CommandOnOf commandOnOf = new CommandOnOf(value); + doBearerRequestPatch(getUrlOnOff(unitId, managementPointType), commandOnOf); + } + + public void setPowerfulModeOnOff(String unitId, Enums.ManagementPoint managementPointType, Enums.OnOff value) { + logger.debug("setPowerfulModeOnOff : {}, {}, {}", unitId, managementPointType.getValue(), value); + CommandOnOf commandOnOf = new CommandOnOf(value); + doBearerRequestPatch(getUrlPowerfulModeOnOff(unitId, managementPointType), commandOnOf); + } + + public void setEconoMode(String unitId, Enums.ManagementPoint managementPointType, Enums.OnOff value) { + logger.debug("setEconoMode: {}, {}, {}", unitId, managementPointType.getValue(), value); + CommandOnOf commandOnOf = new CommandOnOf(value); + doBearerRequestPatch(getEconoMode(unitId, managementPointType), commandOnOf); + } + + public void setCurrentOperationMode(String unitId, Enums.ManagementPoint managementPointType, + Enums.OperationMode operationMode) { + logger.debug("setCurrentOperationMode : {}, {}, {}", unitId, managementPointType.getValue(), + operationMode.getValue()); + doBearerRequestPatch(getOperationModeUrl(unitId, managementPointType), getOperationModeCommand(operationMode)); + } + + public void setCurrentTemperatureRoomSet(String unitId, String embeddedId, Enums.OperationMode currentMode, + float value) { + logger.debug("setCurrentTemperatureRoomSet : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + doBearerRequestPatch(getTemperatureControlUrl(unitId, embeddedId), + getTemperatureRoomControlCommand(value, currentMode)); + } + + public void setCurrentTemperatureHotWaterSet(String unitId, String embeddedId, Enums.OperationMode currentMode, + float value) { + logger.debug("setCurrentTemperatureHotWaterSet : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + doBearerRequestPatch(getTemperatureControlUrl(unitId, embeddedId), + getTemperatureHotWaterControlCommand(value, currentMode)); + } + + public void setFanSpeed(String unitId, String embeddedId, Enums.OperationMode currentMode, + Enums.FanSpeed fanspeed) { + logger.debug("setFanSpeed : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + doBearerRequestPatch(getTFanControlUrl(unitId, embeddedId), getTFanSpeedCurrentCommand(currentMode, fanspeed)); + if (fanspeed.getValueMode().equals(Enums.FanSpeedMode.FIXED.getValue())) { + doBearerRequestPatch(getTFanControlUrl(unitId, embeddedId), + getTFanSpeedFixedCommand(currentMode, fanspeed)); + } + } + + public void setCurrentFanDirection(String unitId, String embeddedId, Enums.OperationMode currentMode, + Enums.FanMovement fanMovement) { + logger.debug("setCurrentFanDirection : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + String url = getTFanControlUrl(unitId, embeddedId); + switch (fanMovement) { + case STOPPED: + doBearerRequestPatch(url, getTFanDirectionHorCommand(currentMode, Enums.FanMovementHor.STOPPED)); + doBearerRequestPatch(url, getTFanDirectionVerCommand(currentMode, Enums.FanMovementVer.STOPPED)); + break; + case VERTICAL: + doBearerRequestPatch(url, getTFanDirectionHorCommand(currentMode, Enums.FanMovementHor.STOPPED)); + doBearerRequestPatch(url, getTFanDirectionVerCommand(currentMode, Enums.FanMovementVer.SWING)); + break; + case HORIZONTAL: + doBearerRequestPatch(url, getTFanDirectionHorCommand(currentMode, Enums.FanMovementHor.SWING)); + doBearerRequestPatch(url, getTFanDirectionVerCommand(currentMode, Enums.FanMovementVer.STOPPED)); + break; + case VERTICAL_AND_HORIZONTAL: + doBearerRequestPatch(url, getTFanDirectionHorCommand(currentMode, Enums.FanMovementHor.SWING)); + doBearerRequestPatch(url, getTFanDirectionVerCommand(currentMode, Enums.FanMovementVer.SWING)); + break; + default: + break; + } + } + + public void setCurrentFanDirectionHor(String unitId, String embeddedId, Enums.OperationMode currentMode, + Enums.FanMovementHor fanMovement) { + logger.debug("setCurrentFanDirectionHor : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + String url = getTFanControlUrl(unitId, embeddedId); + doBearerRequestPatch(url, getTFanDirectionHorCommand(currentMode, fanMovement)); + } + + public void setCurrentFanDirectionVer(String unitId, String embeddedId, Enums.OperationMode currentMode, + Enums.FanMovementVer fanMovement) { + logger.debug("setCurrentFanDirectionVer : {}, {}, {}", unitId, embeddedId, currentMode.getValue()); + String url = getTFanControlUrl(unitId, embeddedId); + doBearerRequestPatch(url, getTFanDirectionVerCommand(currentMode, fanMovement)); + } + + public void setStreamerMode(String unitId, String embeddedId, Enums.OnOff value) { + logger.debug("setStreamerMode: {}, {}, {}", unitId, embeddedId, value); + CommandOnOf commandOnOf = new CommandOnOf(value); + doBearerRequestPatch(getStreamerMode(unitId, embeddedId), commandOnOf); + } + + public void setHolidayMode(String unitId, String embeddedId, Enums.OnOff value) { + logger.debug("setHolidayMode: {}, {}, {}", unitId, embeddedId, value); + CommandTrueFalse commandTrueFalse = new CommandTrueFalse(value); + doBearerRequestPatch(getHolidayMode(unitId, embeddedId), commandTrueFalse); + } + + public void setDemandControl(String unitId, String embeddedId, Enums.DemandControl value) { + logger.debug("setDemandControl: {}, {}, {}", unitId, embeddedId, value); + doBearerRequestPatch(getTDemandControlUrl(unitId, embeddedId), getTDemandControlCommand(value)); + } + + public void setDemandControlFixedValue(String unitId, String embeddedId, Integer value) { + logger.debug("setDemandControlFixedValue: {}, {}, {}", unitId, embeddedId, value); + + doBearerRequestPatch(getTDemandControlUrl(unitId, embeddedId), getTDemandControlFixedValueCommand(value)); + } + + public void setTargetTemperatur(String unitId, String embeddedId, Float value) { + logger.debug("setRefreshToken: {}, {}, {}", unitId, embeddedId, value); + doBearerRequestPatch(getTargetTemperaturUrl(unitId, embeddedId), getTargetTemperaturCommand(value)); + } + + public void setSetpointLeavingWaterOffset(String unitId, String embeddedId, Enums.OperationMode operationMode, + Float value) { + logger.debug("setRefreshToken: {}, {}, {}. {}", unitId, embeddedId, operationMode, value); + doBearerRequestPatch(getTemperatureControlUrl(unitId, embeddedId), + getSetpointLeavingWaterOffsetCommand(value, operationMode)); + } + + public void setSetpointLeavingWaterTemperature(String unitId, String embeddedId, Enums.OperationMode operationMode, + Float value) { + logger.debug("setRefreshToken: {}, {}, {}, {}", unitId, embeddedId, operationMode, value); + doBearerRequestPatch(getTemperatureControlUrl(unitId, embeddedId), + getSetpointLeavingWaterTemperatureCommand(value, operationMode)); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaProperties.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaProperties.java new file mode 100644 index 0000000000000..788fa6b226159 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaProperties.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.onecta.internal.api.dto.commands.CommandFloat; +import org.openhab.binding.onecta.internal.api.dto.commands.CommandInteger; +import org.openhab.binding.onecta.internal.api.dto.commands.CommandString; + +/** + * The {@link OnectaProperties} class defines common constants, which are + * used across the whole binding. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaProperties { + + private static final String BASE_URL = "https://api.onecta.daikineurope.com/v1/gateway-devices/%s"; + private static final String BASE_URL_COMMAND = "/management-points/%s/characteristics/%s"; + private static final String COMMAND_ONOFFMODE = "onOffMode"; + private static final String COMMAND_POWERFULMODE = "powerfulMode"; + private static final String COMMAND_ECONOMODE = "econoMode"; + private static final String COMMAND_OPERATIONMODE = "operationMode"; + private static final String COMMAND_TEMPERATURECONTROL = "temperatureControl"; + private static final String COMMAND_TARGETTEMPERATURE = "targetTemperature"; + private static final String COMMAND_STREAMERMODE = "streamerMode"; + private static final String COMMAND_HOLIDAYMODE = "holidayMode"; + private static final String COMMAND_SUBPATH_TEMPERATURECONTROL_ROOM = "/operationModes/%s/setpoints/roomTemperature"; + private static final String COMMAND_SUBPATH_TEMPERATURECONTROL_HOTWATERTANK = "/operationModes/%s/setpoints/domesticHotWaterTemperature"; + private static final String COMMAND_SUBPATH_TEMPERATURECONTROL_LEAVINGWATEROFFSET = "/operationModes/%s/setpoints/leavingWaterOffset"; + private static final String COMMAND_SUBPATH_TEMPERATURECONTROL_LEAVINGWATERTEMP = "/operationModes/%s/setpoints/leavingWaterTemperature"; + private static final String COMMAND_FANSPEED_CONTROL = "fanControl"; + private static final String COMMAND_DEMAND_CONTROL = "demandControl"; + private static final String COMMAND_SUBPATH_FANSPEED = "/operationModes/%s/fanSpeed/currentMode"; + private static final String COMMAND_SUBPATH_FANSPEED_FIXED = "/operationModes/%s/fanSpeed/modes/fixed"; + private static final String COMMAND_SUBPATH_FANDITECTION_HOR = "/operationModes/%s/fanDirection/horizontal/currentMode"; + private static final String COMMAND_SUBPATH_FANDITECTION_VER = "/operationModes/%s/fanDirection/vertical/currentMode"; + private static final String COMMAND_SUBPATH_DEMAND_CONTROL = "/currentMode"; + private static final String COMMAND_SUBPATH_DEMAND_CONTROL_FIXED_VALUE = "/modes/fixed"; + + public static String getBaseUrl(String unitId) { + return StringUtils.removeEnd(String.format(BASE_URL, unitId), "/"); + } + + public static String getUrlOnOff(String unitId, Enums.ManagementPoint managementPointType) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, managementPointType.getValue(), COMMAND_ONOFFMODE); + } + + public static String getUrlPowerfulModeOnOff(String unitId, Enums.ManagementPoint managementPointType) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, managementPointType.getValue(), + COMMAND_POWERFULMODE); + } + + public static String getEconoMode(String unitId, Enums.ManagementPoint managementPointType) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, managementPointType.getValue(), COMMAND_ECONOMODE); + } + + public static String getOperationModeUrl(String unitId, Enums.ManagementPoint managementPointType) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, managementPointType.getValue(), + COMMAND_OPERATIONMODE); + } + + public static String getTargetTemperaturUrl(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_TARGETTEMPERATURE); + } + + public static CommandFloat getTargetTemperaturCommand(float value) { + return new CommandFloat(value); + } + + public static CommandString getOperationModeCommand(Enums.OperationMode operationMode) { + return new CommandString(operationMode.getValue()); + } + + public static String getTemperatureControlUrl(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_TEMPERATURECONTROL); + } + + public static CommandFloat getTemperatureRoomControlCommand(float value, Enums.OperationMode currentMode) { + return new CommandFloat(value, String.format(COMMAND_SUBPATH_TEMPERATURECONTROL_ROOM, currentMode.getValue())); + } + + public static CommandFloat getSetpointLeavingWaterOffsetCommand(float value, Enums.OperationMode currentMode) { + return new CommandFloat(value, + String.format(COMMAND_SUBPATH_TEMPERATURECONTROL_LEAVINGWATEROFFSET, currentMode.getValue())); + } + + public static CommandFloat getSetpointLeavingWaterTemperatureCommand(float value, Enums.OperationMode currentMode) { + return new CommandFloat(value, + String.format(COMMAND_SUBPATH_TEMPERATURECONTROL_LEAVINGWATERTEMP, currentMode.getValue())); + } + + public static CommandFloat getTemperatureHotWaterControlCommand(float value, Enums.OperationMode currentMode) { + return new CommandFloat(value, + String.format(COMMAND_SUBPATH_TEMPERATURECONTROL_HOTWATERTANK, currentMode.getValue())); + } + + public static String getTFanControlUrl(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_FANSPEED_CONTROL); + } + + public static CommandString getTFanSpeedCurrentCommand(Enums.OperationMode currentMode, Enums.FanSpeed fanspeed) { + return new CommandString(fanspeed.getValueMode(), + String.format(COMMAND_SUBPATH_FANSPEED, currentMode.getValue())); + } + + public static CommandInteger getTFanSpeedFixedCommand(Enums.OperationMode currentMode, Enums.FanSpeed fanspeed) { + return new CommandInteger(fanspeed.getValueSpeed(), + String.format(COMMAND_SUBPATH_FANSPEED_FIXED, currentMode.getValue())); + } + + public static CommandString getTFanDirectionHorCommand(Enums.OperationMode currentMode, + Enums.FanMovementHor fanMovement) { + return new CommandString(fanMovement.getValue(), + String.format(COMMAND_SUBPATH_FANDITECTION_HOR, currentMode.getValue())); + } + + public static CommandString getTFanDirectionVerCommand(Enums.OperationMode currentMode, + Enums.FanMovementVer fanMovement) { + return new CommandString(fanMovement.getValue(), + String.format(COMMAND_SUBPATH_FANDITECTION_VER, currentMode.getValue())); + } + + public static String getStreamerMode(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_STREAMERMODE); + } + + public static String getHolidayMode(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_HOLIDAYMODE); + } + + public static String getTDemandControlUrl(String unitId, String embeddedId) { + return String.format(getBaseUrl(unitId) + BASE_URL_COMMAND, embeddedId, COMMAND_DEMAND_CONTROL); + } + + public static CommandString getTDemandControlCommand(Enums.DemandControl value) { + return new CommandString(value.getValue(), String.format(COMMAND_SUBPATH_DEMAND_CONTROL, value.getValue())); + } + + public static CommandInteger getTDemandControlFixedValueCommand(Integer value) { + return new CommandInteger(value, COMMAND_SUBPATH_DEMAND_CONTROL_FIXED_VALUE); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaSignInClient.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaSignInClient.java new file mode 100644 index 0000000000000..c78ba0aa78f33 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/OnectaSignInClient.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api; + +import static org.openhab.binding.onecta.internal.OnectaBridgeConstants.OAUTH2_SERVICE_HANDLE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.exception.DaikinCommunicationException; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthTokenRefreshListener; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthTokenRefresher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaSignInClient implements OAuthTokenRefreshListener { + + private final Logger logger = LoggerFactory.getLogger(OnectaSignInClient.class); + + private @Nullable OAuthTokenRefresher oAuthTokenRefresher; + + public OnectaSignInClient() { + super(); + } + + public void SignIn() throws DaikinCommunicationException { + try { + this.oAuthTokenRefresher = OnectaConfiguration.getOAuthTokenRefresher(); + oAuthTokenRefresher.unsetRefreshListener(OAUTH2_SERVICE_HANDLE); + oAuthTokenRefresher.setRefreshListener(this, OAUTH2_SERVICE_HANDLE); + } catch (Throwable e) { + throw new DaikinCommunicationException(e); + } + } + + public void refreshToken() throws DaikinCommunicationException { + logger.debug("Refresh token."); + try { + oAuthTokenRefresher.refreshToken(OAUTH2_SERVICE_HANDLE); + } catch (Throwable e) { + throw new DaikinCommunicationException(e); + } + } + + @Override + public void onNewAccessToken(String accessToken) { + logger.debug("new access token: {}", accessToken); + } + + public String getToken() throws DaikinCommunicationException { + try { + return oAuthTokenRefresher.getAccessTokenFromStorage(OAUTH2_SERVICE_HANDLE).get(); + } catch (Throwable e) { + throw new DaikinCommunicationException(e); + } + } + + public Boolean isOnline() { + try { + return !getToken().isEmpty(); + } catch (Throwable e) { + return false; + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandFloat.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandFloat.java new file mode 100644 index 0000000000000..a66ea07b298b5 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandFloat.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.commands; + +/** + * @author Alexander Drent - Initial contribution + */ +public class CommandFloat { + public float value; + public String path; + + public CommandFloat(float value) { + this.value = value; + } + + public CommandFloat(float value, String path) { + this.value = value; + this.path = path; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandInteger.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandInteger.java new file mode 100644 index 0000000000000..29618573d1d7e --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandInteger.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.commands; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class CommandInteger { + @SerializedName("value") + public Integer value; + + @SerializedName("path") + public String path; + + public CommandInteger(Integer value) { + this.value = value; + } + + public CommandInteger(Integer value, String path) { + this.value = value; + this.path = path; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandOnOf.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandOnOf.java new file mode 100644 index 0000000000000..40883f1f1cd2a --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandOnOf.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.commands; + +import org.openhab.binding.onecta.internal.api.Enums; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class CommandOnOf { + @SerializedName("value") + public String value; + + @SerializedName("path") + public String path; + + public CommandOnOf(Enums.OnOff value) { + this.value = value.getValue(); + } + + public CommandOnOf(Enums.OnOff value, String path) { + this.value = value.getValue(); + this.path = path; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandString.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandString.java new file mode 100644 index 0000000000000..385f4d1fff184 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandString.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.commands; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class CommandString { + @SerializedName("value") + public String value; + + @SerializedName("path") + public String path; + + public CommandString(String value) { + this.value = value; + } + + public CommandString(String value, String path) { + this.value = value; + this.path = path; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandTrueFalse.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandTrueFalse.java new file mode 100644 index 0000000000000..23c01aeba90a3 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/commands/CommandTrueFalse.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.commands; + +import org.openhab.binding.onecta.internal.api.Enums; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class CommandTrueFalse { + @SerializedName("value.enabled") + public boolean value; + + public CommandTrueFalse(Enums.OnOff value) { + this.value = value.equals(Enums.OnOff.ON); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ActionTypesModes.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ActionTypesModes.java new file mode 100644 index 0000000000000..7442caef53ca0 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ActionTypesModes.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class ActionTypesModes { + private FanSpeedFixed fixed; + + public FanSpeedFixed getFixed() { + return fixed; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/AutoFanSpeed.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/AutoFanSpeed.java new file mode 100644 index 0000000000000..2405ad0599302 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/AutoFanSpeed.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class AutoFanSpeed { + private FanCurrentMode currentMode; + private ActionTypesModes modes; + + public ActionTypesModes getModes() { + return modes; + } + + public FanCurrentMode getCurrentMode() { + return currentMode; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionData.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionData.java new file mode 100644 index 0000000000000..706efda3a4731 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionData.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class ConsumptionData { + private String ref; + private boolean settable; + private ConsumptionDataValue value; + + public String getRef() { + return ref; + } + + public boolean isSettable() { + return settable; + } + + public ConsumptionDataValue getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionDataValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionDataValue.java new file mode 100644 index 0000000000000..02ee427c1ba22 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ConsumptionDataValue.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class ConsumptionDataValue { + private Electrical electrical; + + public Electrical getElectrical() { + return electrical; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControl.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControl.java new file mode 100644 index 0000000000000..b3fdf289a6abd --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DemandControl { + private String ref; + private boolean settable; + private DemandControlValue value; + + public String getRef() { + return ref; + } + + public boolean isSettable() { + return settable; + } + + public DemandControlValue getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModes.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModes.java new file mode 100644 index 0000000000000..02f9f0e486b84 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModes.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DemandControlModes { + @SerializedName("fixed") + private DemandControlModesFixed fixedValues; + + public DemandControlModesFixed getFixedValues() { + return fixedValues; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModesFixed.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModesFixed.java new file mode 100644 index 0000000000000..a4831897f26ef --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlModesFixed.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DemandControlModesFixed { + private Integer stepValue; + private Integer value; + private Integer minValue; + private Integer maxValue; + private Boolean settable; + + public Integer getStepValue() { + return stepValue; + } + + public Integer getValue() { + return value; + } + + public Integer getMinValue() { + return minValue; + } + + public Integer getMaxValue() { + return maxValue; + } + + public Boolean getSettable() { + return settable; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlValue.java new file mode 100644 index 0000000000000..14b3cb19c5078 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/DemandControlValue.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DemandControlValue { + private GatwaySubValueString currentMode; + private DemandControlModes modes; + + public DemandControlModes getModes() { + return modes; + } + + public GatwaySubValueString getCurrentMode() { + return currentMode; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Electrical.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Electrical.java new file mode 100644 index 0000000000000..0775c4cb626c1 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Electrical.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class Electrical { + private String unit; + private Ing heating; + private Ing cooling; + + public String getUnit() { + return unit; + } + + public Ing getHeating() { + return heating; + } + + public Ing getCooling() { + return cooling; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControl.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControl.java new file mode 100644 index 0000000000000..289b72d620ece --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanControl { + private String ref; + private boolean settable; + private FanControlValue value; + + public String getRef() { + return ref; + } + + public boolean isSettable() { + return settable; + } + + public FanControlValue getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControlValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControlValue.java new file mode 100644 index 0000000000000..76fff446c66f6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanControlValue.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanControlValue { + private FanOperationModes operationModes; + + public FanOperationModes getOperationModes() { + return operationModes; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanCurrentMode.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanCurrentMode.java new file mode 100644 index 0000000000000..0a6f724cf500e --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanCurrentMode.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanCurrentMode { + private String value; + private boolean settable; + private String[] values; + + public boolean isSettable() { + return settable; + } + + public String[] getValues() { + return values; + } + + public String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanDirection.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanDirection.java new file mode 100644 index 0000000000000..0d0220f09a7f6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanDirection.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanDirection { + private FanMovement vertical; + private FanMovement horizontal; + + public FanMovement getHorizontal() { + return horizontal; + } + + public FanMovement getVertical() { + return vertical; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanMovement.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanMovement.java new file mode 100644 index 0000000000000..d2437f2b0c2f7 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanMovement.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Alexander Drent - Initial contribution + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class FanMovement { + private FanCurrentMode currentMode; + + public FanCurrentMode getCurrentMode() { + return currentMode; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOnlyClass.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOnlyClass.java new file mode 100644 index 0000000000000..2232db93d677b --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOnlyClass.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanOnlyClass { + private AutoFanSpeed fanSpeed; + private FanDirection fanDirection; + + public AutoFanSpeed getFanSpeed() { + return fanSpeed; + } + + public FanDirection getFanDirection() { + return fanDirection; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOperationModes.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOperationModes.java new file mode 100644 index 0000000000000..c179d2f3339c7 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanOperationModes.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import org.openhab.binding.onecta.internal.api.Enums; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanOperationModes { + + private FanOnlyClass heating; + private FanOnlyClass cooling; + private FanOnlyClass auto; + private FanOnlyClass dry; + private FanOnlyClass fanOnly; + + public FanOnlyClass getFanOperationMode(Enums.OperationMode operationMode) { + if (operationMode.equals(Enums.OperationMode.HEAT)) { + return this.heating; + } else if (operationMode.equals(Enums.OperationMode.COLD)) { + return this.cooling; + } else if (operationMode.equals(Enums.OperationMode.AUTO)) { + return this.auto; + } else if (operationMode.equals(Enums.OperationMode.FAN)) { + return this.fanOnly; + } else if (operationMode.equals(Enums.OperationMode.DEHUMIDIFIER)) { + return this.dry; + } else + return null; + } + + public FanOnlyClass getHeating() { + return heating; + } + + public FanOnlyClass getCooling() { + return cooling; + } + + public FanOnlyClass getAuto() { + return auto; + } + + public FanOnlyClass getDry() { + return dry; + } + + public FanOnlyClass getFanOnly() { + return fanOnly; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanSpeedFixed.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanSpeedFixed.java new file mode 100644 index 0000000000000..6aef1912c3f3a --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/FanSpeedFixed.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class FanSpeedFixed { + private boolean settable; + private Integer value; + private Integer maxValue; + private Integer minValue; + private Integer stepValue; + private String unit; + + public boolean isSettable() { + return settable; + } + + public Integer getValue() { + return value; + } + + public Integer getMaxValue() { + return maxValue; + } + + public Integer getMinValue() { + return minValue; + } + + public Integer getStepValue() { + return stepValue; + } + + public String getUnit() { + return unit; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueBoolean.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueBoolean.java new file mode 100644 index 0000000000000..9c5334f4b4a37 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueBoolean.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class GatwaySubValueBoolean { + private boolean settable; + private Boolean value; + + public boolean isSettable() { + return settable; + } + + public Boolean getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueInteger.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueInteger.java new file mode 100644 index 0000000000000..8620197c32975 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueInteger.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class GatwaySubValueInteger { + private boolean settable; + private Integer value; + private Integer[] values; + + public boolean isSettable() { + return settable; + } + + public Integer getValue() { + return value; + } + + public Integer[] getValues() { + return values; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueString.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueString.java new file mode 100644 index 0000000000000..4fafa682ed7f1 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/GatwaySubValueString.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class GatwaySubValueString { + private boolean settable; + private String value; + private String[] values; + + public boolean isSettable() { + return settable; + } + + public String getValue() { + return value; + } + + public String[] getValues() { + return values; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayMode.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayMode.java new file mode 100644 index 0000000000000..55941f9423f03 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayMode.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Alexander Drent - Initial contribution + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HolidayMode { + private String ref; + private boolean settable; + private HolidayModeValue value; + + public String getRef() { + return ref; + } + + public boolean isSettable() { + return settable; + } + + public String getValue() { + return value.isEnabled() ? "ON" : "OFF"; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayModeValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayModeValue.java new file mode 100644 index 0000000000000..4ed51b757e9d5 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/HolidayModeValue.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Alexander Drent - Initial contribution + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HolidayModeValue { + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/IconID.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/IconID.java new file mode 100644 index 0000000000000..5d9cbe94c8d11 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/IconID.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class IconID { + private boolean settable; + private Float value; + private Float maxValue; + private Float minValue; + private Float stepValue; + private String unit; + + public boolean isSettable() { + return settable; + } + + public Float getValue() { + return value; + } + + public Float getMaxValue() { + return maxValue; + } + + public Float getMinValue() { + return minValue; + } + + public Float getStepValue() { + return stepValue; + } + + public String getUnit() { + return unit; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Ing.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Ing.java new file mode 100644 index 0000000000000..51ad8f7ef1ebf --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Ing.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class Ing { + @SerializedName("d") + private Float[] day; + @SerializedName("w") + private Float[] week; + @SerializedName("m") + private Float[] month; + + public Float[] getDay() { + return day; + } + + public Float[] getWeek() { + return week; + } + + public Float[] getMonth() { + return month; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ManagementPoint.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ManagementPoint.java new file mode 100644 index 0000000000000..41665f3b22789 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/ManagementPoint.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class ManagementPoint { + private String embeddedId; + private String managementPointType; + private String managementPointSubType; + private String managementPointCategory; + private GatwaySubValueBoolean daylightSavingTimeEnabled; + private GatwaySubValueString errorCode; + private GatwaySubValueString firmwareVersion; + private GatwaySubValueBoolean isFirmwareUpdateSupported; + private GatwaySubValueBoolean isInErrorState; + private GatwaySubValueBoolean ledEnabled; + private GatwaySubValueString ipAddress; + private GatwaySubValueString macAddress; + private GatwaySubValueString modelInfo; + private GatwaySubValueString regionCode; + private GatwaySubValueString serialNumber; + private GatwaySubValueString eepromVersion; + private GatwaySubValueString ssid; + private GatwaySubValueString timeZone; + private GatwaySubValueString wifiConnectionSSID; + private GatwaySubValueInteger wifiConnectionStrength; + private ConsumptionData consumptionData; + private DemandControl demandControl; + private GatwaySubValueString econoMode; + private FanControl fanControl; + private HolidayMode holidayMode; + private GatwaySubValueBoolean isHolidayModeActive; + private GatwaySubValueBoolean isInEmergencyState; + private GatwaySubValueString heatupMode; + private GatwaySubValueString setpointMode; + private GatwaySubValueBoolean isInWarningState; + private GatwaySubValueBoolean isInInstallerState; + private Name name; + private GatwaySubValueString onOffMode; + private GatwaySubValueString operationMode; + private IconID targetTemperature; + private GatwaySubValueString outdoorSilentMode; + private GatwaySubValueString powerfulMode; + private GatwaySubValueBoolean isPowerfulModeActive; + private SensoryData sensoryData; + private GatwaySubValueString streamerMode; + private TemperatureControl temperatureControl; + private GatwaySubValueString softwareVersion; + private GatwaySubValueString dryKeepSetting; + + public String getEmbeddedId() { + return embeddedId; + } + + public String getManagementPointType() { + return managementPointType; + } + + public String getManagementPointSubType() { + return managementPointSubType; + } + + public String getManagementPointCategory() { + return managementPointCategory; + } + + public GatwaySubValueBoolean getDaylightSavingTimeEnabled() { + return daylightSavingTimeEnabled; + } + + public GatwaySubValueString getErrorCode() { + return errorCode; + } + + public GatwaySubValueString getFirmwareVersion() { + return firmwareVersion; + } + + public GatwaySubValueBoolean getIsFirmwareUpdateSupported() { + return isFirmwareUpdateSupported; + } + + public GatwaySubValueBoolean getIsInErrorState() { + return isInErrorState; + } + + public GatwaySubValueBoolean getisInWarningState() { + return isInWarningState; + } + + public GatwaySubValueBoolean getIsInInstallerState() { + return isInInstallerState; + } + + public GatwaySubValueBoolean getIsInWarningState() { + return isInWarningState; + } + + public GatwaySubValueBoolean getIsLedEnabled() { + return ledEnabled; + } + + public GatwaySubValueString getIpAddress() { + return ipAddress; + } + + public GatwaySubValueString getMacAddress() { + return macAddress; + } + + public GatwaySubValueString getModelInfo() { + return modelInfo; + } + + public GatwaySubValueString getRegionCode() { + return regionCode; + } + + public GatwaySubValueString getSerialNumber() { + return serialNumber; + } + + public GatwaySubValueString getSsid() { + return ssid; + } + + public GatwaySubValueString getTimeZone() { + return timeZone; + } + + public GatwaySubValueString getWifiConnectionSSID() { + return wifiConnectionSSID; + } + + public GatwaySubValueInteger getWifiConnectionStrength() { + return wifiConnectionStrength; + } + + public ConsumptionData getConsumptionData() { + return consumptionData; + } + + public DemandControl getDemandControl() { + return demandControl; + } + + public HolidayMode getHolidayMode() { + return holidayMode; + } + + public GatwaySubValueBoolean getisHolidayModeActive() { + return isHolidayModeActive; + } + + public GatwaySubValueBoolean getIsInEmergencyState() { + return isInEmergencyState; + } + + public Name getName() { + return name; + } + + public String getNameValue() { + return name != null ? name.getValue() : ""; + } + + public GatwaySubValueString getOnOffMode() { + return onOffMode; + } + + public GatwaySubValueString getOperationMode() { + return operationMode; + } + + public GatwaySubValueString getHeatupMode() { + return heatupMode; + } + + public GatwaySubValueString getSetpointMode() { + return setpointMode; + } + + public IconID getTargetTemperature() { + return targetTemperature; + } + + public GatwaySubValueString getOutdoorSilentMode() { + return outdoorSilentMode; + } + + public GatwaySubValueString getPowerfulMode() { + return powerfulMode; + } + + public GatwaySubValueBoolean getIsPowerfulModeActive() { + return isPowerfulModeActive; + } + + public SensoryData getSensoryData() { + return sensoryData; + } + + public GatwaySubValueString getStreamerMode() { + return streamerMode; + } + + public TemperatureControl getTemperatureControl() { + return temperatureControl; + } + + public FanControl getFanControl() { + return fanControl; + } + + public GatwaySubValueString getEconoMode() { + return econoMode; + } + + public GatwaySubValueString getEepromVersion() { + return eepromVersion; + } + + public GatwaySubValueString getSoftwareVersion() { + return softwareVersion; + } + + public GatwaySubValueString getDryKeepSetting() { + return dryKeepSetting; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Name.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Name.java new file mode 100644 index 0000000000000..21648e35f141e --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Name.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class Name { + private boolean settable; + private Integer maxLength; + private String value; + + public boolean isSettable() { + return settable; + } + + public Integer getMaxLength() { + return maxLength; + } + + public String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OperationModes.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OperationModes.java new file mode 100644 index 0000000000000..2ad58ba9f0f8f --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OperationModes.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import org.openhab.binding.onecta.internal.api.Enums; + +/** + * @author Alexander Drent - Initial contribution + */ +public class OperationModes { + private OpertationMode heating; + private OpertationMode cooling; + private OpertationMode auto; + + public OpertationMode getOperationMode(Enums.OperationMode operationMode) { + if (operationMode.equals(Enums.OperationMode.HEAT)) { + return this.heating; + } else if (operationMode.equals(Enums.OperationMode.COLD)) { + return this.cooling; + } else if (operationMode.equals(Enums.OperationMode.AUTO)) { + return this.auto; + } else + return null; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OpertationMode.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OpertationMode.java new file mode 100644 index 0000000000000..450d6be93c93f --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/OpertationMode.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class OpertationMode { + private Setpoints setpoints; + + public Setpoints getSetpoints() { + return setpoints; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryData.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryData.java new file mode 100644 index 0000000000000..8cc06a42151eb --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryData.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class SensoryData { + private String ref; + private boolean settable; + private SensoryDataValue value; + + public String getRef() { + return ref; + } + + public boolean isSettable() { + return settable; + } + + public SensoryDataValue getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryDataValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryDataValue.java new file mode 100644 index 0000000000000..db9b3c25b9154 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/SensoryDataValue.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import org.openhab.binding.onecta.internal.api.Enums; + +/** + * @author Alexander Drent - Initial contribution + */ +public class SensoryDataValue { + private IconID roomTemperature; + private IconID roomHumidity; + private IconID outdoorTemperature; + private IconID leavingWaterTemperature; + private IconID tankTemperature; + private IconID deltaD; + private IconID fanMotorRotationSpeed; + private IconID heatExchangerTemperature; + private IconID suctionTemperature; + + public IconID getRoomTemperature() { + return roomTemperature; + } + + public IconID getRoomHumidity() { + return roomHumidity; + } + + public IconID getOutdoorTemperature() { + return outdoorTemperature; + } + + public IconID getLeavingWaterTemperature() { + return leavingWaterTemperature; + } + + public IconID getTankTemperature() { + return tankTemperature; + } + + public IconID getDeltaD() { + return deltaD; + } + + public IconID getFanMotorRotationSpeed() { + return fanMotorRotationSpeed; + } + + public IconID getHeatExchangerTemperature() { + return heatExchangerTemperature; + } + + public IconID getSuctionTemperature() { + return suctionTemperature; + } + + public IconID getSensorData(Enums.SensorData sensorData) { + + if (sensorData.equals(Enums.SensorData.ROOMTEMP)) { + return this.roomTemperature; + } else if (sensorData.equals(Enums.SensorData.ROOMHUMINITY)) { + return this.roomHumidity; + } else if (sensorData.equals(Enums.SensorData.OUTDOORTEMP)) { + return this.outdoorTemperature; + } else if (sensorData.equals(Enums.SensorData.LEAVINGWATERTEMP)) { + return this.leavingWaterTemperature; + } else + return null; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Setpoints.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Setpoints.java new file mode 100644 index 0000000000000..2e1ebaebcdf47 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Setpoints.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Alexander Drent - Initial contribution + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Setpoints { + private IconID roomTemperature; + private IconID leavingWaterTemperature; + private IconID leavingWaterOffset; + + private IconID domesticHotWaterTemperature; + + public IconID getRoomTemperature() { + return roomTemperature; + } + + public IconID getLeavingWaterTemperature() { + return leavingWaterTemperature; + } + + public IconID getLeavingWaterOffset() { + return leavingWaterOffset; + } + + public IconID getdomesticHotWaterTemperature() { + return domesticHotWaterTemperature; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControl.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControl.java new file mode 100644 index 0000000000000..ff6cb9809db72 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class TemperatureControl { + private String ref; + private boolean settable; + private TemperatureControlValue value; + + public boolean isSettable() { + return settable; + } + + public String getRef() { + return ref; + } + + public TemperatureControlValue getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControlValue.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControlValue.java new file mode 100644 index 0000000000000..ab3f93de09728 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/TemperatureControlValue.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +/** + * @author Alexander Drent - Initial contribution + */ +public class TemperatureControlValue { + private OperationModes operationModes; + + public OperationModes getOperationModes() { + return operationModes; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Unit.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Unit.java new file mode 100644 index 0000000000000..201d5278924a0 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Unit.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import java.util.List; +import java.util.UUID; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Alexander Drent - Initial contribution + */ +public class Unit { + @SerializedName("_id") + private String id; + @SerializedName("id") + private UUID initID; + private String type; + private String deviceModel; + private GatwaySubValueBoolean IsCloudConnectionUp; + private List managementPoints; + private String embeddedId; + private String timestamp; + + public String getId() { + return id; + } + + public UUID getInitID() { + return initID; + } + + public String getType() { + return type; + } + + public String getDeviceModel() { + return deviceModel; + } + + public GatwaySubValueBoolean getIsCloudConnectionUp() { + return IsCloudConnectionUp; + } + + public List getManagementPoints() { + return managementPoints; + } + + public String getEmbeddedId() { + return embeddedId; + } + + public String getTimestamp() { + return timestamp; + } + + public ManagementPoint findManagementPointsByType(String key) { + return managementPoints.stream() + .filter(managementPoint -> key.equals(managementPoint.getManagementPointType().toString())).findFirst() + .orElse(null); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Units.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Units.java new file mode 100644 index 0000000000000..ca1c0e91274ad --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/api/dto/units/Units.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.api.dto.units; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Alexander Drent - Initial contribution + */ +public class Units { + private List units; + + public Units() { + this.units = new ArrayList<>(); + } + + public List getAll() { + return this.units; + } + + public Unit get(int index) { + return this.units.get(index); + } + + public Unit findById(String key) { + return units.stream().filter(unit -> key.equals(unit.getId().toString())).findFirst().orElse(null); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationDataException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationDataException.java new file mode 100644 index 0000000000000..bd476babd6fa7 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationDataException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.exception; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DaikinCommunicationDataException extends DaikinCommunicationException { + + public DaikinCommunicationDataException(String message) { + super(message); + } + + public DaikinCommunicationDataException(Throwable ex) { + super(ex); + } + + public DaikinCommunicationDataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationException.java new file mode 100644 index 0000000000000..70b1925a1f888 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.exception; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class DaikinCommunicationException extends IOException { + + public DaikinCommunicationException(String message) { + super(message); + } + + public DaikinCommunicationException(Throwable ex) { + super(ex); + } + + public DaikinCommunicationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationForbiddenException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationForbiddenException.java new file mode 100644 index 0000000000000..59781d7b0fe66 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/exception/DaikinCommunicationForbiddenException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.exception; + +/** + * @author Alexander Drent - Initial contribution + */ +public class DaikinCommunicationForbiddenException extends DaikinCommunicationException { + + public DaikinCommunicationForbiddenException(String message) { + super(message); + } + + public DaikinCommunicationForbiddenException(Throwable ex) { + super(ex); + } + + public DaikinCommunicationForbiddenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/AbstractOnectaHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/AbstractOnectaHandler.java new file mode 100644 index 0000000000000..3b9ee39143a62 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/AbstractOnectaHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import java.util.Optional; + +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.BaseThingHandler; + +/** + * The {@link AbstractOnectaHandler} abstract for all Onecta Handlers + * + * @author Alexander Drent - Initial contribution + */ +public abstract class AbstractOnectaHandler extends BaseThingHandler { + + public AbstractOnectaHandler(Thing thing) { + super(thing); + } + + public abstract void refreshDevice(); + + public String getUnitID() { + return Optional.ofNullable(thing.getConfiguration().get("unitID")).orElse("").toString(); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaBridgeHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaBridgeHandler.java new file mode 100644 index 0000000000000..c3465d04858d8 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaBridgeHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import static org.openhab.binding.onecta.internal.OnectaBridgeConstants.*; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.OnectaConnectionClient; +import org.openhab.binding.onecta.internal.api.dto.units.Units; +import org.openhab.binding.onecta.internal.exception.DaikinCommunicationException; +import org.openhab.binding.onecta.internal.service.DeviceDiscoveryService; +import org.openhab.core.thing.*; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OnectaBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaBridgeHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(OnectaBridgeHandler.class); + + private @Nullable ScheduledFuture pollingJob; + + private Units units = new Units(); + private OnectaConnectionClient onectaConnectionClient; + + public Units getUnits() { + return units; + } + + private @Nullable DeviceDiscoveryService deviceDiscoveryService; + + /** + * Defines a runnable for a discovery + */ + Runnable runnable = new Runnable() { + @Override + public void run() { + if (deviceDiscoveryService != null) { + deviceDiscoveryService.startScan(); + } + } + }; + + public OnectaBridgeHandler(Bridge bridge) { + super(bridge); + onectaConnectionClient = OnectaConfiguration.getOnectaConnectionClient(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + logger.debug("initialize."); + updateStatus(ThingStatus.UNKNOWN); + if (onectaConnectionClient.isOnline()) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "@text/offline.communication-error"); + } + + pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevices, 0, + Integer.parseInt(thing.getConfiguration().get(CONFIG_PAR_REFRESHINTERVAL).toString()), + TimeUnit.SECONDS); + } + + @Override + public void dispose() { + logger.debug("Handler disposed."); + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + } + + private void pollDevices() { + logger.debug("pollDevices."); + + if (getThing().getStatus().equals(ThingStatus.ONLINE)) { + + try { + onectaConnectionClient.refreshUnitsData(); + updateStatus(ThingStatus.ONLINE); + + List things = getThing().getThings(); + for (Thing t : things) { + if (t.isEnabled()) { + ((AbstractOnectaHandler) Objects.requireNonNull(t.getHandler())).refreshDevice(); + } + } + } catch (DaikinCommunicationException e) { + logger.debug("DaikinCommunicationException: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (NullPointerException e) { + logger.debug("NullPointerException: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, e.getMessage()); + } + } + } + + public void setDiscoveryService(DeviceDiscoveryService deviceDiscoveryService) { + this.deviceDiscoveryService = deviceDiscoveryService; + } + + @Override + public Collection> getServices() { + return Set.of(DeviceDiscoveryService.class); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaDeviceHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaDeviceHandler.java new file mode 100644 index 0000000000000..f442dfc96ca1a --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaDeviceHandler.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import static org.openhab.binding.onecta.internal.OnectaClimateControlConstants.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.Enums; +import org.openhab.binding.onecta.internal.service.ChannelsRefreshDelay; +import org.openhab.binding.onecta.internal.service.DataTransportService; +import org.openhab.binding.onecta.internal.type.TypeHandler; +import org.openhab.core.library.types.*; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OnectaDeviceHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaDeviceHandler extends AbstractOnectaHandler { + + private final Logger logger = LoggerFactory.getLogger(OnectaDeviceHandler.class); + + private @Nullable OnectaConfiguration config; + + private @Nullable ScheduledFuture pollingJob; + + private final DataTransportService dataTransService; + private @Nullable ChannelsRefreshDelay channelsRefreshDelay; + + public OnectaDeviceHandler(Thing thing) { + super(thing); + this.dataTransService = new DataTransportService(getUnitID(), Enums.ManagementPoint.CLIMATECONTROL); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + try { + channelsRefreshDelay.add(channelUID.getId()); + switch (channelUID.getId()) { + case CHANNEL_AC_POWER: + if (command instanceof OnOffType) { + dataTransService.setPowerOnOff(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_AC_POWERFULMODE: + if (command instanceof OnOffType) { + dataTransService.setPowerfulModeOnOff(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_AC_OPERATIONMODE: + if (command instanceof StringType) { + dataTransService.setCurrentOperationMode(Enums.OperationMode.valueOf(command.toString())); + } + break; + case CHANNEL_AC_TEMP: + if (command instanceof QuantityType) { + dataTransService.setCurrentTemperatureSet(((QuantityType) command).floatValue()); + } + break; + case CHANNEL_AC_FANSPEED: + if (command instanceof StringType) { + dataTransService.setFanSpeed(Enums.FanSpeed.valueOf(command.toString())); + } + break; + case CHANNEL_AC_FANMOVEMENT: + if (command instanceof StringType) { + dataTransService.setCurrentFanDirection(Enums.FanMovement.valueOf(command.toString())); + } + break; + case CHANNEL_AC_FANMOVEMENT_HOR: + if (command instanceof StringType) { + dataTransService.setCurrentFanDirectionHor(Enums.FanMovementHor.valueOf(command.toString())); + } + break; + case CHANNEL_AC_FANMOVEMENT_VER: + if (command instanceof StringType) { + dataTransService.setCurrentFanDirectionVer(Enums.FanMovementVer.valueOf(command.toString())); + } + break; + case CHANNEL_AC_ECONOMODE: + if (command instanceof OnOffType) { + dataTransService.setEconoMode(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_AC_STREAMER: + if (command instanceof OnOffType) { + dataTransService.setStreamerMode(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_AC_HOLIDAYMODE: + if (command instanceof OnOffType) { + dataTransService.setHolidayMode(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_AC_DEMANDCONTROL: + if (command instanceof StringType) { + dataTransService.setDemandControl(Enums.DemandControl.valueOf(command.toString())); + } + break; + case CHANNEL_AC_DEMANDCONTROLFIXEDVALUE: + if (command instanceof QuantityType) { + dataTransService.setDemandControlFixedValue(((QuantityType) command).intValue()); + } + break; + case CHANNEL_AC_TARGETTEMP: + if (command instanceof QuantityType) { + dataTransService.setTargetTemperatur(((QuantityType) command).intValue()); + } + break; + case CHANNEL_AC_SETPOINT_LEAVINGWATER_OFFSET: + if (command instanceof QuantityType) { + dataTransService.setSetpointLeavingWaterOffset(((QuantityType) command).intValue()); + } + break; + case CHANNEL_AC_SETPOINT_LEAVINGWATER_TEMP: + if (command instanceof QuantityType) { + dataTransService.setSetpointLeavingWaterTemperature(((QuantityType) command).intValue()); + } + break; + } + + updateStatus(ThingStatus.ONLINE); + } catch (RuntimeException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + @Override + public void initialize() { + config = getConfigAs(OnectaConfiguration.class); + channelsRefreshDelay = new ChannelsRefreshDelay( + Long.parseLong(thing.getConfiguration().get("refreshDelay").toString()) * 1000); + if (dataTransService.isAvailable()) { + refreshDevice(); + } + thing.setProperty(CHANNEL_AC_NAME, ""); + } + + @Override + public void refreshDevice() { + dataTransService.refreshUnit(); + + if (dataTransService.isAvailable()) { + logger.debug("refreshDevice : {}, {}", dataTransService.getManagementPointType(), + dataTransService.getUnitName()); + + updateStatus(ThingStatus.ONLINE); + getThing().setProperty(PROPERTY_AC_NAME, dataTransService.getUnitName()); + + updateState(CHANNEL_AC_RAWDATA, getRawData()); + + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_POWER)) { + updateState(CHANNEL_AC_POWER, getPowerOnOff()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_POWERFULMODE)) { + updateState(CHANNEL_AC_POWERFULMODE, getPowerfulMode()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_OPERATIONMODE)) { + updateState(CHANNEL_AC_OPERATIONMODE, getCurrentOperationMode()); + } + + // Set Temp + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TEMP)) { + updateState(CHANNEL_AC_TEMP, getCurrentTemperatureSet()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TEMPMIN)) { + updateState(CHANNEL_AC_TEMPMIN, getCurrentTemperatureSetMin()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TEMPMAX)) { + updateState(CHANNEL_AC_TEMPMAX, getCurrentTemperatureSetMax()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TEMPSTEP)) { + updateState(CHANNEL_AC_TEMPSTEP, getCurrentTemperatureSetStep()); + } + + // Target temp + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TARGETTEMP)) { + updateState(CHANNEL_AC_TARGETTEMP, getTargetTemperatur()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TARGETTEMPMIN)) { + updateState(CHANNEL_AC_TARGETTEMPMIN, getTargetTemperaturMin()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TARGETTEMPMAX)) { + updateState(CHANNEL_AC_TARGETTEMPMAX, getTargetTemperaturMax()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_TARGETTEMPSTEP)) { + updateState(CHANNEL_AC_TARGETTEMPSTEP, getTargetTemperaturStep()); + } + + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_SETPOINT_LEAVINGWATER_OFFSET)) { + updateState(CHANNEL_AC_SETPOINT_LEAVINGWATER_OFFSET, getSetpointLeavingWaterOffset()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_SETPOINT_LEAVINGWATER_TEMP)) { + updateState(CHANNEL_AC_SETPOINT_LEAVINGWATER_TEMP, getSetpointLeavingWaterTemperature()); + } + + // Fan + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_FANMOVEMENT)) { + updateState(CHANNEL_AC_FANMOVEMENT, getCurrentFanDirection()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_FANSPEED)) { + updateState(CHANNEL_AC_FANSPEED, getCurrentFanspeed()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_ECONOMODE)) { + updateState(CHANNEL_AC_ECONOMODE, getEconoMode()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_STREAMER)) { + updateState(CHANNEL_AC_STREAMER, getStreamerMode()); + } + + updateState(CHANNEL_INDOOR_TEMP, getIndoorTemperature()); + updateState(CHANNEL_OUTDOOR_TEMP, getOutdoorTemperature()); + updateState(CHANNEL_LEAVINGWATER_TEMP, getLeavingWaterTemperature()); + updateState(CHANNEL_INDOOR_HUMIDITY, getIndoorHumidity()); + updateState(CHANNEL_AC_TIMESTAMP, getTimeStamp()); + + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_FANMOVEMENT_HOR)) { + updateState(CHANNEL_AC_FANMOVEMENT_HOR, getCurrentFanDirectionHor()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_FANMOVEMENT_VER)) { + updateState(CHANNEL_AC_FANMOVEMENT_VER, getCurrentFanDirectionVer()); + } + + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_HOLIDAYMODE)) { + updateState(CHANNEL_AC_HOLIDAYMODE, getHolidayMode()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_DEMANDCONTROL)) { + updateState(CHANNEL_AC_DEMANDCONTROL, getDemandControl()); + } + + // DEMANDCONTROL + if (channelsRefreshDelay.isDelayPassed(CHANNEL_AC_DEMANDCONTROLFIXEDVALUE)) { + updateState(CHANNEL_AC_DEMANDCONTROLFIXEDVALUE, getDemandControlFixedValue()); + } + updateState(CHANNEL_AC_DEMANDCONTROLFIXEDSTEPVALUE, getDemandControlFixedStepValue()); + updateState(CHANNEL_AC_DEMANDCONTROLFIXEDMINVALUE, getDemandControlFixedMinValue()); + updateState(CHANNEL_AC_DEMANDCONTROLFIXEDMAXVALUE, getDemandControlFixedMaxValue()); + + // Energy consumption Cooling Day + if (dataTransService.getConsumptionCoolingDay() != null) { + for (int i = 0; i < dataTransService.getConsumptionCoolingDay().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_COOLING_DAY, i), + dataTransService.getConsumptionCoolingDay()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionCoolingDay()[i])); + } + } + // Energy consumption Cooling Week + if (dataTransService.getConsumptionCoolingWeek() != null) { + for (int i = 0; i < dataTransService.getConsumptionCoolingWeek().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_COOLING_WEEK, i), + dataTransService.getConsumptionCoolingWeek()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionCoolingWeek()[i])); + } + } + // Energy consumption Cooling Month + if (dataTransService.getConsumptionCoolingMonth() != null) { + for (int i = 0; i < dataTransService.getConsumptionCoolingMonth().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_COOLING_MONTH, i), + dataTransService.getConsumptionCoolingMonth()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionCoolingMonth()[i])); + } + } + + // Energy consumption Heating Day + if (dataTransService.getConsumptionHeatingDay() != null) { + for (int i = 0; i < dataTransService.getConsumptionHeatingDay().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_HEATING_DAY, i), + dataTransService.getConsumptionHeatingDay()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionHeatingDay()[i])); + } + } + // Energy consumption Heating Week + if (dataTransService.getConsumptionHeatingWeek() != null) { + for (int i = 0; i < dataTransService.getConsumptionHeatingWeek().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_HEATING_WEEK, i), + dataTransService.getConsumptionHeatingWeek()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionHeatingWeek()[i])); + } + } + // Energy consumption Heating Month + if (dataTransService.getConsumptionHeatingMonth() != null) { + for (int i = 0; i < dataTransService.getConsumptionHeatingMonth().length; i++) { + updateState(String.format(CHANNEL_AC_ENERGY_HEATING_MONTH, i), + dataTransService.getConsumptionHeatingMonth()[i] == null ? UnDefType.UNDEF + : new DecimalType(dataTransService.getConsumptionHeatingMonth()[i])); + } + } + // calculate current day and year energy consumption + updateState(CHANNEL_AC_ENERGY_HEATING_CURRENT_DAY, getEnergyHeatingCurrentDay()); + updateState(CHANNEL_AC_ENERGY_HEATING_CURRENT_YEAR, getEnergyHeatingCurrentYear()); + updateState(CHANNEL_AC_ENERGY_COOLING_CURRENT_DAY, getEnergyCoolingCurrentDay()); + updateState(CHANNEL_AC_ENERGY_COOLING_CURRENT_YEAR, getEnergyCoolingCurrentYear()); + + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + getThing().setProperty(PROPERTY_AC_NAME, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + } + } + + private State getRawData() { + return TypeHandler.stringType(dataTransService.getRawData()); + } + + private State getPowerOnOff() { + return TypeHandler.onOffType(dataTransService.getPowerOnOff()); + } + + private State getPowerfulMode() { + return TypeHandler.onOffType(dataTransService.getPowerfulModeOnOff()); + } + + private State getCurrentOperationMode() { + return TypeHandler.stringType(dataTransService.getCurrentOperationMode()); + } + + private State getCurrentFanspeed() { + return TypeHandler.stringType(dataTransService.getCurrentFanspeed()); + } + + private State getCurrentTemperatureSet() { + return TypeHandler.decimalType(dataTransService.getCurrentTemperatureSet()); + } + + private State getSetpointLeavingWaterTemperature() { + return TypeHandler.decimalType(dataTransService.getSetpointLeavingWaterTemperature()); + } + + private State getSetpointLeavingWaterOffset() { + return TypeHandler.decimalType(dataTransService.getSetpointLeavingWaterOffset()); + } + + private State getCurrentTemperatureSetMin() { + return TypeHandler.decimalType(dataTransService.getCurrentTemperatureSetMin()); + } + + private State getCurrentTemperatureSetMax() { + return TypeHandler.decimalType(dataTransService.getCurrentTemperatureSetMax()); + } + + private State getCurrentTemperatureSetStep() { + return TypeHandler.decimalType(dataTransService.getCurrentTemperatureSetStep()); + } + + private State getOutdoorTemperature() { + return TypeHandler.decimalType(dataTransService.getOutdoorTemperature()); + } + + private State getIndoorTemperature() { + return TypeHandler.decimalType(dataTransService.getIndoorTemperature()); + } + + private State getLeavingWaterTemperature() { + return TypeHandler.decimalType(dataTransService.getLeavingWaterTemperature()); + } + + private State getTargetTemperatur() { + return TypeHandler.decimalType(dataTransService.getTargetTemperatur()); + } + + private State getTargetTemperaturMax() { + return TypeHandler.decimalType(dataTransService.getTargetTemperaturMax()); + } + + private State getTargetTemperaturMin() { + return TypeHandler.decimalType(dataTransService.getTargetTemperaturMin()); + } + + private State getTargetTemperaturStep() { + return TypeHandler.decimalType(dataTransService.getTargetTemperaturStep()); + } + + private State getDemandControlFixedValue() { + return TypeHandler.decimalType(dataTransService.getDemandControlFixedValue()); + } + + private State getDemandControlFixedStepValue() { + return TypeHandler.decimalType(dataTransService.getDemandControlFixedStepValue()); + } + + private State getDemandControlFixedMinValue() { + return TypeHandler.decimalType(dataTransService.getDemandControlFixedMinValue()); + } + + private State getDemandControlFixedMaxValue() { + return TypeHandler.decimalType(dataTransService.getDemandControlFixedMaxValue()); + } + + private State getIndoorHumidity() { + return TypeHandler.decimalType(dataTransService.getIndoorHumidity()); + } + + private State getTimeStamp() { + return TypeHandler.dateTimeType(dataTransService.getTimeStamp()); + } + + private State getEconoMode() { + return TypeHandler.onOffType(dataTransService.getEconoMode()); + } + + private State getStreamerMode() { + return TypeHandler.onOffType(dataTransService.getStreamerMode()); + } + + private State getCurrentFanDirectionHor() { + return TypeHandler.stringType(dataTransService.getCurrentFanDirectionHor()); + } + + private State getCurrentFanDirectionVer() { + return TypeHandler.stringType(dataTransService.getCurrentFanDirectionVer()); + } + + private State getCurrentFanDirection() { + return TypeHandler.stringType(dataTransService.getCurrentFanDirection()); + } + + private State getHolidayMode() { + return TypeHandler.onOffType(dataTransService.getHolidayMode()); + } + + private State getDemandControl() { + return TypeHandler.stringType(dataTransService.getDemandControl()); + } + + private int getCurrentDayOfWeek() { + LocalDate today = LocalDate.now(); + return today.getDayOfWeek().getValue() - 1; + } + + private State getEnergyHeatingCurrentDay() { + State state = Optional.ofNullable(dataTransService.getConsumptionHeatingWeek()) + .filter(consumptionArray -> consumptionArray.length > 7 + getCurrentDayOfWeek()) // + .map(consumptionArray -> consumptionArray[7 + getCurrentDayOfWeek()]) // + .map(TypeHandler::decimalType) // + .orElse(UnDefType.UNDEF); // + if (state == null) { + state = UnDefType.UNDEF; + } + return state; + } + + private State getEnergyCoolingCurrentDay() { + State state = Optional.ofNullable(dataTransService.getConsumptionCoolingWeek()) + .filter(consumptionArray -> consumptionArray.length > 7 + getCurrentDayOfWeek()) // + .map(consumptionArray -> consumptionArray[7 + getCurrentDayOfWeek()]) // + .map(TypeHandler::decimalType) // + .orElse(UnDefType.UNDEF); // + if (state == null) { + state = UnDefType.UNDEF; + } + return state; + } + + private Boolean isFirst2HourOfYear() { + LocalDateTime localDateTime = LocalDateTime.now(); + return localDateTime.getDayOfYear() == 1 && (localDateTime.getHour() == 0 || localDateTime.getHour() == 1); + } + + private State getEnergyCoolingCurrentYear() { + double total = 0; + try { + if (!isFirst2HourOfYear()) { + for (int i = 12; i <= 23; i++) { + if (dataTransService.getConsumptionCoolingMonth()[i] != null) { + total += dataTransService.getConsumptionCoolingMonth()[i]; + } + } + } + return new DecimalType(Math.round(total * 10) / 10D); + } catch (NullPointerException | IndexOutOfBoundsException e) { + return UnDefType.UNDEF; + } + } + + private State getEnergyHeatingCurrentYear() { + double total = 0; + try { + if (!isFirst2HourOfYear()) { + for (int i = 12; i <= 23; i++) { + if (dataTransService.getConsumptionHeatingMonth()[i] != null) { + total += dataTransService.getConsumptionHeatingMonth()[i]; + } + } + } + return TypeHandler.decimalType(Math.round(total * 10) / 10D); + } catch (NullPointerException | IndexOutOfBoundsException e) { + return UnDefType.UNDEF; + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaGatewayHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaGatewayHandler.java new file mode 100644 index 0000000000000..1cd54b493707c --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaGatewayHandler.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import static org.openhab.binding.onecta.internal.OnectaGatewayConstants.*; + +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.Enums; +import org.openhab.binding.onecta.internal.service.DataTransportService; +import org.openhab.binding.onecta.internal.type.TypeHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OnectaGatewayHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaGatewayHandler extends AbstractOnectaHandler { + + private final Logger logger = LoggerFactory.getLogger(OnectaGatewayHandler.class); + + private @Nullable OnectaConfiguration config; + + private @Nullable ScheduledFuture pollingJob; + + private final DataTransportService dataTransService; + + public OnectaGatewayHandler(Thing thing) { + super(thing); + dataTransService = new DataTransportService(getUnitID(), Enums.ManagementPoint.GATEWAY); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + config = getConfigAs(OnectaConfiguration.class); + if (dataTransService.isAvailable()) { + refreshDevice(); + } + thing.setProperty(PROPERTY_GW_NAME, ""); + } + + @Override + public void refreshDevice() { + dataTransService.refreshUnit(); + + if (dataTransService.isAvailable()) { + logger.debug("refreshDevice : {}, {}", dataTransService.getManagementPointType(), + dataTransService.getUnitName()); + updateStatus(ThingStatus.ONLINE); + getThing().setProperty(PROPERTY_GW_NAME, dataTransService.getUnitName()); + + updateState(CHANNEL_GW_DAYLIGHTSAVINGENABLED, getDaylightSavingTimeEnabled()); + updateState(CHANNEL_GW_FIRMWAREVERSION, getFirmwareVerion()); + updateState(CHANNEL_GW_IS_FIRMWAREUPDATE_SUPPORTED, getIsFirmwareUpdateSupported()); + updateState(CHANNEL_GW_IS_IN_ERROR_STATE, getIsInErrorState()); + updateState(CHANNEL_GW_LED_ENABLED, getIsLedEnabled()); + updateState(CHANNEL_GW_REGION_CODE, getRegionCode()); + updateState(CHANNEL_GW_SERIAL_NUMBER, getSerialNumber()); + updateState(CHANNEL_GW_SSID, getSsid()); + updateState(CHANNEL_GW_TIME_ZONE, getTimeZone()); + updateState(CHANNEL_GW_WIFICONNENTION_SSID, getWifiConnectionSsid()); + updateState(CHANNEL_GW_WIFICONNENTION_STRENGTH, getWifiConnectionStrength()); + updateState(CHANNEL_GW_IP_ADDRESS, getIpAddress()); + updateState(CHANNEL_GW_MODEL_INFO, getModelInfo()); + updateState(CHANNEL_GW_MAC_ADDRESS, getMacAddress()); + + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + getThing().setProperty(PROPERTY_GW_NAME, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + } + } + + private State getDaylightSavingTimeEnabled() { + return TypeHandler.onOffType(this.dataTransService.getDaylightSavingTimeEnabled()); + } + + private State getFirmwareVerion() { + return TypeHandler.stringType(this.dataTransService.getFirmwareVerion()); + } + + private State getIsFirmwareUpdateSupported() { + return TypeHandler.onOffType(this.dataTransService.getIsFirmwareUpdateSupported()); + } + + private State getIsInErrorState() { + return TypeHandler.onOffType(this.dataTransService.getIsInErrorState()); + } + + private State getIsLedEnabled() { + return TypeHandler.onOffType(this.dataTransService.getIsLedEnabled()); + } + + private State getRegionCode() { + return TypeHandler.stringType(this.dataTransService.getRegionCode()); + } + + private State getSerialNumber() { + return TypeHandler.stringType(this.dataTransService.getSerialNumber()); + } + + private State getSsid() { + return TypeHandler.stringType(this.dataTransService.getSsid()); + } + + private State getTimeZone() { + return TypeHandler.stringType(this.dataTransService.getTimeZone()); + } + + private State getWifiConnectionSsid() { + return TypeHandler.stringType(this.dataTransService.getWifiConectionSSid()); + } + + private State getWifiConnectionStrength() { + return TypeHandler.decimalType(this.dataTransService.getWifiConectionStrength()); + } + + private State getModelInfo() { + return TypeHandler.stringType(this.dataTransService.getModelInfo()); + } + + private State getIpAddress() { + return TypeHandler.stringType(this.dataTransService.getIpAddress()); + } + + private State getMacAddress() { + return TypeHandler.stringType(this.dataTransService.getMacAddress()); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaIndoorUnitHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaIndoorUnitHandler.java new file mode 100644 index 0000000000000..b1a3bd5416682 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaIndoorUnitHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import static org.openhab.binding.onecta.internal.OnectaIndoorUnitConstants.*; + +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.Enums; +import org.openhab.binding.onecta.internal.service.DataTransportService; +import org.openhab.binding.onecta.internal.type.TypeHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OnectaIndoorUnitHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaIndoorUnitHandler extends AbstractOnectaHandler { + + private final Logger logger = LoggerFactory.getLogger(OnectaIndoorUnitHandler.class); + + private @Nullable OnectaConfiguration config; + + private @Nullable ScheduledFuture pollingJob; + + private final DataTransportService dataTransService; + + public OnectaIndoorUnitHandler(Thing thing) { + super(thing); + dataTransService = new DataTransportService(getUnitID(), Enums.ManagementPoint.INDOORUNIT); + } + + @Override + public void initialize() { + config = getConfigAs(OnectaConfiguration.class); + if (dataTransService.isAvailable()) { + refreshDevice(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void refreshDevice() { + dataTransService.refreshUnit(); + + if (dataTransService.isAvailable()) { + logger.debug("refreshDevice : {}, {}", dataTransService.getManagementPointType(), + dataTransService.getUnitName()); + + updateStatus(ThingStatus.ONLINE); + + getThing().setProperty(PROPERTY_IDU_MODELINFO, getModelInfo().toString()); + getThing().setProperty(PROPERTY_IDU_SOFTWAREVERSION, getSoftwareVerion().toString()); + getThing().setProperty(PROPERTY_IDU_EEPROMVERSION, getEepromVerion().toString()); + + updateState(CHANNEL_IDU_ISKEEPDRY, getDryKeepSetting()); + updateState(CHANNEL_IDU_FANSPEED, getFanMotorRotationSpeed()); + updateState(CHANNEL_IDU_DELTAD, getDeltaD()); + updateState(CHANNEL_IDU_HEATEXCHANGETEMP, getHeatExchangerTemperature()); + updateState(CHANNEL_IDU_SUCTIONTEMP, getSuctionTemperature()); + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + } + } + + private State getModelInfo() { + return TypeHandler.stringType(this.dataTransService.getModelInfo()); + } + + private State getSoftwareVerion() { + return TypeHandler.stringType(dataTransService.getSoftwareVersion()); + } + + private State getEepromVerion() { + return TypeHandler.stringType(dataTransService.getEepromVerion()); + } + + private State getDryKeepSetting() { + return TypeHandler.onOffType(dataTransService.getDryKeepSetting()); + } + + private State getFanMotorRotationSpeed() { + return TypeHandler.decimalType(dataTransService.getFanMotorRotationSpeed()); + } + + private State getDeltaD() { + return TypeHandler.decimalType(dataTransService.getDeltaD()); + } + + private State getHeatExchangerTemperature() { + return TypeHandler.decimalType(dataTransService.getHeatExchangerTemperature()); + } + + private State getSuctionTemperature() { + return TypeHandler.decimalType(dataTransService.getSuctionTemperature()); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaWaterTankHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaWaterTankHandler.java new file mode 100644 index 0000000000000..10713e09c929c --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/handler/OnectaWaterTankHandler.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.handler; + +import static org.openhab.binding.onecta.internal.OnectaWaterTankConstants.*; + +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaConfiguration; +import org.openhab.binding.onecta.internal.api.Enums; +import org.openhab.binding.onecta.internal.service.ChannelsRefreshDelay; +import org.openhab.binding.onecta.internal.service.DataTransportService; +import org.openhab.binding.onecta.internal.type.TypeHandler; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link OnectaWaterTankHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Alexander Drent - Initial contribution + */ +@NonNullByDefault +public class OnectaWaterTankHandler extends AbstractOnectaHandler { + + private final Logger logger = LoggerFactory.getLogger(OnectaWaterTankHandler.class); + + private @Nullable OnectaConfiguration config; + + private @Nullable ScheduledFuture pollingJob; + + private final DataTransportService dataTransService; + private @Nullable ChannelsRefreshDelay channelsRefreshDelay; + + public OnectaWaterTankHandler(Thing thing) { + super(thing); + dataTransService = new DataTransportService(getUnitID(), Enums.ManagementPoint.WATERTANK); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + try { + channelsRefreshDelay.add(channelUID.getId()); + switch (channelUID.getId()) { + case CHANNEL_HWT_POWER: + if (command instanceof OnOffType) { + dataTransService.setPowerOnOff(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_HWT_POWERFUL_MODE: + if (command instanceof OnOffType) { + dataTransService.setPowerfulModeOnOff(Enums.OnOff.valueOf(command.toString())); + } + break; + case CHANNEL_HWT_SETTEMP: + if (command instanceof QuantityType) { + dataTransService.setCurrentTankTemperatureSet(((QuantityType) command).floatValue()); + } + break; + } + + updateStatus(ThingStatus.ONLINE); + } catch (RuntimeException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + @Override + public void initialize() { + config = getConfigAs(OnectaConfiguration.class); + channelsRefreshDelay = new ChannelsRefreshDelay( + Long.parseLong(thing.getConfiguration().get("refreshDelay").toString()) * 1000); + if (dataTransService.isAvailable()) { + refreshDevice(); + } + thing.setProperty(PROPERTY_HWT_NAME, ""); + } + + @Override + public void refreshDevice() { + dataTransService.refreshUnit(); + + if (dataTransService.isAvailable()) { + logger.debug("refreshDevice : {}, {}", dataTransService.getManagementPointType(), + dataTransService.getUnitName()); + + updateStatus(ThingStatus.ONLINE); + getThing().setProperty(PROPERTY_HWT_NAME, dataTransService.getUnitName()); + + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_POWER)) { + updateState(CHANNEL_HWT_POWER, getCurrentOnOff()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_OPERATION_MODE)) { + updateState(CHANNEL_HWT_OPERATION_MODE, getCurrentOperationMode()); + } + + updateState(CHANNEL_HWT_ERRORCODE, getErrorState()); + updateState(CHANNEL_HWT_IS_IN_EMERGENCY_STATE, getIsInEmergencyState()); + updateState(CHANNEL_HWT_IS_IN_ERROR_STATE, getIsInErrorState()); + updateState(CHANNEL_HWT_IS_IN_INSTALLER_STATE, getIsInInstallerState()); + updateState(CHANNEL_HWT_IS_IN_WARNING_STATE, getIsInWarningState()); + + updateState(CHANNEL_HWT_IS_HOLIDAY_MODE_ACTIVE, getIsHolidayModeActive()); + updateState(CHANNEL_HWT_POWERFUL_MODE, getPowerfulMode()); + + updateState(CHANNEL_HWT_HEATUP_MODE, getHeatupMode()); + updateState(CHANNEL_HWT_TANK_TEMPERATURE, getTankTemperatur()); + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_SETTEMP)) { + updateState(CHANNEL_HWT_SETTEMP, getCurrentTankTemperatureSet()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_SETTEMP_MIN)) { + updateState(CHANNEL_HWT_SETTEMP_MIN, getCurrentTankTemperatureSetMin()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_SETTEMP_MAX)) { + updateState(CHANNEL_HWT_SETTEMP_MAX, getCurrentTankTemperatureSetMax()); + } + if (channelsRefreshDelay.isDelayPassed(CHANNEL_HWT_SETTEMP_STEP)) { + updateState(CHANNEL_HWT_SETTEMP_STEP, getCurrentTankTemperatureSetStep()); + } + + updateState(CHANNEL_HWT_SETPOINT_MODE, getSetpointMode()); + + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + getThing().setProperty(PROPERTY_HWT_NAME, + OnectaConfiguration.getTranslation().getText("unknown.unitid-not-exists")); + } + } + + private State getCurrentOnOff() { + return TypeHandler.onOffType(dataTransService.getPowerOnOff()); + } + + private State getCurrentOperationMode() { + return TypeHandler.stringType(dataTransService.getCurrentOperationMode()); + } + + private State getSetpointMode() { + return TypeHandler.stringType(dataTransService.getSetpointMode()); + } + + private State getIsHolidayModeActive() { + return TypeHandler.onOffType(dataTransService.getIsHolidayModeActive()); + } + + private State getTankTemperatur() { + return TypeHandler.decimalType(dataTransService.getTankTemperature()); + } + + private State getHeatupMode() { + return TypeHandler.stringType(this.dataTransService.getHeatupMode()); + } + + private State getIsInErrorState() { + return TypeHandler.onOffType(this.dataTransService.getIsInErrorState()); + } + + private State getErrorState() { + return TypeHandler.stringType(this.dataTransService.getErrorCode()); + } + + private State getIsInEmergencyState() { + return TypeHandler.onOffType(this.dataTransService.getIsInEmergencyState()); + } + + private State getIsInInstallerState() { + return TypeHandler.onOffType(this.dataTransService.getIsInInstallerState()); + } + + private State getIsInWarningState() { + return TypeHandler.onOffType(this.dataTransService.getIsInWarningState()); + } + + private State getPowerfulMode() { + return TypeHandler.onOffType(this.dataTransService.getPowerfulModeOnOff()); + } + + private State getCurrentTankTemperatureSet() { + return TypeHandler.decimalType(dataTransService.getCurrentTankTemperatureSet()); + } + + private State getCurrentTankTemperatureSetMin() { + return TypeHandler.decimalType(dataTransService.getCurrentTankTemperatureSetMin()); + } + + private State getCurrentTankTemperatureSetMax() { + return TypeHandler.decimalType(dataTransService.getCurrentTankTemperatureSetMax()); + } + + private State getCurrentTankTemperatureSetStep() { + return TypeHandler.decimalType(dataTransService.getCurrentTankTemperatureSetStep()); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthException.java new file mode 100644 index 0000000000000..22347c895b351 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.auth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Indicates an error in the OAuth2 authorization process. + * + * @author Roland Edelhoff - Initial contribution + */ +@NonNullByDefault +public class OAuthException extends RuntimeException { + private static final long serialVersionUID = -1863609233382694104L; + + public OAuthException(final String message) { + super(message); + } + + public OAuthException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefreshListener.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefreshListener.java new file mode 100644 index 0000000000000..6e639bde356f4 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefreshListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.auth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Listener that is invoked when an OAuth 2 access token was refreshed. + * + * @author Björn Lange - Initial contribution + */ +@NonNullByDefault +public interface OAuthTokenRefreshListener { + /** + * Invoked when a new access token becomes available. + * + * @param accessToken The new access token. + */ + void onNewAccessToken(String accessToken); +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefresher.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefresher.java new file mode 100644 index 0000000000000..732b507c2338f --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OAuthTokenRefresher.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.auth; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An {@link OAuthTokenRefresher} offers convenient access to OAuth 2 authentication related functionality, + * especially refreshing the access token. + * + * @author Roland Edelhoff - Initial contribution + * @author Björn Lange - Allow removing tokens from the storage + */ +@NonNullByDefault +public interface OAuthTokenRefresher { + /** + * Sets the listener that is called when the access token was refreshed. + * + * @param listener The listener to register. + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @throws OAuthException if the listener needs to be registered at an underlying service which is not available + * because the account has not yet been authorized + */ + void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle); + + /** + * Unsets a listener. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + */ + void unsetRefreshListener(String serviceHandle); + + /** + * Refreshes the access and refresh tokens for the given service handle. If an {@link OAuthTokenRefreshListener} is + * registered for the service handle then it is notified after the refresh has completed. + * + * This call will succeed if the access token is still valid or a valid refresh token exists, which can be used to + * refresh the expired access token. If refreshing fails, an {@link OAuthException} is thrown. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @throws OAuthException if the token cannot be obtained or refreshed + */ + void refreshToken(String serviceHandle); + + /** + * Gets the currently stored access token from persistent storage. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @return The currently stored access token or an empty {@link Optional} if there is no stored token. + */ + Optional getAccessTokenFromStorage(String serviceHandle); + + /** + * Removes the tokens from persistent storage. + * + * Note: Calling this method will force the user to run through the pairing process again in order to obtain a + * working bridge. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + */ + void removeTokensFromStorage(String serviceHandle); +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OpenHabOAuthTokenRefresher.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OpenHabOAuthTokenRefresher.java new file mode 100644 index 0000000000000..e7b7f774b414b --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/auth/OpenHabOAuthTokenRefresher.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.auth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +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; + +/** + * Handles refreshing of OAuth2 tokens managed by the openHAB runtime. + * + * @author Björn Lange - Initial contribution + */ +@Component +@NonNullByDefault +public final class OpenHabOAuthTokenRefresher implements OAuthTokenRefresher { + private final Logger logger = LoggerFactory.getLogger(OpenHabOAuthTokenRefresher.class); + + private final OAuthFactory oauthFactory; + private Map listenerByServiceHandle = new HashMap<>(); + + @Activate + public OpenHabOAuthTokenRefresher(@Reference OAuthFactory oauthFactory) { + this.oauthFactory = oauthFactory; + } + + @Override + public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle) { + final AccessTokenRefreshListener refreshListener = tokenResponse -> { + final String accessToken = tokenResponse.getAccessToken(); + if (accessToken == null) { + // Fail without exception to ensure that the OAuthClientService notifies all listeners. + logger.warn("Ignoring access token response without access token."); + } else { + listener.onNewAccessToken(accessToken); + } + }; + + OAuthClientService clientService = getOAuthClientService(serviceHandle); + clientService.addAccessTokenRefreshListener(refreshListener); + listenerByServiceHandle.put(serviceHandle, refreshListener); + } + + @Override + public void unsetRefreshListener(String serviceHandle) { + final AccessTokenRefreshListener refreshListener = listenerByServiceHandle.get(serviceHandle); + if (refreshListener != null) { + try { + OAuthClientService clientService = getOAuthClientService(serviceHandle); + clientService.removeAccessTokenRefreshListener(refreshListener); + } catch (OAuthException e) { + logger.warn("Failed to remove refresh listener: OAuth client service is unavailable. Cause: {}", + e.getMessage()); + } + } + listenerByServiceHandle.remove(serviceHandle); + } + + @Override + public void refreshToken(String serviceHandle) { + if (listenerByServiceHandle.get(serviceHandle) == null) { + logger.warn("Token refreshing was requested but there is no token refresh listener registered!"); + return; + } + + OAuthClientService clientService = getOAuthClientService(serviceHandle); + refreshAccessToken(clientService); + } + + private OAuthClientService getOAuthClientService(String serviceHandle) { + final OAuthClientService clientService = oauthFactory.getOAuthClientService(serviceHandle); + if (clientService == null) { + throw new OAuthException("OAuth client service is not available."); + } + return clientService; + } + + private void refreshAccessToken(OAuthClientService clientService) { + try { + final AccessTokenResponse accessTokenResponse = clientService.refreshToken(); + final String accessToken = accessTokenResponse.getAccessToken(); + if (accessToken == null) { + throw new OAuthException("Access token is not available."); + } + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("An error occured during token refresh: " + e.getMessage(), e); + } catch (IOException e) { + throw new OAuthException("A network error occured during token refresh: " + e.getMessage(), e); + } catch (OAuthResponseException e) { + throw new OAuthException("Onecta returned an illegal response: " + e.getMessage(), e); + } + } + + @Override + public Optional getAccessTokenFromStorage(String serviceHandle) { + try { + AccessTokenResponse tokenResponse = getOAuthClientService(serviceHandle).getAccessTokenResponse(); + if (tokenResponse == null) { + return Optional.empty(); + } else { + return Optional.of(tokenResponse.getAccessToken()); + } + } catch (OAuthException | org.openhab.core.auth.client.oauth2.OAuthException | IOException + | OAuthResponseException e) { + logger.debug("Cannot obtain access token from persistent storage.", e); + return Optional.empty(); + } + } + + @Override + public void removeTokensFromStorage(String serviceHandle) { + oauthFactory.deleteServiceAndAccessToken(serviceHandle); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandler.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandler.java new file mode 100644 index 0000000000000..78469d318020d --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.onecta.internal.oauth2.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.onecta.internal.oauth2.config.exception.OngoingAuthorizationException; + +/** + * Handles OAuth 2 authorization processes. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public interface OAuthAuthorizationHandler { + /** + * Begins the authorization process after the user provided client ID, client secret and a bridge ID. + * + * @param clientId Client ID. + * @param clientSecret Client secret. + * @param email E-mail address identifying the account to authorize. + * @throws OngoingAuthorizationException if there already is an ongoing authorization. + */ + void beginAuthorization(String clientId, String clientSecret, String email); + + /** + * Creates the authorization URL for the ongoing authorization. + * + * @param redirectUri The URI to which the user is redirected after a successful login. This should point to our own + * service. + * @return The authorization URL to which the user is redirected for the log in. + * @throws NoOngoingAuthorizationException if there is no ongoing authorization. + * @throws OAuthException if the authorization URL cannot be determined. In this case the ongoing authorization is + * cancelled. + */ + String getAuthorizationUrl(String redirectUri); + + /** + * Completes the authorization by extracting the authorization code from the given redirection URL, fetching the + * access token response and persisting it. After this method succeeded the access token can be read from the + * persistent storage. + * + * @param redirectUrlWithParameters The URL the remote service redirected the user to. This is the URL our servlet + * was called with. + * @throws NoOngoingAuthorizationException if there is no ongoing authorization. + * @throws OAuthException if the authorization failed. In this case the ongoing authorization is cancelled. + */ + void completeAuthorization(String redirectUrlWithParameters); + + /** + * Gets the access token from persistent storage. + * + * @param email E-mail address for which the access token is requested. + * @return The access token. + * @throws OAuthException if the access token cannot be obtained. + */ + String getAccessToken(String email); +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandlerImpl.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandlerImpl.java new file mode 100644 index 0000000000000..b7701bd4c4f2e --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OAuthAuthorizationHandlerImpl.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaBridgeConstants; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthException; +import org.openhab.binding.onecta.internal.oauth2.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.onecta.internal.oauth2.config.exception.OngoingAuthorizationException; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; + +/** + * {@link OAuthAuthorizationHandler} implementation handling the OAuth 2 authorization via openHAB services. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class OAuthAuthorizationHandlerImpl implements OAuthAuthorizationHandler { + private static final String TOKEN_URL = OnectaBridgeConstants.THIRD_PARTY_ENDPOINTS_BASENAME + "/token"; + private static final String AUTHORIZATION_URL = OnectaBridgeConstants.THIRD_PARTY_ENDPOINTS_BASENAME + "/authorize"; + + private static final long AUTHORIZATION_TIMEOUT_IN_MINUTES = 5; + + private final OAuthFactory oauthFactory; + private final ScheduledExecutorService scheduler; + + @Nullable + private OAuthClientService oauthClientService; + @Nullable + private String handle; + @Nullable + private String redirectUri; + @Nullable + private ScheduledFuture timer; + @Nullable + private LocalDateTime timerExpiryTimestamp; + + /** + * Creates a new {@link OAuthAuthorizationHandlerImpl}. + * + * @param oauthFactory Factory for accessing the {@link OAuthClientService}. + * @param scheduler System-wide scheduler. + */ + public OAuthAuthorizationHandlerImpl(OAuthFactory oauthFactory, ScheduledExecutorService scheduler) { + this.oauthFactory = oauthFactory; + this.scheduler = scheduler; + } + + @Override + public synchronized void beginAuthorization(String clientId, String clientSecret, String handle) { + if (this.oauthClientService != null) { + throw new OngoingAuthorizationException("There is already an ongoing authorization!", timerExpiryTimestamp); + } + + this.oauthClientService = oauthFactory.createOAuthClientService(handle, TOKEN_URL, AUTHORIZATION_URL, clientId, + clientSecret, null, false); + this.handle = handle; + redirectUri = null; + timer = null; + timerExpiryTimestamp = null; + } + + @Override + public synchronized String getAuthorizationUrl(String redirectUri) { + final OAuthClientService oauthClientService = this.oauthClientService; + if (oauthClientService == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization!"); + } + + this.redirectUri = redirectUri; + try { + timer = scheduler.schedule(this::cancelAuthorization, AUTHORIZATION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES); + timerExpiryTimestamp = LocalDateTime.now().plusMinutes(AUTHORIZATION_TIMEOUT_IN_MINUTES); + return oauthClientService.getAuthorizationUrl(redirectUri, "openid onecta:basic.integration", null); + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + abortTimer(); + cancelAuthorization(); + throw new OAuthException("Failed to determine authorization URL: " + e.getMessage(), e); + } + } + + @Override + public synchronized void completeAuthorization(String redirectUrlWithParameters) { + abortTimer(); + + final OAuthClientService oauthClientService = this.oauthClientService; + if (oauthClientService == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization."); + } + + try { + String authorizationCode = oauthClientService.extractAuthCodeFromAuthResponse(redirectUrlWithParameters); + + // Although this method is called "get" it actually fetches and stores the token response as a side effect. + oauthClientService.getAccessTokenResponseByAuthorizationCode(authorizationCode, redirectUri); + } catch (IOException e) { + throw new OAuthException("Network error while retrieving token response: " + e.getMessage(), e); + } catch (OAuthResponseException e) { + throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e); + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("Error while processing Onecta service response: " + e.getMessage(), e); + } finally { + this.oauthClientService = null; + this.handle = null; + this.redirectUri = null; + } + } + + /** + * Aborts the timer. + * + * Note: All calls to this method must be {@code synchronized} to ensure thread-safety. Also note that + * {@link #cancelAuthorization()} is {@code synchronized} so the execution of this method and + * {@link #cancelAuthorization()} cannot overlap. Therefore, this method is an atomic operation from the timer's + * perspective. + */ + private void abortTimer() { + final ScheduledFuture timer = this.timer; + if (timer == null) { + return; + } + + if (!timer.isDone()) { + timer.cancel(false); + } + this.timer = null; + timerExpiryTimestamp = null; + } + + private synchronized void cancelAuthorization() { + oauthClientService = null; + handle = null; + redirectUri = null; + final ScheduledFuture timer = this.timer; + if (timer != null) { + timer.cancel(false); + this.timer = null; + timerExpiryTimestamp = null; + } + } + + @Override + public String getAccessToken(String handle) { + OAuthClientService clientService = oauthFactory.getOAuthClientService(handle); + if (clientService == null) { + throw new OAuthException("There is no access token registered for '" + handle + "'"); + } + + try { + AccessTokenResponse response = clientService.getAccessTokenResponse(); + if (response == null) { + throw new OAuthException( + "There is no access token in the persistent storage or it already expired and could not be refreshed"); + } else { + return response.getAccessToken(); + } + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("Failed to read access token from persistent storage: " + e.getMessage(), e); + } catch (IOException e) { + throw new OAuthException( + "Network error during token refresh or error while reading from persistent storage: " + + e.getMessage(), + e); + } catch (OAuthResponseException e) { + throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e); + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OnectaAuthConfigService.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OnectaAuthConfigService.java new file mode 100644 index 0000000000000..a2fb8e41caa4d --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/OnectaAuthConfigService.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config; + +import java.util.Hashtable; +import java.util.Map; + +import javax.servlet.ServletException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.AccountOverviewServlet; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.FailureServlet; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.ForwardToLoginServlet; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.PairAccountServlet; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.ResourceLoader; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.ResultServlet; +import org.openhab.binding.onecta.internal.oauth2.config.servlet.SuccessServlet; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.config.discovery.inbox.Inbox; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.thing.ThingRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Alexander Drent - Initial contribution + */ +@Component(service = OnectaAuthConfigService.class, immediate = true, configurationPid = "binding.onecta.configService") +@NonNullByDefault +public final class OnectaAuthConfigService { + private static final String ROOT_ALIAS = "/onecta"; + private static final String PAIR_ALIAS = ROOT_ALIAS + "/pair"; + private static final String FORWARD_TO_LOGIN_ALIAS = ROOT_ALIAS + "/forwardToLogin"; + private static final String RESULT_ALIAS = ROOT_ALIAS + "/result"; + private static final String SUCCESS_ALIAS = ROOT_ALIAS + "/success"; + private static final String FAILURE_ALIAS = ROOT_ALIAS + "/failure"; + private static final String CSS_ALIAS = ROOT_ALIAS + "/assets/css"; + private static final String JS_ALIAS = ROOT_ALIAS + "/assets/js"; + private static final String IMG_ALIAS = ROOT_ALIAS + "/assets/img"; + + private static final String WEBSITE_RESOURCE_BASE_PATH = "org/openhab/binding/onecta/internal/config"; + private static final String WEBSITE_CSS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/css"; + private static final String WEBSITE_JS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/js"; + private static final String WEBSITE_IMG_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/img"; + + private final Logger logger = LoggerFactory.getLogger(OnectaAuthConfigService.class); + + private HttpService httpService; + private OAuthFactory oauthFactory; + private Inbox inbox; + private ThingRegistry thingRegistry; + + /** + * For integration test purposes only. + */ + @Nullable + private AccountOverviewServlet accountOverviewServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private ForwardToLoginServlet forwardToLoginServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private ResultServlet resultServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private SuccessServlet successServlet; + + @Activate + public OnectaAuthConfigService(@Reference HttpService httpService, @Reference OAuthFactory oauthFactory, + @Reference Inbox inbox, @Reference ThingRegistry thingRegistry, @Reference LocaleProvider localeProvider) { + this.httpService = httpService; + this.oauthFactory = oauthFactory; + this.inbox = inbox; + this.thingRegistry = thingRegistry; + } + + @Nullable + public AccountOverviewServlet getAccountOverviewServlet() { + return accountOverviewServlet; + } + + @Nullable + public ForwardToLoginServlet getForwardToLoginServlet() { + return forwardToLoginServlet; + } + + @Nullable + public ResultServlet getResultServlet() { + return resultServlet; + } + + @Nullable + public SuccessServlet getSuccessServlet() { + return successServlet; + } + + @Activate + protected void activate(ComponentContext componentContext, Map properties) { + registerWebsite(componentContext.getBundleContext()); + } + + private void registerWebsite(BundleContext bundleContext) { + ResourceLoader resourceLoader = new ResourceLoader(WEBSITE_RESOURCE_BASE_PATH, bundleContext); + OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory, + ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON)); + + try { + HttpContext httpContext = httpService.createDefaultHttpContext(); + httpService.registerServlet(ROOT_ALIAS, + accountOverviewServlet = new AccountOverviewServlet(resourceLoader, thingRegistry, inbox), + new Hashtable<>(), httpContext); + httpService.registerServlet(PAIR_ALIAS, new PairAccountServlet(resourceLoader), new Hashtable<>(), + httpContext); + httpService.registerServlet(FORWARD_TO_LOGIN_ALIAS, + forwardToLoginServlet = new ForwardToLoginServlet(authorizationHandler), new Hashtable<>(), + httpContext); + httpService.registerServlet(RESULT_ALIAS, resultServlet = new ResultServlet(authorizationHandler), + new Hashtable<>(), httpContext); + httpService.registerServlet(SUCCESS_ALIAS, successServlet = new SuccessServlet(resourceLoader), + new Hashtable<>(), httpContext); + + httpService.registerServlet(FAILURE_ALIAS, new FailureServlet(resourceLoader), new Hashtable<>(), + httpContext); + httpService.registerResources(CSS_ALIAS, WEBSITE_CSS_RESOURCE_PATH, httpContext); + httpService.registerResources(JS_ALIAS, WEBSITE_JS_RESOURCE_PATH, httpContext); + httpService.registerResources(IMG_ALIAS, WEBSITE_IMG_RESOURCE_PATH, httpContext); + logger.debug("Registered Onecta binding website at /onecta"); + } catch (NamespaceException | ServletException e) { + logger.warn("Failed to register Onecta binding website. Onecta binding website will not be available.", e); + unregisterWebsite(); + } + } + + @Deactivate + protected void deactivate() { + unregisterWebsite(); + } + + private void unregisterWebsite() { + unregisterWebResource(ROOT_ALIAS); + unregisterWebResource(PAIR_ALIAS); + unregisterWebResource(FORWARD_TO_LOGIN_ALIAS); + unregisterWebResource(RESULT_ALIAS); + unregisterWebResource(SUCCESS_ALIAS); + + unregisterWebResource(FAILURE_ALIAS); + unregisterWebResource(CSS_ALIAS); + unregisterWebResource(JS_ALIAS); + unregisterWebResource(IMG_ALIAS); + forwardToLoginServlet = null; + resultServlet = null; + + logger.debug("Unregistered Onecta binding website at /onecta"); + } + + private void unregisterWebResource(String alias) { + try { + httpService.unregister(alias); + } catch (IllegalArgumentException e) { + logger.warn("Failed to unregister Onecta binding website alias {}", alias, e); + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/NoOngoingAuthorizationException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/NoOngoingAuthorizationException.java new file mode 100644 index 0000000000000..b40bbe300e3ba --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/NoOngoingAuthorizationException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when no authorization is ongoing. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class NoOngoingAuthorizationException extends RuntimeException { + private static final long serialVersionUID = 3074275827393542416L; + + public NoOngoingAuthorizationException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/OngoingAuthorizationException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/OngoingAuthorizationException.java new file mode 100644 index 0000000000000..0b3ef1374f848 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/exception/OngoingAuthorizationException.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.exception; + +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Exception thrown when there already is an ongoing authorization process. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class OngoingAuthorizationException extends RuntimeException { + private static final long serialVersionUID = -6742384930140134244L; + + @Nullable + private final LocalDateTime ongoingAuthorizationExpiryTimestamp; + + /** + * Creates a new {@link OngoingAuthorizationException}. + * + * @param message Exception message. + * @param ongoingAuthorizationExpiryTimestamp Timestamp when the ongoing authorization will expire. + */ + public OngoingAuthorizationException(String message, @Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) { + super(message); + this.ongoingAuthorizationExpiryTimestamp = ongoingAuthorizationExpiryTimestamp; + } + + /** + * Gets the timestamp representing when the ongoing authorization will expire. + */ + @Nullable + public LocalDateTime getOngoingAuthorizationExpiryTimestamp() { + return ongoingAuthorizationExpiryTimestamp; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractRedirectionServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractRedirectionServlet.java new file mode 100644 index 0000000000000..e20df1319617e --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractRedirectionServlet.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for servlets that have no visible frontend and just serve the purpose of redirecting the user to another + * website. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractRedirectionServlet extends HttpServlet { + private static final long serialVersionUID = 4280026301732437523L; + + private final Logger logger = LoggerFactory.getLogger(AbstractRedirectionServlet.class); + + @Override + protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (response == null) { + logger.warn("Ignoring received request without response."); + return; + } + if (request == null) { + logger.warn("Ignoring illegal request."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + response.sendRedirect(getRedirectionDestination(request)); + } + + /** + * Gets the redirection destination. This can be a relative or absolute path or a link to another website. + * + * @param request The original request sent by the browser. + * @return The redirection destination. + */ + protected abstract String getRedirectionDestination(HttpServletRequest request); +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractShowPageServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractShowPageServlet.java new file mode 100644 index 0000000000000..13922234c1f2f --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AbstractShowPageServlet.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for servlets that show a visible frontend in the browser. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractShowPageServlet extends HttpServlet { + private static final long serialVersionUID = 3820684716753275768L; + + private static final String CONTENT_TYPE = "text/html;charset=UTF-8"; + + private final Logger logger = LoggerFactory.getLogger(AbstractShowPageServlet.class); + + private final ResourceLoader resourceLoader; + + protected ResourceLoader getResourceLoader() { + return resourceLoader; + } + + /** + * Creates a new {@link AbstractShowPageServlet}. + * + * @param resourceLoader Loader for resource files. + */ + public AbstractShowPageServlet(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (response == null) { + logger.warn("Ignoring received request without response."); + return; + } + if (request == null) { + logger.warn("Ignoring illegal request."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + try { + String html = handleGetRequest(request, response); + response.setContentType(CONTENT_TYPE); + try (PrintWriter writer = response.getWriter()) { + writer.write(html); + } + } catch (OnectaHttpException e) { + response.sendError(e.getHttpErrorCode()); + } catch (IOException e) { + logger.warn("Failed to load resources.", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * Handles a GET request. + * + * @param request The request. + * @param response The response. + * @return A rendered HTML body to be displayed in the browser. The body will be framed by the binding's frontend + * layout. + * @throws OnectaHttpException if an error occurs that should be handled by sending a default error response. + * @throws IOException if an error occurs while loading resources. + */ + protected abstract String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws OnectaHttpException, IOException; +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AccountOverviewServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AccountOverviewServlet.java new file mode 100644 index 0000000000000..f3e0614e60927 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/AccountOverviewServlet.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.onecta.internal.OnectaBridgeConstants; +import org.openhab.core.config.discovery.inbox.Inbox; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; + +/** + * Servlet showing the account overview page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class AccountOverviewServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = -4551210904923220429L; + private static final String ACCOUNTS_SKELETON = "index.html"; + + private static final String BRIDGES_TITLE_PLACEHOLDER = ""; + private static final String BRIDGES_PLACEHOLDER = ""; + private static final String NO_SSL_WARNING_PLACEHOLDER = ""; + + private final ThingRegistry thingRegistry; + private final Inbox inbox; + + /** + * Creates a new {@link AccountOverviewServlet}. + * + * @param resourceLoader Loader to use for resources. + * @param thingRegistry openHAB thing registry. + * @param inbox openHAB inbox for discovery results. + */ + public AccountOverviewServlet(ResourceLoader resourceLoader, ThingRegistry thingRegistry, Inbox inbox) { + super(resourceLoader); + this.thingRegistry = thingRegistry; + this.inbox = inbox; + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws OnectaHttpException, IOException { + String skeleton = getResourceLoader().loadResourceAsString(ACCOUNTS_SKELETON); + skeleton = renderBridges(skeleton); + skeleton = renderSslWarning(request, skeleton); + return skeleton; + } + + private String renderBridges(String skeleton) { + List bridges = thingRegistry.stream().filter(this::isOnectaBridge).collect(Collectors.toList()); + if (bridges.isEmpty()) { + return renderNoBridges(skeleton); + } else { + return renderBridgesIntoSkeleton(skeleton, bridges); + } + } + + private String renderNoBridges(String skeleton) { + return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "There is no account paired at the moment.") + .replace(BRIDGES_PLACEHOLDER, ""); + } + + private String renderBridgesIntoSkeleton(String skeleton, List bridges) { + StringBuilder builder = new StringBuilder(); + + int index = 0; + Iterator bridgeIterator = bridges.iterator(); + while (bridgeIterator.hasNext()) { + builder.append(renderBridge(bridgeIterator.next(), index)); + index++; + } + + return skeleton.replace(BRIDGES_PLACEHOLDER, builder.toString()); + } + + private String renderBridge(Thing bridge, int index) { + StringBuilder builder = new StringBuilder(); + builder.append("
  • \n"); + + String thingUid = bridge.getUID().getAsString(); + String thingId = bridge.getUID().getId(); + builder.append(" "); + builder.append(thingUid.substring(0, thingUid.length() - thingId.length())); + builder.append(" "); + builder.append(thingId); + + builder.append("\n"); + + builder.append(" "); + builder.append(status.toString()); + builder.append("\n"); + + builder.append("
  • "); + + return builder.toString(); + } + + private boolean isOnectaBridge(Thing thing) { + return OnectaBridgeConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID()); + } + + private String renderSslWarning(HttpServletRequest request, String skeleton) { + if (!request.isSecure()) { + return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "
    \n" + + " Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n" + + " See Securing access to openHAB for details.\n" + + "
    "); + } else { + return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, ""); + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/FailureServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/FailureServlet.java new file mode 100644 index 0000000000000..d135ddfe6c8e7 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/FailureServlet.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Servlet showing a failure page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class FailureServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = -5195984256535664942L; + + public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error"; + public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse"; + public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization"; + public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization"; + public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid"; + public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail"; + public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid"; + public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl"; + + public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied"; + public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request"; + public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; + public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type"; + public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope"; + public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error"; + public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable"; + + private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = ""; + + /** + * Creates a new {@link FailureServlet}. + * + * @param resourceLoader Loader to use for resources. + */ + public FailureServlet(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws OnectaHttpException, IOException { + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + getErrorMessage(request)); + } + + private String getErrorMessage(HttpServletRequest request) { + String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME); + if (oauth2Error != null) { + return getOAuth2ErrorMessage(oauth2Error); + } else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) { + return "Onecta service returned an illegal response."; + } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) { + return "There is no ongoing authorization. Please start an authorization first."; + } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) { + return "Completing the final authorization request failed. Please try the config flow again."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) { + return "Missing bridge UID."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) { + return "Missing e-mail address."; + } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) { + return "Malformed bridge UID."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) { + return "Missing request URL. Please try the config flow again."; + } else { + return "Unknown error."; + } + } + + private String getOAuth2ErrorMessage(String oauth2Error) { + return "OAuth2 authentication with Onecta service failed: " + getOAuth2ErrorDetailMessage(oauth2Error); + } + + private String getOAuth2ErrorDetailMessage(String oauth2Error) { + switch (oauth2Error) { + case OAUTH2_ERROR_ACCESS_DENIED: + return "Access denied."; + case OAUTH2_ERROR_INVALID_REQUEST: + return "Malformed request."; + case OAUTH2_ERROR_UNAUTHORIZED_CLIENT: + return "Account not authorized to request authorization code."; + case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE: + return "Obtaining an authorization code is not supported."; + case OAUTH2_ERROR_INVALID_SCOPE: + return "Invalid scope."; + case OAUTH2_ERROR_SERVER_ERROR: + return "Unexpected server error."; + case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE: + return "Authorization server temporarily unavailable."; + default: + return "Unknown error code \"" + oauth2Error + "\"."; + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ForwardToLoginServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ForwardToLoginServlet.java new file mode 100644 index 0000000000000..7a0fe887e75ef --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ForwardToLoginServlet.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.onecta.internal.OnectaBridgeConstants; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthException; +import org.openhab.binding.onecta.internal.oauth2.config.OAuthAuthorizationHandler; +import org.openhab.binding.onecta.internal.oauth2.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.onecta.internal.oauth2.config.exception.OngoingAuthorizationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet gathers and processes required information to perform an authorization with the Onecta service + * and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an + * e-mail address. If the given parameters are valid, the browser is redirected to the Onecta service login. Otherwise, + * the browser is redirected to the previous page with an according error message. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ForwardToLoginServlet extends AbstractRedirectionServlet { + private static final long serialVersionUID = -9094642228439994183L; + + public static final String CLIENT_ID_PARAMETER_NAME = "clientId"; + public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret"; + public static final String RETURN_URL_PARAMETER_NAME = "returnUrl"; + public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId"; + public static final String EMAIL_PARAMETER_NAME = "email"; + + private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class); + + private final OAuthAuthorizationHandler authorizationHandler; + + /** + * Creates a new {@link ForwardToLoginServlet}. + * + * @param authorizationHandler Handler implementing the OAuth authorization process. + */ + public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) { + this.authorizationHandler = authorizationHandler; + } + + @Override + protected String getRedirectionDestination(HttpServletRequest request) { + String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME); + String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME); + String requestUrl = request.getParameter(RETURN_URL_PARAMETER_NAME); + + if (clientId == null || clientId.isEmpty()) { + logger.warn("Request is missing client ID."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME); + } + clientId = clientId.strip(); + + if (clientSecret == null || clientSecret.isEmpty()) { + logger.warn("Request is missing client secret."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME); + } + clientSecret = clientSecret.strip(); + + try { + authorizationHandler.beginAuthorization(clientId, clientSecret, + OnectaBridgeConstants.OAUTH2_SERVICE_HANDLE); + } catch (OngoingAuthorizationException e) { + logger.warn("Cannot begin new authorization process while another one is still running."); + return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp()); + } + + if (requestUrl == null) { + return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME); + } + + try { + return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString())); + } catch (NoOngoingAuthorizationException e) { + logger.warn( + "Failed to create authorization URL: There was no ongoing authorization although we just started one."); + return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME); + } catch (OAuthException e) { + logger.warn("Failed to create authorization URL.", e); + return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME); + } + } + + private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) { + if (ongoingAuthorizationExpiryTimestamp == null) { + return getErrorRedirectionUrl( + PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME, + PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME); + } + + long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp) + + 1; + return getErrorRedirectionUrl( + PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME, + Long.toString(minutesUntilExpiry)); + } + + private String getErrorRedirectionUrl(String errorCode) { + return getErrorRedirectionUrl(errorCode, "true"); + } + + private String getErrorRedirectionUrl(String errorCode, String parameterValue) { + return "/onecta/pair?" + errorCode + "=" + parameterValue; + } + + private String deriveRedirectUri(String requestUrl) { + return requestUrl.replace("forwardToLogin", "result"); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/OnectaHttpException.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/OnectaHttpException.java new file mode 100644 index 0000000000000..fd4f119961362 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/OnectaHttpException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception wrapping a HTTP error code for further processing. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class OnectaHttpException extends Exception { + private static final long serialVersionUID = 1825214275413952809L; + + private final int httpErrorCode; + + public OnectaHttpException(int httpErrorCode) { + this.httpErrorCode = httpErrorCode; + } + + public int getHttpErrorCode() { + return httpErrorCode; + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/PairAccountServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/PairAccountServlet.java new file mode 100644 index 0000000000000..78d62bd371362 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/PairAccountServlet.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; +import java.util.Objects; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Servlet showing the pair account page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class PairAccountServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = 6565378471951635420L; + + public static final String CLIENT_ID_PARAMETER_NAME = "clientId"; + public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret"; + + public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId"; + public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret"; + public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl"; + public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes"; + public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown"; + public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2"; + public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl"; + + private static final String PAIR_ACCOUNT_SKELETON = "pairing.html"; + + private static final String CLIENT_ID_PLACEHOLDER = ""; + private static final String CLIENT_SECRET_PLACEHOLDER = ""; + private static final String ERROR_MESSAGE_PLACEHOLDER = ""; + + private static final String REDIRECT_URI_PLACEHOLDER = ""; + + /** + * Creates a new {@link PairAccountServlet}. + * + * @param resourceLoader Loader for resources. + */ + public PairAccountServlet(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws OnectaHttpException, IOException { + String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON); + skeleton = renderClientIdAndClientSecret(request, skeleton); + skeleton = renderErrorMessage(request, skeleton); + skeleton = renderRedirectUri(request, skeleton); + return skeleton; + } + + private String renderRedirectUri(HttpServletRequest request, String skeleton) { + return skeleton.replace(REDIRECT_URI_PLACEHOLDER, deriveRedirectUri(request.getRequestURL().toString())); + } + + private String deriveRedirectUri(String requestUrl) { + return requestUrl.replace("pair", "result"); + } + + private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) { + String prefilledClientId = Objects.requireNonNullElse(request.getParameter(CLIENT_ID_PARAMETER_NAME), ""); + String prefilledClientSecret = Objects.requireNonNullElse(request.getParameter(CLIENT_SECRET_PARAMETER_NAME), + ""); + return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER, + prefilledClientSecret); + } + + private String renderErrorMessage(HttpServletRequest request, String skeleton) { + if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing client ID.
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + + "
    Missing client secret.
    "); + } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Failed to derive redirect URL.
    "); + } else if (ServletUtil.isParameterPresent(request, + ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) { + String minutesUntilExpiry = request + .getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME); + if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in " + + minutesUntilExpiry + " minutes.
    "); + } + } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing request URL. Please try again.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, ""); + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResourceLoader.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResourceLoader.java new file mode 100644 index 0000000000000..8059ebec995b6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResourceLoader.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.osgi.framework.BundleContext; + +/** + * Provides access to resource files for servlets. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ResourceLoader { + private static final String BEGINNING_OF_INPUT = "\\A"; + + private final String basePath; + private final BundleContext bundleContext; + + /** + * Creates a new {@link ResourceLoader}. + * + * @param basePath The base path to use for loading. A trailing {@code "/"} is removed. + * @param bundleContext {@link BundleContext} to load from. + */ + public ResourceLoader(String basePath, BundleContext bundleContext) { + this.basePath = removeTrailingSlashes(basePath); + this.bundleContext = bundleContext; + } + + private String removeTrailingSlashes(String value) { + String ret = value; + while (ret.endsWith("/")) { + ret = ret.substring(0, ret.length() - 1); + } + return ret; + } + + /** + * Opens a resource relative to the base path. + * + * @param filename The filename of the resource to load. + * @return A stream reading from the resource file. + * @throws FileNotFoundException If the requested resource file cannot be found. + * @throws IOException If an error occurs while opening a stream to the resource. + */ + public InputStream openResource(String filename) throws IOException { + URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename); + if (url == null) { + throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'"); + } + + return url.openStream(); + } + + /** + * Loads the contents of a resource file as UTF-8 encoded {@link String}. + * + * @param filename The filename of the resource to load. + * @return The contents of the file. + * @throws FileNotFoundException If the requested resource file cannot be found. + * @throws IOException If an error occurs while opening a stream to the resource or reading from it. + */ + public String loadResourceAsString(String filename) throws IOException { + try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) { + return scanner.useDelimiter(BEGINNING_OF_INPUT).next(); + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResultServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResultServlet.java new file mode 100644 index 0000000000000..040800efa76d6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ResultServlet.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.onecta.internal.oauth2.auth.OAuthException; +import org.openhab.binding.onecta.internal.oauth2.config.OAuthAuthorizationHandler; +import org.openhab.binding.onecta.internal.oauth2.config.exception.NoOngoingAuthorizationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet processing the response by the Onecta service after a login. This servlet is called as a result of a + * completed login to the Onecta service and assumes that the OAuth 2 parameters are passed. Depending on the parameters + * and whether the token response can be fetched either the browser is redirected to the success or the failure page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ResultServlet extends AbstractRedirectionServlet { + private static final long serialVersionUID = 2157912755568949550L; + + public static final String CODE_PARAMETER_NAME = "code"; + public static final String STATE_PARAMETER_NAME = "state"; + public static final String ERROR_PARAMETER_NAME = "error"; + + private final Logger logger = LoggerFactory.getLogger(ResultServlet.class); + + private final OAuthAuthorizationHandler authorizationHandler; + + /** + * Creates a new {@link ResultServlet}. + * + * @param authorizationHandler Handler implementing the OAuth authorization. + */ + public ResultServlet(OAuthAuthorizationHandler authorizationHandler) { + this.authorizationHandler = authorizationHandler; + } + + @Override + protected String getRedirectionDestination(HttpServletRequest request) { + String error = request.getParameter(ERROR_PARAMETER_NAME); + if (error != null) { + logger.warn("Received error response: {}", error); + return "/onecta/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error; + } + + String code = request.getParameter(CODE_PARAMETER_NAME); + if (code == null) { + logger.warn("Code is null"); + return "/onecta/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true"; + } + String state = request.getParameter(STATE_PARAMETER_NAME); + if (state == null) { + logger.warn("State is null"); + return "/onecta/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true"; + } + + try { + + StringBuffer requestUrl = request.getRequestURL(); + if (requestUrl == null) { + return "/onecta/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true"; + } + + try { + authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString()); + } catch (OAuthException e) { + logger.warn("Failed to complete authorization.", e); + return "/onecta/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME + "=true"; + } + + return "/onecta/success"; + } catch (NoOngoingAuthorizationException e) { + logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out"); + return "/onecta/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true"; + } + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ServletUtil.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ServletUtil.java new file mode 100644 index 0000000000000..3f9a519f6c8b6 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/ServletUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Utility class for common servlet tasks. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ServletUtil { + private ServletUtil() { + throw new UnsupportedOperationException(); + } + + /** + * Gets the value of a request parameter or returns a default if the parameter is not present. + */ + public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName, + String defaultValue) { + String parameterValue = request.getParameter(parameterName); + if (parameterValue == null) { + return defaultValue; + } else { + return parameterValue; + } + } + + /** + * Checks whether a request parameter is enabled. + */ + public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) { + return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false")); + } + + /** + * Checks whether a parameter is present in a request. + */ + public static boolean isParameterPresent(HttpServletRequest request, String parameterName) { + String parameterValue = request.getParameter(parameterName); + return parameterValue != null && !parameterValue.trim().isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/SuccessServlet.java b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/SuccessServlet.java new file mode 100644 index 0000000000000..2fbbbc2d96aa1 --- /dev/null +++ b/bundles/org.openhab.binding.onecta/src/main/java/org/openhab/binding/onecta/internal/oauth2/config/servlet/SuccessServlet.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.onecta.internal.oauth2.config.servlet; + +import java.io.IOException; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet showing the success page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class SuccessServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = 7013060161686096950L; + + public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed"; + public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed"; + + private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = ""; + private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = ""; + + private static final String LOCALE_OPTIONS_PLACEHOLDER = ""; + + private static final Set SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es"); + + private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class); + + /** + * Creates a new {@link SuccessServlet}. + * + * @param resourceLoader Loader for resources. + */ + public SuccessServlet(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws OnectaHttpException, IOException { + + String skeleton = getResourceLoader().loadResourceAsString("success.html"); + skeleton = renderErrorMessage(request, skeleton); + skeleton = renderLocaleSelection(skeleton); + skeleton = renderBridgeConfigurationTemplate(skeleton); + return skeleton; + } + + private String renderErrorMessage(HttpServletRequest request, String skeleton) { + if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "
    Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.
    "); + } else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "
    Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, ""); + } + } + + private String renderLocaleSelection(String skeleton) { + String preSelectedLanguage = "en"; + + return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER, + SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get) + .sorted() + .map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode()))) + .collect(Collectors.joining("\n"))); + } + + private String createOptionTag(Language language, boolean selected) { + String firstPart = "