[homeconnect] Initial contribution (#9187)

Signed-off-by: Jonas Brüstel <jonas@bruestel.net>
Co-authored-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
bruestel 2021-05-13 14:56:03 +02:00 committed by GitHub
parent e1a76505a0
commit a1a990989e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 9521 additions and 0 deletions

View File

@ -109,6 +109,7 @@
/bundles/org.openhab.binding.helios/ @kgoderis
/bundles/org.openhab.binding.heliosventilation/ @ramack
/bundles/org.openhab.binding.heos/ @Wire82
/bundles/org.openhab.binding.homeconnect/ @bruestel
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
/bundles/org.openhab.binding.homewizard/ @Daniel-42
/bundles/org.openhab.binding.hpprinter/ @cossey

View File

@ -531,6 +531,11 @@
<artifactId>org.openhab.binding.heos</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.homeconnect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.homematic</artifactId>

View File

@ -0,0 +1,65 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab2-addons
== Third-party Content
Thymeleaf
* License: Apache License 2.0
* Project: https://www.thymeleaf.org/
* Source: https://github.com/thymeleaf/thymeleaf
Thymeleaf - Java 8 Time API compatibility
* License: Apache License 2.0
* Project: https://www.thymeleaf.org/
* Source: https://github.com/thymeleaf/thymeleaf-extras-java8time
OGNL Object Graph Navigation Library
* License: Apache License 2.0
* Project: http://www.opensymphony.com/ognl/
* Source: https://github.com/jkuhnert/ognl/
Javassist
* License: Apache License 2.0
* Project: http://www.javassist.org/
* Source: https://github.com/jboss-javassist/javassist
ATTOPARSER
* License: Apache License 2.0
* Project: http://www.attoparser.org/
* Source: https://github.com/attoparser/attoparser
UNBESCAPE
* License: Apache License 2.0
* Project: http://www.unbescape.org/
* Source: https://github.com/unbescape/unbescape
Bucket4j
* License: Apache License 2.0
* Project: https://github.com/vladimir-bukhtoyarov/bucket4j
* Source: https://github.com/vladimir-bukhtoyarov/bucket4j
Feather icons
* License: MIT License
* Project: https://feathericons.com/
* Source: https://github.com/feathericons/feather
jQuery
* License: MIT License
* Project: https://jquery.com/
* Source: https://github.com/jquery/jquery
Bootstrap
* License: MIT License
* Project: https://getbootstrap.com/
* Source: https://github.com/twbs/bootstrap

View File

@ -0,0 +1,317 @@
# Home Connect Binding
The binding integrates the [Home Connect](https://www.home-connect.com/) system into openHAB.
By using the Home Connect API it connects to household devices from brands like Bosch and Siemens.
Because all status updates and commands have to go through the API, a permanent internet connection is required.
## Supported Things
### Bridge
The __Home Connect API__ (Bridge Type ID: api_bridge) is responsible for the communication with the Home Connect API. All devices use a bridge to execute commands and listen for updates. Without a working bridge the devices cannot communicate.
### Devices
Supported devices: dishwasher, washer, washer / dryer combination, dryer, oven, refrigerator freezer, coffee machine, hood, cooktop*
*\* experimental support*
| Home appliance | Thing Type ID |
| --------------- | ------------ |
| Dishwasher | dishwasher |
| Washer | washer |
| Washer / Dryer combination | washerdryer |
| Dryer | dryer |
| Oven | oven |
| Hood | hood |
| Cooktop | hob |
| Refrigerator Freezer | fridgefreezer |
| Coffee Machine | coffeemaker |
> **INFO**: Currently the Home Connect API does not support all appliance programs. Please check if your desired program is available (e.g. https://developer.home-connect.com/docs/washing-machine/supported_programs_and_options).
## Discovery
After the bridge has been added and authorized, devices are discovered automatically.
## Channels
| Channel Type ID | Item Type | Read only | Description | Available on thing |
| --------------- | --------- | --------- | ----------- | ------------------ |
| power_state | Switch | false | This setting describes the current power state of the home appliance. | dishwasher, oven, coffeemaker, hood, hob |
| door_state | Contact | true | This status describes the door state of a home appliance. A status change is either triggered by the user operating the home appliance locally (i.e. opening/closing door) or automatically by the home appliance (i.e. locking the door). | dishwasher, washer, washerdryer, dryer, oven, fridgefreezer |
| operation_state | String | true | This status describes the operation state of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hood, hob, coffeemaker |
| remote_start_allowance_state | Switch | true | This status indicates whether the remote program start is enabled. This can happen due to a programmatic change (only disabling), or manually by the user changing the flag locally on the home appliance, or automatically after a certain duration - usually in 24 hours. | dishwasher, washer, washerdryer, dryer, oven, hood, coffeemaker |
| remote_control_active_state | Switch | true | This status indicates whether the allowance for remote controlling is enabled. | dishwasher, washer, washerdryer, dryer, oven, hood, hob |
| active_program_state | String | true | This status describes the active program of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hood, hob, coffeemaker |
| selected_program_state | String | false | This state describes the selected program of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hob, coffeemaker |
| remaining_program_time_state | Number:Time | true | This status indicates the remaining program time of the home appliance. | dishwasher, washer, washerdryer, dryer, oven |
| elapsed_program_time | Number:Time | true | This status indicates the elapsed program time of the home appliance. | oven |
| program_progress_state | Number:Dimensionless | true | This status describes the program progress of the home appliance in percent. | dishwasher, washer, washerdryer, dryer, oven, coffeemaker |
| duration | Number:Time | true | This status describes the duration of the program of the home appliance. | oven |
| oven_current_cavity_temperature | Number:Temperature | true | This status describes the current cavity temperature of the home appliance. | oven |
| setpoint_temperature | Number:Temperature | false | This status describes the setpoint/target temperature of the home appliance. | oven |
| laundry_care_washer_temperature | String | false | This status describes the temperature of the washing program of the home appliance. | washer, washerdryer |
| laundry_care_washer_spin_speed | String | false | This status defines the spin speed of a washer program of the home appliance. | washer, washerdryer |
| laundry_care_washer_idos1 | String | false | This status defines the i-Dos 1 dosing level of a washer program of the home appliance (if appliance supports i-Dos). | washer |
| laundry_care_washer_idos2 | String | false | This status defines the i-Dos 2 dosing level of a washer program of the home appliance (if appliance supports i-Dos). | washer |
| dryer_drying_target | String | false | This status defines the desired dryness of a program of the home appliance. | dryer, washerdryer |
| setpoint_temperature_refrigerator | Number:Temperature | false | Target temperature of the refrigerator compartment (range depends on appliance - common range 2 to 8°C). | fridgefreezer |
| setpoint_temperature_freezer | Number:Temperature | false | Target temperature of the freezer compartment (range depends on appliance - common range -16 to -24°C). | fridgefreezer |
| super_mode_refrigerator | Switch | false | The setting has no impact on setpoint temperatures but will make the fridge compartment cool to the lowest possible temperature until it is disabled manually by the customer or by the HA because of a timeout. | fridgefreezer |
| super_mode_freezer | Switch | false | This setting has no impact on setpoint temperatures but will make the freezer compartment cool to the lowest possible temperature until it is disabled manually by the customer or by the home appliance because of a timeout. | fridgefreezer |
| coffeemaker_drip_tray_full_state | Switch | true | Is coffee maker drip tray full? | coffeemaker |
| coffeemaker_water_tank_empty_state | Switch | true | Is coffee maker water tank empty? | coffeemaker |
| coffeemaker_bean_container_empty_state | Switch | true | Is coffee maker bean container empty? | coffeemaker |
| hood_venting_level | String | true | This option defines the required fan setting of the hood. | hood |
| hood_intensive_level | String | true | This option defines the intensive setting of the hood. | hood |
| hood_program_state | String | false | Adds hood controller actions to the appliance. The following commands are supported: `stop`, `venting1`, `venting2`, `venting3`, `venting4`, `venting5`, `ventingIntensive1`, `ventingIntensive1`, `automatic` and `delayed`. Furthermore it is possible to send raw (Home Connect JSON payload) to the home appliance. | hood |
| basic_actions_state | String | false | Adds basic controller actions to the appliance. The following basic commands are supported: `start` (start current selected program), `stop` (stop current program) and `selected` (show current program information). Furthermore it is possible to send raw (Home Connect JSON payload) to the home appliance. | dishwasher, oven, washer, washerdryer, dryer, coffeemaker |
| functional_light_state | Switch | false | This setting describes the current functional light state of the home appliance. | hood |
| functional_light_brightness_state | Dimmer | false | This setting describes the brightness state of the functional light. | hood |
| ambient_light_state | Switch | false | This setting describes the current ambient light state of the home appliance. | dishwasher, hood |
| ambient_light_brightness_state | Dimmer | false | This setting describes the brightness state of the ambient light. *INFO: Please note that the brightness can't be set if the ambient light color is set to `CustomColor`.* | dishwasher, hood |
| ambient_light_color_state | String | false | This setting describes the current ambient light color state of the home appliance. | dishwasher, hood |
| ambient_light_custom_color_state | Color | false | This setting describes the custom color state of the ambient light. HSB color commands are supported as well as hex color string e.g. `#11ff00`. *INFO: Please note that the brightness can't be set.* | dishwasher, hood |
## Thing Configuration
### Configuring the __Home Connect API__ Bridge
#### 1. Preconditions
1. Please create an account at [Home Connect](https://www.home-connect.com/) and add your physical appliance to your account.
2. Test the connection to your physical appliance via mobile app ([Apple App Store (iOS)](https://itunes.apple.com/de/app/home-connect-app/id901397789?mt=8) or [Google Play Store (Android)](https://play.google.com/store/apps/details?id=com.bshg.homeconnect.android.release)).
#### 2. Create Home Connect developer account
1. Create an account at [https://developer.home-connect.com](https://developer.home-connect.com) and login.
2. Please make sure you've added your associated Home Connect account email at <https://developer.home-connect.com/user/me/edit>. You should fill in your email address, which you use for the official Android or iOS app, at `Default Home Connect User Account for Testing`.
![Screenshot Home Connect profile page](doc/home_connect_profile.png "Screenshot Home Connect profile page")
3. Register / Create an application at [https://developer.home-connect.com/applications](https://developer.home-connect.com/applications)
* _Application ID_: e.g. `openhab-binding`
* _OAuth Flow_: Authorization Code Grant Flow
* _Home Connect User Account for Testing_: the associated user account email from [Home Connect](https://www.home-connect.com/)
> **WARNING**: Please don't use your developer account username
**_Please don't use your developer account username_**
* _Redirect URIs_: add your openHAB URL followed by `/homeconnect`
for example: `http://192.168.178.34:8080/homeconnect` or `https://myhome.domain.com/homeconnect`
* _One Time Token Mode_: keep unchecked
* _Proof Key for Code Exchange_: keep unchecked
4. After your application has been created, you should see the _Client ID_ and _Client Secret_ of the application. Please save these for later.
![Screenshot Home Connect application page](doc/home_connect_application.png "Screenshot Home Connect application page")
#### 3. Setup bridge (openHAB UI)
The Home Connect bridge can be configured in the openHAB UI as follows:
1. Go to the Inbox and press the add button
2. Choose `Home Connect Binding`
3. Select `Home Connect API`
4. Setup and save thing
* __client id:__ your application client id
* __client secret:__ your application client secret
* __simulator:__ false
5. Now navigate to the URL (`Redirct URI`) you've added to your Home Connect application in the previous step (2.3). For example `http://192.168.178.80:8080/homeconnect`.
6. Please follow the steps shown to authenticate your binding. You can redo this step every time. For example if you have authentication problems, just start wizard again.
![Screenshot Home Connect wizard page 1](doc/homeconnect_setup_1.png "Screenshot Home Connect wizard page 1")
![Screenshot Home Connect wizard page 2](doc/homeconnect_setup_2.png "Screenshot Home Connect wizard page 2")
![Screenshot Home Connect wizard page 3](doc/homeconnect_setup_3.png "Screenshot Home Connect wizard page 3")
![Screenshot Home Connect wizard page 4](doc/homeconnect_setup_4.png "Screenshot Home Connect wizard page 4")
7. That's it! Now you can use autodiscovery to add devices. Your devices should show up if you start a device scan in the openHAB UI.
## Examples: File based configuration
If you prefer to configure everything via file instead of openHAB UI, here are some examples.
### things/homeconnect.things
```
Bridge homeconnect:api_bridge:api_bridge_at_home "Home Connect API" [ clientId="1234", clientSecret="1234", simulator=false] {
// Thing configurations
Thing dishwasher dishwasher1 "Dishwasher" [ haId="SIEMENS-HCS02DWH1-6F2FC400C1EA4A" ]
Thing washer washer1 "Washer" [ haId="SIEMENS-HCS03WCH1-1F35EC2BE34A0F" ]
Thing fridgefreezer fridge1 "Fridge Freezer [ haId="SIEMENS-HCS05FRF1-7B3FA5EB3D885B" ]
Thing oven oven1 "Oven" [ haId="BOSCH-HCS01OVN1-2132B6FA25BA21" ]
Thing dryer dryer1 "Dryer" [ haId="BOSCH-HCS04DYR1-3921C766AD5BAF" ]
Thing coffeemaker coffee1 "Coffee machine" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ]
Thing washerdryer washerdryer1 "Washerdryer" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ]
Thing fridgefreezer fridgefreezer1 "Fridge/Freezer" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ]
Thing hood hood1 "Hood" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ]
Thing hob hob1 "Hob" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ]
}
```
### items/homeconnect.items
The channel parameter uses the following syntax: `homeconnect:<thing type id>:<bridge id>:<thing id>:<channel type id>`. For example: `homeconnect:dishwasher:api_bridge_at_home:dishwasher1:power_state`
```
// dishwasher
Switch Dishwasher_PowerState "Power State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:power_state"}
Contact Dishwasher_DoorState "Door State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:door_state"}
String Dishwasher_OperationState "Operation State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:operation_state"}
Switch Dishwasher_RemoteStartAllowanceState "Remote Start Allowance State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remote_start_allowance_state"}
Switch Dishwasher_RemoteControlActiveState "Remote Control Activation State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remote_control_active_state"}
String Dishwasher_SelectedProgramState "Selected Program" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:selected_program_state"}
String Dishwasher_ActiveProgramState "Active Program" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:active_program_state"}
Number:Time Dishwasher_RemainingProgramTimeState "Remaining program time" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remaining_program_time_state"}
Number:Dimensionless Dishwasher_ProgramProgressState "Progress State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:program_progress_state"}
```
## Home Connect Console
The binding comes with a separate user interface, which is reachable through the web browser http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect (e.g. http://192.168.178.100:8080/homeconnect).
Features:
* overview of your bridges and appliances
* send commands to your appliances
* see latest API requests
* see received events from the Home Connect backend
* API request counts
> **INFO**: If you have a problems with your installation, please always provide request and event exports. ![Screenshot Home Connect wizard page 4](doc/export_button.png "Export button")
## How To
### Notification on credential error
To get notified when your Home Connect credentials have been revoked or expired you can use the following rule to get notified.
This can happen if
* your openHAB instance was offline for a longer period or
* new terms weren't accepted or
* a technical problem occurred.
```java
rule "Offline check - Home Connect bridge"
when
Thing "<thingUID>" changed
then
val statusInfo = getThingStatusInfo("<thingUID>")
val status = statusInfo.getStatus()
val statusDetail = statusInfo.getStatusDetail()
if ((status !== null) && (statusDetail !== null)) {
logInfo("api_bridge", "Home Connect bridge status: " + status.toString() + " detail: " + statusDetail.toString())
if (status.toString() == 'OFFLINE' && statusDetail.toString() == 'CONFIGURATION_PENDING') {
logError("api_bridge", "Home Connect bridge offline.")
// send push, email, ...
}
}
end
```
### Start program with custom settings
Currently, not all program options of a device are available as items in openHAB. For example, you cannot change the `Fill quantity` of a coffee maker program. If you wish to start a program with a custom setting, you can send a special command to the item of type `basic_actions_state`.
> **INFO**: Only for advanced users. You need to know how to use the `curl` command. Alternatively you you can use the binding UI to trigger the commands.
#### 1. Retrieve "special command" payload
You have a couple options to get the program settings payload.
a) You could have a look at the Home Connect developer documentation (https://developer.home-connect.com/docs/) and create the payload on your own.
b) You could have a look at the request logs and extract the payload from there.
1. On the physical device, select your desired program with the appropriate options.
2. Open the appliance section of the binding UI (http(s)://[YOUROPENHAB]:[YOURPORT]/appliances) and click the 'Selected Program' button.
![Screenshot Home Connect wizard page 4](doc/selected_program_1.png "Get selected program")
3. ![Screenshot Home Connect wizard page 4](doc/selected_program_2.png "Get selected program") Copy the JSON payload. In a further step, this payload will be used to start the program.
#### 2. Start program
After you've extracted the desired program command, you can start your program via openHAB rule or through a `curl` command.
##### in rule
*Example rule:*
```java
rule "trigger program"
when
Time cron "0 32 13 ? * * *"
then
homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state.sendCommand('{"data":{"key":"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato","options":[{"key":"ConsumerProducts.CoffeeMaker.Option.CoffeeTemperature","value":"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.BeanAmount","value":"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.FillQuantity","value":60,"unit":"ml"}]}}')
end
```
Please replace `homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state` with your item name (of channel type `basic_actions_state`).
##### via curl
*Example command:*
```bash
curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d '{"data":{"key":"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato","options":[{"key":"ConsumerProducts.CoffeeMaker.Option.CoffeeTemperature","value":"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.BeanAmount","value":"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.FillQuantity","value":60,"unit":"ml"}]}}' "http://localhost:8080/rest/items/homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state"
```
Please replace `homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state` with your item name (of channel type `basic_actions_state`).
## FAQ
### I can't start my oven via openHAB.
Some operations are not possible at the moment. You need to sign an "Additional Partner Agreement". Please have a look at:
https://developer.home-connect.com/docs/authorization/scope
### I can't switch remote start to on.
The channel of type `remote_start_allowance_state` is read only. You can only enable it directly on the physical appliance.
### In case of error...
Please check log UI (http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect) and ask for help in the community forum or on github. Please provide request and event exports.
![Screenshot Home Connect wizard page 4](doc/export_button.png "Export button")
### Rate limit reached
The Home Connect API enforces rate [limits](https://developer.home-connect.com/docs/general/ratelimiting). If you have a lot of `429` response codes in your request log section (http(s)://[YOUROPENHAB]:[YOURPORT]/log/requests), please check the error response.
### Error message 'Program not supported', 'Unsupported operation' or 'SDK.Error.UnsupportedOption'
Not all appliance programs and program options are supported by the Home Connect API. Unfortunately you can't use them. You will see error messages like the following in the binding UI (request log):
```json
{
"error": {
"key": "SDK.Error.UnsupportedProgram",
"description": "Unsupported operation: LaundryCare.Washer.Program.Cotton.CottonEco"
}
}
```
```json
{
"error": {
"key": "SDK.Error.UnsupportedProgram",
"description": "Program not supported"
}
}
```
### How to find the Home Appliance ID (HaID) of my device?
You have two options to find the right HaID of your device.
1. You can use the openHAB UI and start a scan. ![Screenshot openHAB UI Scan for new devices](doc/ui-scan-for-haid.png "Scan")
2. You can use Home Connect binding UI. Please have a look at the first API request. ![Screenshot Home Connect Binding UI](doc/binding-ui-haid.png "First request")

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.homeconnect</artifactId>
<name>openHAB Add-ons :: Bundles :: Home Connect Binding</name>
<properties>
<bnd.importpackage>android.*;resolution:="optional",com.android.*;resolution:="optional",dalvik.*;resolution:="optional",org.apache.harmony.*;resolution:="optional",org.conscrypt.*;resolution:="optional",sun.*;resolution:="optional",javax.annotation.meta.*;resolution:="optional",com.fasterxml.jackson.*;resolution:="optional",com.sun.jdi.*;resolution:="optional"</bnd.importpackage>
</properties>
<dependencies>
<!--Thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.1.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.attoparser</groupId>
<artifactId>attoparser</artifactId>
<version>2.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
<scope>compile</scope>
</dependency>
<!-- lib for rate limiting -->
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>4.10.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.homeconnect-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
</repository>
<feature name="openhab-binding-homeconnect" description="HomeConnect Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.homeconnect/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,231 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link HomeConnectBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectBindingConstants {
public static final String BINDING_ID = "homeconnect";
public static final String HA_ID = "haId";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_API_BRIDGE = new ThingTypeUID(BINDING_ID, "api_bridge");
public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher");
public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven");
public static final ThingTypeUID THING_TYPE_WASHER = new ThingTypeUID(BINDING_ID, "washer");
public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washerdryer");
public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridgefreezer");
public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer");
public static final ThingTypeUID THING_TYPE_COFFEE_MAKER = new ThingTypeUID(BINDING_ID, "coffeemaker");
public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood");
public static final ThingTypeUID THING_TYPE_COOKTOP = new ThingTypeUID(BINDING_ID, "hob");
// Setting
public static final String SETTING_POWER_STATE = "BSH.Common.Setting.PowerState";
public static final String SETTING_LIGHTING = "Cooking.Common.Setting.Lighting";
public static final String SETTING_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled";
public static final String SETTING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness";
public static final String SETTING_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness";
public static final String SETTING_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor";
public static final String SETTING_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor";
public static final String SETTING_FREEZER_SETPOINT_TEMPERATURE = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer";
public static final String SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator";
public static final String SETTING_REFRIGERATOR_SUPER_MODE = "Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator";
public static final String SETTING_FREEZER_SUPER_MODE = "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer";
// Status
public static final String STATUS_DOOR_STATE = "BSH.Common.Status.DoorState";
public static final String STATUS_OPERATION_STATE = "BSH.Common.Status.OperationState";
public static final String STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE = "Cooking.Oven.Status.CurrentCavityTemperature";
public static final String STATUS_REMOTE_CONTROL_START_ALLOWED = "BSH.Common.Status.RemoteControlStartAllowed";
public static final String STATUS_REMOTE_CONTROL_ACTIVE = "BSH.Common.Status.RemoteControlActive";
public static final String STATUS_LOCAL_CONTROL_ACTIVE = "BSH.Common.Status.LocalControlActive";
// SSE Event types
public static final String EVENT_ELAPSED_PROGRAM_TIME = "BSH.Common.Option.ElapsedProgramTime";
public static final String EVENT_OVEN_CAVITY_TEMPERATURE = STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE;
public static final String EVENT_POWER_STATE = SETTING_POWER_STATE;
public static final String EVENT_CONNECTED = "CONNECTED";
public static final String EVENT_DISCONNECTED = "DISCONNECTED";
public static final String EVENT_DOOR_STATE = STATUS_DOOR_STATE;
public static final String EVENT_OPERATION_STATE = STATUS_OPERATION_STATE;
public static final String EVENT_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram";
public static final String EVENT_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram";
public static final String EVENT_REMOTE_CONTROL_START_ALLOWED = STATUS_REMOTE_CONTROL_START_ALLOWED;
public static final String EVENT_REMOTE_CONTROL_ACTIVE = STATUS_REMOTE_CONTROL_ACTIVE;
public static final String EVENT_LOCAL_CONTROL_ACTIVE = STATUS_LOCAL_CONTROL_ACTIVE;
public static final String EVENT_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime";
public static final String EVENT_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress";
public static final String EVENT_SETPOINT_TEMPERATURE = "Cooking.Oven.Option.SetpointTemperature";
public static final String EVENT_DURATION = "BSH.Common.Option.Duration";
public static final String EVENT_WASHER_TEMPERATURE = "LaundryCare.Washer.Option.Temperature";
public static final String EVENT_WASHER_SPIN_SPEED = "LaundryCare.Washer.Option.SpinSpeed";
public static final String EVENT_WASHER_IDOS_1_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos1DosingLevel";
public static final String EVENT_WASHER_IDOS_2_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos2DosingLevel";
public static final String EVENT_FREEZER_SETPOINT_TEMPERATURE = SETTING_FREEZER_SETPOINT_TEMPERATURE;
public static final String EVENT_FRIDGE_SETPOINT_TEMPERATURE = SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE;
public static final String EVENT_FREEZER_SUPER_MODE = SETTING_FREEZER_SUPER_MODE;
public static final String EVENT_FRIDGE_SUPER_MODE = SETTING_REFRIGERATOR_SUPER_MODE;
public static final String EVENT_DRYER_DRYING_TARGET = "LaundryCare.Dryer.Option.DryingTarget";
public static final String EVENT_COFFEEMAKER_BEAN_CONTAINER_EMPTY = "ConsumerProducts.CoffeeMaker.Event.BeanContainerEmpty";
public static final String EVENT_COFFEEMAKER_WATER_TANK_EMPTY = "ConsumerProducts.CoffeeMaker.Event.WaterTankEmpty";
public static final String EVENT_COFFEEMAKER_DRIP_TRAY_FULL = "ConsumerProducts.CoffeeMaker.Event.DripTrayFull";
public static final String EVENT_HOOD_VENTING_LEVEL = "Cooking.Common.Option.Hood.VentingLevel";
public static final String EVENT_HOOD_INTENSIVE_LEVEL = "Cooking.Common.Option.Hood.IntensiveLevel";
public static final String EVENT_FUNCTIONAL_LIGHT_STATE = SETTING_LIGHTING;
public static final String EVENT_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE = SETTING_LIGHTING_BRIGHTNESS;
public static final String EVENT_AMBIENT_LIGHT_STATE = SETTING_AMBIENT_LIGHT_ENABLED;
public static final String EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE = SETTING_AMBIENT_LIGHT_BRIGHTNESS;
public static final String EVENT_AMBIENT_LIGHT_COLOR_STATE = SETTING_AMBIENT_LIGHT_COLOR;
public static final String EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE = SETTING_AMBIENT_LIGHT_CUSTOM_COLOR;
// Channel IDs
public static final String CHANNEL_DOOR_STATE = "door_state";
public static final String CHANNEL_ELAPSED_PROGRAM_TIME = "elapsed_program_time";
public static final String CHANNEL_POWER_STATE = "power_state";
public static final String CHANNEL_OPERATION_STATE = "operation_state";
public static final String CHANNEL_ACTIVE_PROGRAM_STATE = "active_program_state";
public static final String CHANNEL_SELECTED_PROGRAM_STATE = "selected_program_state";
public static final String CHANNEL_BASIC_ACTIONS_STATE = "basic_actions_state";
public static final String CHANNEL_REMOTE_START_ALLOWANCE_STATE = "remote_start_allowance_state";
public static final String CHANNEL_REMOTE_CONTROL_ACTIVE_STATE = "remote_control_active_state";
public static final String CHANNEL_LOCAL_CONTROL_ACTIVE_STATE = "local_control_active_state";
public static final String CHANNEL_REMAINING_PROGRAM_TIME_STATE = "remaining_program_time_state";
public static final String CHANNEL_PROGRAM_PROGRESS_STATE = "program_progress_state";
public static final String CHANNEL_OVEN_CURRENT_CAVITY_TEMPERATURE = "oven_current_cavity_temperature";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint_temperature";
public static final String CHANNEL_DURATION = "duration";
public static final String CHANNEL_WASHER_TEMPERATURE = "laundry_care_washer_temperature";
public static final String CHANNEL_WASHER_SPIN_SPEED = "laundry_care_washer_spin_speed";
public static final String CHANNEL_WASHER_IDOS1 = "laundry_care_washer_idos1";
public static final String CHANNEL_WASHER_IDOS2 = "laundry_care_washer_idos2";
public static final String CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE = "setpoint_temperature_refrigerator";
public static final String CHANNEL_REFRIGERATOR_SUPER_MODE = "super_mode_refrigerator";
public static final String CHANNEL_FREEZER_SETPOINT_TEMPERATURE = "setpoint_temperature_freezer";
public static final String CHANNEL_FREEZER_SUPER_MODE = "super_mode_freezer";
public static final String CHANNEL_DRYER_DRYING_TARGET = "dryer_drying_target";
public static final String CHANNEL_COFFEEMAKER_DRIP_TRAY_FULL_STATE = "coffeemaker_drip_tray_full_state";
public static final String CHANNEL_COFFEEMAKER_WATER_TANK_EMPTY_STATE = "coffeemaker_water_tank_empty_state";
public static final String CHANNEL_COFFEEMAKER_BEAN_CONTAINER_EMPTY_STATE = "coffeemaker_bean_container_empty_state";
public static final String CHANNEL_HOOD_VENTING_LEVEL = "hood_venting_level";
public static final String CHANNEL_HOOD_INTENSIVE_LEVEL = "hood_intensive_level";
public static final String CHANNEL_HOOD_ACTIONS_STATE = "hood_program_state";
public static final String CHANNEL_FUNCTIONAL_LIGHT_STATE = "functional_light_state";
public static final String CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE = "functional_light_brightness_state";
public static final String CHANNEL_AMBIENT_LIGHT_STATE = "ambient_light_state";
public static final String CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE = "ambient_light_brightness_state";
public static final String CHANNEL_AMBIENT_LIGHT_COLOR_STATE = "ambient_light_color_state";
public static final String CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE = "ambient_light_custom_color_state";
// List of all supported devices
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_API_BRIDGE,
THING_TYPE_DISHWASHER, THING_TYPE_OVEN, THING_TYPE_WASHER, THING_TYPE_DRYER, THING_TYPE_WASHER_DRYER,
THING_TYPE_FRIDGE_FREEZER, THING_TYPE_COFFEE_MAKER, THING_TYPE_HOOD, THING_TYPE_COOKTOP);
// Discoverable devices
public static final Set<ThingTypeUID> DISCOVERABLE_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_DISHWASHER,
THING_TYPE_OVEN, THING_TYPE_WASHER, THING_TYPE_DRYER, THING_TYPE_WASHER_DRYER, THING_TYPE_FRIDGE_FREEZER,
THING_TYPE_COFFEE_MAKER, THING_TYPE_HOOD, THING_TYPE_COOKTOP);
// List of state values
public static final String STATE_POWER_OFF = "BSH.Common.EnumType.PowerState.Off";
public static final String STATE_POWER_ON = "BSH.Common.EnumType.PowerState.On";
public static final String STATE_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby";
public static final String STATE_DOOR_OPEN = "BSH.Common.EnumType.DoorState.Open";
public static final String STATE_DOOR_LOCKED = "BSH.Common.EnumType.DoorState.Locked";
public static final String STATE_DOOR_CLOSED = "BSH.Common.EnumType.DoorState.Closed";
public static final String STATE_OPERATION_READY = "BSH.Common.EnumType.OperationState.Ready";
public static final String STATE_OPERATION_FINISHED = "BSH.Common.EnumType.OperationState.Finished";
public static final String STATE_OPERATION_RUN = "BSH.Common.EnumType.OperationState.Run";
public static final String STATE_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off";
// List of program options
public static final String OPTION_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime";
public static final String OPTION_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress";
public static final String OPTION_ELAPSED_PROGRAM_TIME = "BSH.Common.Option.ElapsedProgramTime";
public static final String OPTION_SETPOINT_TEMPERATURE = "Cooking.Oven.Option.SetpointTemperature";
public static final String OPTION_DURATION = "BSH.Common.Option.Duration";
public static final String OPTION_WASHER_TEMPERATURE = "LaundryCare.Washer.Option.Temperature";
public static final String OPTION_WASHER_SPIN_SPEED = "LaundryCare.Washer.Option.SpinSpeed";
public static final String OPTION_WASHER_IDOS_1_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos1DosingLevel";
public static final String OPTION_WASHER_IDOS_2_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos2DosingLevel";
public static final String OPTION_DRYER_DRYING_TARGET = "LaundryCare.Dryer.Option.DryingTarget";
public static final String OPTION_HOOD_VENTING_LEVEL = "Cooking.Common.Option.Hood.VentingLevel";
public static final String OPTION_HOOD_INTENSIVE_LEVEL = "Cooking.Common.Option.Hood.IntensiveLevel";
// List of stages
public static final String STAGE_FAN_OFF = "Cooking.Hood.EnumType.Stage.FanOff";
public static final String STAGE_FAN_STAGE_01 = "Cooking.Hood.EnumType.Stage.FanStage01";
public static final String STAGE_FAN_STAGE_02 = "Cooking.Hood.EnumType.Stage.FanStage02";
public static final String STAGE_FAN_STAGE_03 = "Cooking.Hood.EnumType.Stage.FanStage03";
public static final String STAGE_FAN_STAGE_04 = "Cooking.Hood.EnumType.Stage.FanStage04";
public static final String STAGE_FAN_STAGE_05 = "Cooking.Hood.EnumType.Stage.FanStage05";
public static final String STAGE_INTENSIVE_STAGE_OFF = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStageOff";
public static final String STAGE_INTENSIVE_STAGE_1 = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1";
public static final String STAGE_INTENSIVE_STAGE_2 = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStage2";
public static final String STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = "BSH.Common.EnumType.AmbientLightColor.CustomColor";
// List of programs
public static final String PROGRAM_HOOD_AUTOMATIC = "Cooking.Common.Program.Hood.Automatic";
public static final String PROGRAM_HOOD_VENTING = "Cooking.Common.Program.Hood.Venting";
public static final String PROGRAM_HOOD_DELAYED_SHUT_OFF = "Cooking.Common.Program.Hood.DelayedShutOff";
// Network and oAuth constants
public static final String API_BASE_URL = "https://api.home-connect.com";
public static final String API_SIMULATOR_BASE_URL = "https://simulator.home-connect.com";
public static final String OAUTH_TOKEN_PATH = "/security/oauth/token";
public static final String OAUTH_AUTHORIZE_PATH = "/security/oauth/authorize";
public static final String OAUTH_SCOPE = "IdentifyAppliance Monitor Settings Dishwasher-Control Washer-Control Dryer-Control WasherDryer-Control CoffeeMaker-Control Hood-Control CleaningRobot-Control";
// Operation states
public static final String OPERATION_STATE_INACTIVE = "BSH.Common.EnumType.OperationState.Inactive";
public static final String OPERATION_STATE_READY = "BSH.Common.EnumType.OperationState.Ready";
public static final String OPERATION_STATE_DELAYED_START = "BSH.Common.EnumType.OperationState.DelayedStart";
public static final String OPERATION_STATE_RUN = "BSH.Common.EnumType.OperationState.Run";
public static final String OPERATION_STATE_PAUSE = "BSH.Common.EnumType.OperationState.Pause";
public static final String OPERATION_STATE_ACTION_REQUIRED = "BSH.Common.EnumType.OperationState.ActionRequired";
public static final String OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished";
public static final String OPERATION_STATE_ERROR = "BSH.Common.EnumType.OperationState.Error";
public static final String OPERATION_STATE_ABORTING = "BSH.Common.EnumType.OperationState.Aborting";
// Commands
public static final String COMMAND_START = "start";
public static final String COMMAND_STOP = "stop";
public static final String COMMAND_SELECTED = "selected";
public static final String COMMAND_VENTING_1 = "venting1";
public static final String COMMAND_VENTING_2 = "venting2";
public static final String COMMAND_VENTING_3 = "venting3";
public static final String COMMAND_VENTING_4 = "venting4";
public static final String COMMAND_VENTING_5 = "venting5";
public static final String COMMAND_VENTING_INTENSIVE_1 = "ventingIntensive1";
public static final String COMMAND_VENTING_INTENSIVE_2 = "ventingIntensive2";
public static final String COMMAND_AUTOMATIC = "automatic";
public static final String COMMAND_DELAYED_SHUT_OFF = "delayed";
// light
public static final int BRIGHTNESS_MIN = 10;
public static final int BRIGHTNESS_MAX = 100;
public static final int BRIGHTNESS_DIM_STEP = 10;
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ArrayBlockingQueue;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* FIFO queue (ring buffer implementation).
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class CircularQueue<E> {
private final ArrayBlockingQueue<E> queue;
public CircularQueue(final int capacity) {
queue = new ArrayBlockingQueue<>(capacity);
}
public synchronized void add(E element) {
ArrayBlockingQueue<E> myQueue = queue;
if (myQueue.remainingCapacity() <= 0) {
myQueue.poll();
}
myQueue.add(element);
}
public synchronized void addAll(Collection<? extends E> collection) {
collection.forEach(this::add);
}
public synchronized Collection<E> getAll() {
return new ArrayList<>(queue);
}
}

View File

@ -0,0 +1,200 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.sse.SseEventSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
import org.openhab.binding.homeconnect.internal.client.model.Event;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Server-Sent-Events client for Home Connect API.
*
* @author Jonas Brüstel - Initial contribution
* @author Laurent Garnier - Replace okhttp SSE by JAX-RS SSE
*
*/
@NonNullByDefault
public class HomeConnectEventSourceClient {
private static final int SSE_REQUEST_READ_TIMEOUT = 90;
private static final int EVENT_QUEUE_SIZE = 300;
private final String apiUrl;
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final OAuthClientService oAuthClientService;
private final Map<HomeConnectEventListener, SseEventSource> eventSourceConnections;
private final Map<SseEventSource, HomeConnectEventSourceListener> eventSourceListeners;
private final ScheduledExecutorService scheduler;
private final CircularQueue<Event> eventQueue;
private final Logger logger = LoggerFactory.getLogger(HomeConnectEventSourceClient.class);
public HomeConnectEventSourceClient(ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory,
OAuthClientService oAuthClientService, boolean simulated, ScheduledExecutorService scheduler,
@Nullable List<Event> eventHistory) {
this.scheduler = scheduler;
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.oAuthClientService = oAuthClientService;
apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
eventSourceConnections = new HashMap<>();
eventSourceListeners = new HashMap<>();
eventQueue = new CircularQueue<>(EVENT_QUEUE_SIZE);
if (eventHistory != null) {
eventQueue.addAll(eventHistory);
}
}
/**
* Register {@link HomeConnectEventListener} to receive events by Home Connect API. This helps to reduce the
* amount of request you would usually need to update all channels.
*
* Checkout rate limits of the API at. https://developer.home-connect.com/docs/general/ratelimiting
*
* @param haId appliance id
* @param eventListener appliance event listener
* @throws CommunicationException API communication exception
* @throws AuthorizationException oAuth authorization exception
*/
public synchronized void registerEventListener(final String haId, final HomeConnectEventListener eventListener)
throws CommunicationException, AuthorizationException {
logger.debug("Register event listener for '{}': {}", haId, eventListener);
if (!eventSourceConnections.containsKey(eventListener)) {
logger.debug("Create new event source listener for '{}'.", haId);
Client client = clientBuilder.readTimeout(SSE_REQUEST_READ_TIMEOUT, TimeUnit.SECONDS).register(
new HomeConnectStreamingRequestFilter(HttpHelper.getAuthorizationHeader(oAuthClientService)))
.build();
SseEventSource eventSource = eventSourceFactory
.newSource(client.target(apiUrl + "/api/homeappliances/" + haId + "/events"));
HomeConnectEventSourceListener eventSourceListener = new HomeConnectEventSourceListener(haId, eventListener,
this, scheduler, eventQueue);
eventSource.register(eventSourceListener::onEvent, eventSourceListener::onError,
eventSourceListener::onComplete);
eventSourceListeners.put(eventSource, eventSourceListener);
eventSourceConnections.put(eventListener, eventSource);
eventSource.open();
}
}
/**
* Unregister {@link HomeConnectEventListener}.
*
* @param eventListener appliance event listener
*/
public synchronized void unregisterEventListener(HomeConnectEventListener eventListener) {
unregisterEventListener(eventListener, false, false);
}
/**
* Unregister {@link HomeConnectEventListener}.
*
* @param eventListener appliance event listener
* @param completed true when the event source is known as already completed by the server
*/
public synchronized void unregisterEventListener(HomeConnectEventListener eventListener, boolean completed) {
unregisterEventListener(eventListener, false, completed);
}
/**
* Unregister {@link HomeConnectEventListener}.
*
* @param eventListener appliance event listener
* @param immediate true when the unregistering of the event source has to be fast
* @param completed true when the event source is known as already completed by the server
*/
public synchronized void unregisterEventListener(HomeConnectEventListener eventListener, boolean immediate,
boolean completed) {
if (eventSourceConnections.containsKey(eventListener)) {
SseEventSource eventSource = eventSourceConnections.get(eventListener);
if (eventSource != null) {
closeEventSource(eventSource, immediate, completed);
eventSourceListeners.remove(eventSource);
}
eventSourceConnections.remove(eventListener);
}
}
private void closeEventSource(SseEventSource eventSource, boolean immediate, boolean completed) {
if (eventSource.isOpen() && !completed) {
logger.debug("Close event source (immediate = {})", immediate);
eventSource.close(immediate ? 0 : 10, TimeUnit.SECONDS);
}
HomeConnectEventSourceListener eventSourceListener = eventSourceListeners.get(eventSource);
if (eventSourceListener != null) {
eventSourceListener.stopMonitor();
}
}
/**
* Connection count.
*
* @return connection count
*/
public synchronized int connectionSize() {
return eventSourceConnections.size();
}
/**
* Dispose event source client
*
* @param immediate true to request a fast execution
*/
public synchronized void dispose(boolean immediate) {
eventSourceConnections.forEach((key, eventSource) -> closeEventSource(eventSource, immediate, false));
eventSourceListeners.clear();
eventSourceConnections.clear();
}
/**
* Get latest events
*
* @return event queue
*/
public Collection<Event> getLatestEvents() {
return eventQueue.getAll();
}
/**
* Get latest events by haId
*
* @param haId appliance id
* @return event queue
*/
public List<Event> getLatestEvents(String haId) {
return eventQueue.getAll().stream().filter(event -> haId.equals(event.getHaId())).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,225 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client;
import static java.time.LocalDateTime.now;
import static org.openhab.binding.homeconnect.internal.client.model.EventType.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.sse.InboundSseEvent;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
import org.openhab.binding.homeconnect.internal.client.model.Event;
import org.openhab.binding.homeconnect.internal.client.model.EventHandling;
import org.openhab.binding.homeconnect.internal.client.model.EventLevel;
import org.openhab.binding.homeconnect.internal.client.model.EventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* Event source listener (Server-Sent-Events).
*
* @author Jonas Brüstel - Initial contribution
* @author Laurent Garnier - Replace okhttp SSE by JAX-RS SSE
*
*/
@NonNullByDefault
public class HomeConnectEventSourceListener {
private static final String EMPTY_DATA = "\"\"";
private static final int SSE_MONITOR_INITIAL_DELAY_MIN = 1;
private static final int SSE_MONITOR_INTERVAL_MIN = 5;
private static final int SSE_MONITOR_BROKEN_CONNECTION_TIMEOUT_MIN = 3;
private final String haId;
private final HomeConnectEventListener eventListener;
private final HomeConnectEventSourceClient client;
private final Logger logger = LoggerFactory.getLogger(HomeConnectEventSourceListener.class);
private final ScheduledFuture<?> eventSourceMonitorFuture;
private final CircularQueue<Event> eventQueue;
private final ScheduledExecutorService scheduledExecutorService;
private @Nullable LocalDateTime lastEventReceived;
public HomeConnectEventSourceListener(String haId, final HomeConnectEventListener eventListener,
final HomeConnectEventSourceClient client, final ScheduledExecutorService scheduler,
CircularQueue<Event> eventQueue) {
this.haId = haId;
this.eventListener = eventListener;
this.client = client;
this.eventQueue = eventQueue;
this.scheduledExecutorService = scheduler;
eventSourceMonitorFuture = createMonitor(scheduler);
}
public void onEvent(InboundSseEvent inboundEvent) {
String id = inboundEvent.getId();
String type = inboundEvent.getName();
String data = inboundEvent.readData();
lastEventReceived = now();
EventType eventType = valueOfType(type);
if (eventType != null) {
mapEventSourceEventToEvent(haId, eventType, data).forEach(event -> {
eventQueue.add(event);
logger.debug("Received event ({}): {}", haId, event);
try {
eventListener.onEvent(event);
} catch (Exception e) {
logger.error("Could not publish event to Listener!", e);
}
});
} else {
logger.warn("Received unknown event source type! haId={}, id={}, type={}, data={}", haId, id, type, data);
}
}
public void onComplete() {
logger.debug("Event source listener channel closed ({}).", haId);
client.unregisterEventListener(eventListener, true);
try {
eventListener.onClosed();
} catch (Exception e) {
logger.error("Could not publish closed event to listener ({})!", haId, e);
}
stopMonitor();
}
public void onError(Throwable error) {
String throwableMessage = error.getMessage();
String throwableClass = error.getClass().getName();
logger.debug("Event source listener connection failure occurred. haId={}, throwable={}, throwableMessage={}",
haId, throwableClass, throwableMessage);
client.unregisterEventListener(eventListener);
try {
if (throwableMessage != null
&& throwableMessage.contains(String.valueOf(HttpStatus.TOO_MANY_REQUESTS_429))) {
logger.warn(
"More than 10 active event monitoring channels was reached. Further event monitoring requests are blocked. haId={}",
haId);
eventListener.onRateLimitReached();
} else {
// The SSE connection is closed by the server every 24 hours.
// When you try to reconnect, it often fails with a NotAuthorizedException (401) for the next few
// seconds. So we wait few seconds before trying again.
if (error instanceof NotAuthorizedException) {
logger.debug(
"Event source listener connection failure due to unauthorized exception : wait 10 seconds... haId={}",
haId);
scheduledExecutorService.schedule(() -> eventListener.onClosed(), 10, TimeUnit.SECONDS);
} else {
eventListener.onClosed();
}
}
} catch (Exception e) {
logger.error("Could not publish closed event to listener ({})!", haId, e);
}
stopMonitor();
}
private ScheduledFuture<?> createMonitor(ScheduledExecutorService scheduler) {
return scheduler.scheduleWithFixedDelay(() -> {
logger.trace("Check event source connection ({}). Last event package received at {}.", haId,
lastEventReceived);
if (lastEventReceived != null && ChronoUnit.MINUTES.between(lastEventReceived,
now()) > SSE_MONITOR_BROKEN_CONNECTION_TIMEOUT_MIN) {
logger.warn("Dead event source connection detected ({}).", haId);
client.unregisterEventListener(eventListener);
try {
eventListener.onClosed();
} catch (Exception e) {
logger.error("Could not publish closed event to listener ({})!", haId, e);
}
stopMonitor();
}
}, SSE_MONITOR_INITIAL_DELAY_MIN, SSE_MONITOR_INTERVAL_MIN, TimeUnit.MINUTES);
}
public void stopMonitor() {
if (!eventSourceMonitorFuture.isDone()) {
logger.debug("Dispose event source connection monitor of appliance ({}).", haId);
eventSourceMonitorFuture.cancel(true);
}
}
private List<Event> mapEventSourceEventToEvent(String haId, EventType type, @Nullable String data) {
List<Event> events = new ArrayList<>();
if ((STATUS.equals(type) || EVENT.equals(type) || NOTIFY.equals(type)) && data != null && !data.trim().isEmpty()
&& !EMPTY_DATA.equals(data)) {
try {
JsonObject responseObject = HttpHelper.parseString(data).getAsJsonObject();
JsonArray items = responseObject.getAsJsonArray("items");
items.forEach(item -> {
JsonObject obj = (JsonObject) item;
String key = getJsonElementAsString(obj, "key").orElse(null);
String value = getJsonElementAsString(obj, "value").orElse(null);
String unit = getJsonElementAsString(obj, "unit").orElse(null);
String name = getJsonElementAsString(obj, "name").orElse(null);
String uri = getJsonElementAsString(obj, "uri").orElse(null);
EventLevel level = getJsonElementAsString(obj, "level").map(EventLevel::valueOfLevel).orElse(null);
EventHandling handling = getJsonElementAsString(obj, "handling").map(EventHandling::valueOfHandling)
.orElse(null);
ZonedDateTime creation = getJsonElementAsLong(obj, "timestamp").map(timestamp -> ZonedDateTime
.ofInstant(Instant.ofEpochSecond(timestamp), TimeZone.getDefault().toZoneId()))
.orElse(ZonedDateTime.now());
events.add(new Event(haId, type, key, name, uri, creation, level, handling, value, unit));
});
} catch (IllegalStateException e) {
logger.error("Could not parse event! haId={}, error={}", haId, e.getMessage());
}
} else {
events.add(new Event(haId, type));
}
return events;
}
private Optional<Long> getJsonElementAsLong(JsonObject jsonObject, String elementName) {
var element = jsonObject.get(elementName);
return element == null || element.isJsonNull() ? Optional.empty() : Optional.of(element.getAsLong());
}
private Optional<String> getJsonElementAsString(JsonObject jsonObject, String elementName) {
var element = jsonObject.get(elementName);
return element == null || element.isJsonNull() ? Optional.empty() : Optional.of(element.getAsString());
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client;
import java.io.IOException;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Inserts Authorization header for requests on the streaming REST API.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class HomeConnectStreamingRequestFilter implements ClientRequestFilter {
private static final String TEXT_EVENT_STREAM = "text/event-stream";
private final String authorizationHeader;
public HomeConnectStreamingRequestFilter(String authorizationHeader) {
this.authorizationHeader = authorizationHeader;
}
@Override
public void filter(@Nullable ClientRequestContext requestContext) throws IOException {
if (requestContext != null) {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.putSingle(HttpHeaders.AUTHORIZATION, authorizationHeader);
headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
headers.putSingle(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM);
}
}
}

View File

@ -0,0 +1,146 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client;
import static io.github.bucket4j.Bandwidth.classic;
import static io.github.bucket4j.Refill.intervally;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
/**
* okHttp helper.
*
* @author Jonas Brüstel - Initial contribution
* @author Laurent Garnier - Removed okhttp
*
*/
@NonNullByDefault
public class HttpHelper {
private static final String BEARER = "Bearer ";
private static final int OAUTH_EXPIRE_BUFFER = 10;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final JsonParser JSON_PARSER = new JsonParser();
private static final Map<String, Bucket> BUCKET_MAP = new HashMap<>();
private static @Nullable String lastAccessToken = null;
public static ContentResponse sendRequest(Request request, String clientId)
throws InterruptedException, TimeoutException, ExecutionException {
if (HttpMethod.GET.name().equals(request.getMethod())) {
try {
getBucket(clientId).asScheduler().consume(1);
} catch (InterruptedException e) {
LoggerFactory.getLogger(HttpHelper.class).debug("Could not consume from bucket! clientId={}, error={}",
clientId, e.getMessage());
}
}
return request.send();
}
public static String formatJsonBody(@Nullable String jsonString) {
if (jsonString == null) {
return "";
}
try {
JsonObject json = parseString(jsonString).getAsJsonObject();
return GSON.toJson(json);
} catch (Exception e) {
return jsonString;
}
}
public static String getAuthorizationHeader(OAuthClientService oAuthClientService)
throws AuthorizationException, CommunicationException {
try {
AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
// refresh the token if it's about to expire
if (accessTokenResponse != null
&& accessTokenResponse.isExpired(LocalDateTime.now(), OAUTH_EXPIRE_BUFFER)) {
LoggerFactory.getLogger(HttpHelper.class).debug("Requesting a refresh of the access token.");
accessTokenResponse = oAuthClientService.refreshToken();
}
if (accessTokenResponse != null) {
String lastToken = lastAccessToken;
if (lastToken == null) {
LoggerFactory.getLogger(HttpHelper.class).debug("The used access token was created at {}",
accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} else if (!lastToken.equals(accessTokenResponse.getAccessToken())) {
LoggerFactory.getLogger(HttpHelper.class).debug("The access token changed. New one created at {}",
accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
lastAccessToken = accessTokenResponse.getAccessToken();
return BEARER + accessTokenResponse.getAccessToken();
} else {
LoggerFactory.getLogger(HttpHelper.class).error("No access token available! Fatal error.");
throw new AuthorizationException("No access token available!");
}
} catch (IOException e) {
String errorMessage = e.getMessage();
throw new CommunicationException(errorMessage != null ? errorMessage : "IOException", e);
} catch (OAuthException | OAuthResponseException e) {
String errorMessage = e.getMessage();
throw new AuthorizationException(errorMessage != null ? errorMessage : "oAuth exception", e);
}
}
public static JsonElement parseString(String json) {
return JSON_PARSER.parse(json);
}
private static synchronized Bucket getBucket(String clientId) {
Bucket bucket = null;
if (BUCKET_MAP.containsKey(clientId)) {
bucket = BUCKET_MAP.get(clientId);
}
if (bucket == null) {
bucket = Bucket4j.builder()
// allows 50 tokens per minute (added 10 second buffer)
.addLimit(classic(50, intervally(50, Duration.ofSeconds(70))).withInitialTokens(40))
// but not often then 50 tokens per second
.addLimit(classic(10, intervally(10, Duration.ofSeconds(1)))).build();
BUCKET_MAP.put(clientId, bucket);
}
return bucket;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.exception;
import static java.lang.String.format;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* API communication exception - appliance offline
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class ApplianceOfflineException extends Exception {
private static final long serialVersionUID = 1L;
public ApplianceOfflineException(int code, String message, String body) {
super(format("Communication error - appliance offline! response code: %d, message: %s, body: %s (Tried at %s)",
code, message, body, new Date()));
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* oAuth authorization exception
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class AuthorizationException extends Exception {
private static final long serialVersionUID = 1L;
public AuthorizationException(String message, Throwable cause) {
super(message, cause);
}
public AuthorizationException(String message) {
super(message);
}
public AuthorizationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.exception;
import static java.lang.String.format;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* API communication exception
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class CommunicationException extends Exception {
private static final long serialVersionUID = 1L;
public CommunicationException(String message, Throwable cause) {
super(message, cause);
}
public CommunicationException(String message) {
super(message);
}
public CommunicationException(Throwable cause) {
super(cause);
}
public CommunicationException(int code, String message, String body) {
super(format("Communication error! response code: %d, message: %s, body: %s (Tried at %s)", code, message, body,
new Date()));
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.model.Event;
/**
* {@link HomeConnectEventListener} inform about new events from Home Connect SSE interface.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public interface HomeConnectEventListener {
/**
* Inform listener about new event
*
* @param event appliance event listener
*/
void onEvent(Event event);
/**
* If SSE connection was closed
*/
default void onClosed() {
}
/**
* If SSE connection was closed due to rate limits
*/
default void onRateLimitReached() {
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
*
* API request model.
*
* @author Jonas Brüstel - Initial Contribution
*/
@NonNullByDefault
public class ApiRequest {
private final String id;
private final ZonedDateTime time;
private final HomeConnectRequest homeConnectRequest;
private final @Nullable HomeConnectResponse homeConnectResponse;
public ApiRequest(ZonedDateTime time, HomeConnectRequest homeConnectRequest,
@Nullable HomeConnectResponse homeConnectResponse) {
this.id = UUID.randomUUID().toString();
this.time = time;
this.homeConnectRequest = homeConnectRequest;
this.homeConnectResponse = homeConnectResponse;
}
public String getId() {
return id;
}
public ZonedDateTime getTime() {
return time;
}
public HomeConnectRequest getRequest() {
return homeConnectRequest;
}
public @Nullable HomeConnectResponse getResponse() {
return homeConnectResponse;
}
@Override
public String toString() {
return "ApiRequest [id=" + id + ", time=" + time + ", request=" + homeConnectRequest + ", response="
+ homeConnectResponse + "]";
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* AvailableProgram model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class AvailableProgram {
private final String key;
private final boolean available;
private final String execution;
public AvailableProgram(String key, boolean available, String execution) {
this.key = key;
this.available = available;
this.execution = execution;
}
public String getKey() {
return key;
}
public boolean isAvailable() {
return available;
}
public String getExecution() {
return execution;
}
@Override
public String toString() {
return "AvailableProgram [key=" + key + ", available=" + available + ", execution=" + execution + "]";
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* AvailableProgramOption model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class AvailableProgramOption {
private final String key;
private final List<String> allowedValues;
public AvailableProgramOption(String key, List<String> allowedValues) {
this.key = key;
this.allowedValues = allowedValues;
}
public String getKey() {
return key;
}
public List<String> getAllowedValues() {
return allowedValues;
}
@Override
public String toString() {
return "AvailableProgramOption [key=" + key + ", allowedValues=" + allowedValues + "]";
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import static java.lang.Boolean.parseBoolean;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Data model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class Data {
private final String name;
private final @Nullable String value;
private final @Nullable String unit;
public Data(String name, @Nullable String value, @Nullable String unit) {
this.name = name;
this.value = value;
this.unit = unit;
}
public String getName() {
return name;
}
public @Nullable String getValue() {
return value;
}
public @Nullable String getUnit() {
return unit;
}
public int getValueAsInt() {
String stringValue = value;
return stringValue != null ? Float.valueOf(stringValue).intValue() : 0;
}
public boolean getValueAsBoolean() {
return parseBoolean(value);
}
@Override
public String toString() {
return "Data [name=" + name + ", value=" + value + ", unit=" + unit + "]";
}
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import static org.openhab.binding.homeconnect.internal.client.model.EventType.EVENT;
import static org.openhab.binding.homeconnect.internal.client.model.EventType.NOTIFY;
import static org.openhab.binding.homeconnect.internal.client.model.EventType.STATUS;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Event model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class Event {
private final String haId;
// event type
private final EventType type;
// event key
private @Nullable final String key;
// user-friendly name of the feature key
private @Nullable final String name;
// URI of the resource that changed
private @Nullable final String uri;
// creation time of event
private @Nullable final ZonedDateTime creation;
// level of the event
private @Nullable final EventLevel level;
// expected activity
private @Nullable final EventHandling handling;
// new value, e.g. in case of a status update (string, number or boolean)
private @Nullable final String value;
// unit string
private @Nullable final String unit;
public Event(final String haId, final EventType type) {
this.haId = haId;
this.type = type;
this.key = null;
this.name = null;
this.uri = null;
this.creation = ZonedDateTime.now();
this.level = null;
this.handling = null;
this.value = null;
this.unit = null;
}
public Event(final String haId, final EventType type, @Nullable final String key, @Nullable final String name,
@Nullable final String uri, @Nullable final ZonedDateTime creation, @Nullable final EventLevel level,
@Nullable final EventHandling handling, @Nullable final String value, @Nullable final String unit) {
this.haId = haId;
this.type = type;
this.key = key;
this.name = name;
this.uri = uri;
this.creation = creation;
this.level = level;
this.handling = handling;
this.value = value;
this.unit = unit;
}
public String getHaId() {
return haId;
}
public EventType getType() {
return type;
}
public @Nullable String getKey() {
return key;
}
public @Nullable String getName() {
return name;
}
public @Nullable String getUri() {
return uri;
}
public @Nullable ZonedDateTime getCreation() {
return creation;
}
public @Nullable EventLevel getLevel() {
return level;
}
public @Nullable EventHandling getHandling() {
return handling;
}
public @Nullable String getValue() {
return value;
}
public boolean getValueAsBoolean() {
return Boolean.parseBoolean(value);
}
public int getValueAsInt() {
String stringValue = value;
return stringValue != null ? Float.valueOf(stringValue).intValue() : 0;
}
public @Nullable String getUnit() {
return unit;
}
@Override
public String toString() {
if (STATUS.equals(type) || EVENT.equals(type) || NOTIFY.equals(type)) {
return "Event{" + "haId='" + haId + '\'' + ", type=" + type + ", key='" + key + '\'' + ", name='" + name
+ '\'' + ", uri='" + uri + '\'' + ", creation=" + creation + ", level=" + level + ", handling="
+ handling + ", value='" + value + '\'' + ", unit='" + unit + '\'' + '}';
} else {
return "Event{" + "haId='" + haId + '\'' + ", type=" + type + '}';
}
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Event handling model.
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public enum EventHandling {
NONE("none"),
ACKNOWLEDGE("acknowledge"),
DECISION("decision");
private final String handling;
EventHandling(String handling) {
this.handling = handling;
}
public String getHandling() {
return this.handling;
}
public static @Nullable EventHandling valueOfHandling(String type) {
for (EventHandling eventType : EventHandling.values()) {
if (eventType.handling.equalsIgnoreCase(type)) {
return eventType;
}
}
return null;
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Event level model.
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public enum EventLevel {
CRITICAL("critical"),
ALERT("alert"),
WARNING("warning"),
HINT("hint"),
INFO("info");
private final String level;
EventLevel(String level) {
this.level = level;
}
public String getLevel() {
return this.level;
}
public static @Nullable EventLevel valueOfLevel(String type) {
for (EventLevel eventType : EventLevel.values()) {
if (eventType.level.equalsIgnoreCase(type)) {
return eventType;
}
}
return null;
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Event type model.
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public enum EventType {
KEEP_ALIVE("KEEP-ALIVE"),
STATUS("STATUS"),
EVENT("EVENT"),
NOTIFY("NOTIFY"),
DISCONNECTED("DISCONNECTED"),
CONNECTED("CONNECTED"),
PAIRED("PAIRED"),
DEPAIRED("DEPAIRED");
private final String type;
EventType(String type) {
this.type = type;
}
public String getType() {
return this.type;
}
public static @Nullable EventType valueOfType(@Nullable String type) {
for (EventType eventType : EventType.values()) {
if (eventType.type.equalsIgnoreCase(type)) {
return eventType;
}
}
return null;
}
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Home appliance model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class HomeAppliance {
private final String name;
private final String brand;
private final String vib;
private final boolean connected;
private final String type;
private final String enumber;
private final String haId;
public HomeAppliance(String haId, String name, String brand, String vib, boolean connected, String type,
String enumber) {
this.haId = haId;
this.name = name;
this.brand = brand;
this.vib = vib;
this.connected = connected;
this.type = type;
this.enumber = enumber;
}
public String getName() {
return name;
}
public String getBrand() {
return brand;
}
public String getVib() {
return vib;
}
public boolean isConnected() {
return connected;
}
public String getType() {
return type;
}
public String getEnumber() {
return enumber;
}
public String getHaId() {
return haId;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + haId.hashCode();
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
HomeAppliance other = (HomeAppliance) obj;
return haId.equals(other.haId);
}
@Override
public String toString() {
return "HomeAppliance [haId=" + haId + ", name=" + name + ", brand=" + brand + ", vib=" + vib + ", connected="
+ connected + ", type=" + type + ", enumber=" + enumber + "]";
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.API_BASE_URL;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.API_SIMULATOR_BASE_URL;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
*
* HTTP request model.
*
* @author Jonas Brüstel - Initial Contribution
*/
@NonNullByDefault
public class HomeConnectRequest {
private final String url;
private final String method;
private final Map<String, String> header;
private @Nullable final String body;
public HomeConnectRequest(String url, String method, Map<String, String> header, @Nullable String body) {
this.url = url;
this.method = method;
this.header = header;
this.body = body;
}
public String getUrl() {
return url;
}
public String getShortUrl() {
return url.replace(API_BASE_URL, "").replace(API_SIMULATOR_BASE_URL, "");
}
public String getMethod() {
return method;
}
public Map<String, String> getHeader() {
return header;
}
public @Nullable String getBody() {
return body;
}
@Override
public String toString() {
return "Request [url=" + url + ", method=" + method + ", header=" + header + ", body=" + body + "]";
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
*
* HTTP response model.
*
* @author Jonas Brüstel - Initial Contribution
*/
@NonNullByDefault
public class HomeConnectResponse {
private final int code;
private final Map<String, String> header;
private final @Nullable String body;
public HomeConnectResponse(int code, Map<String, String> header, @Nullable String body) {
this.code = code;
this.header = header;
this.body = body;
}
public int getCode() {
return code;
}
public Map<String, String> getHeader() {
return header;
}
public @Nullable String getBody() {
return body;
}
@Override
public String toString() {
return "Response [code=" + code + ", header=" + header + ", body=" + body + "]";
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Option model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class Option {
private final @Nullable String key;
private final @Nullable String value;
private final @Nullable String unit;
public Option(@Nullable String key, @Nullable String value, @Nullable String unit) {
this.key = key;
this.value = value;
this.unit = unit;
}
public @Nullable String getKey() {
return key;
}
public @Nullable String getValue() {
return value;
}
public boolean getValueAsBoolean() {
return Boolean.parseBoolean(value);
}
public int getValueAsInt() {
@Nullable
String stringValue = value;
return stringValue != null ? Integer.parseInt(stringValue) : 0;
}
public @Nullable String getUnit() {
return unit;
}
@Override
public String toString() {
return "Option [key=" + key + ", value=" + value + ", unit=" + unit + "]";
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Program model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class Program {
private final String key;
private final List<Option> options;
public Program(String key, List<Option> options) {
this.key = key;
this.options = options;
}
public String getKey() {
return key;
}
public List<Option> getOptions() {
return options;
}
@Override
public String toString() {
return "Program [key=" + key + ", options=" + options + "]";
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.client.model;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Token model
*
* @author Jonas Brüstel - Initial contribution
*
*/
@NonNullByDefault
public class Token {
private final String accessToken;
private final String refreshToken;
private final long accessTokenExpirationInSeconds;
public Token(String accessToken, String refreshToken, long accessTokenExpirationInSeconds) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.accessTokenExpirationInSeconds = accessTokenExpirationInSeconds;
}
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public long getAccessTokenExpiration() {
return accessTokenExpirationInSeconds;
}
@Override
public String toString() {
return "Token [accessToken=" + accessToken + ", refreshToken=" + refreshToken + ", accessTokenExpiration="
+ new Date(accessTokenExpirationInSeconds) + "]";
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ApiBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class ApiBridgeConfiguration {
private String clientId = "";
private String clientSecret = "";
private boolean simulator;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public boolean isSimulator() {
return simulator;
}
public void setSimulator(boolean simulator) {
this.simulator = simulator;
}
@Override
public String toString() {
return "ApiBridgeConfiguration [clientId=" + clientId + ", clientSecret=" + clientSecret + ", simulator="
+ simulator + "]";
}
}

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.discovery;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectDiscoveryService} is responsible for discovering new devices.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService {
private static final int SEARCH_TIME_SEC = 20;
private final Logger logger = LoggerFactory.getLogger(HomeConnectDiscoveryService.class);
private @Nullable HomeConnectBridgeHandler bridgeHandler;
/**
* Construct an {@link HomeConnectDiscoveryService}.
*
*/
public HomeConnectDiscoveryService() {
super(DISCOVERABLE_DEVICE_THING_TYPES_UIDS, SEARCH_TIME_SEC, true);
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof HomeConnectBridgeHandler) {
this.bridgeHandler = (HomeConnectBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
protected void startScan() {
logger.debug("Starting device scan.");
var bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
HomeConnectApiClient apiClient = bridgeHandler.getApiClient();
try {
List<HomeAppliance> appliances = apiClient.getHomeAppliances();
logger.debug("Scan found {} devices.", appliances.size());
// add found devices
for (HomeAppliance appliance : appliances) {
@Nullable
ThingTypeUID thingTypeUID = getThingTypeUID(appliance);
if (thingTypeUID != null) {
logger.debug("Found {} ({}).", appliance.getHaId(), appliance.getType().toUpperCase());
Map<String, Object> properties = Map.of(HA_ID, appliance.getHaId());
String name = appliance.getBrand() + " " + appliance.getName() + " (" + appliance.getHaId()
+ ")";
DiscoveryResult discoveryResult = DiscoveryResultBuilder
.create(new ThingUID(BINDING_ID, appliance.getType(),
bridgeHandler.getThing().getUID().getId(), appliance.getHaId()))
.withThingType(thingTypeUID).withProperties(properties)
.withRepresentationProperty(HA_ID).withBridge(bridgeHandler.getThing().getUID())
.withLabel(name).build();
thingDiscovered(discoveryResult);
} else {
logger.debug("Ignoring unsupported device {} of type {}.", appliance.getHaId(),
appliance.getType());
}
}
} catch (CommunicationException | AuthorizationException e) {
logger.debug("Exception during scan.", e);
}
}
logger.debug("Finished device scan.");
}
@Override
public void deactivate() {
super.deactivate();
var bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
removeOlderResults(System.currentTimeMillis(), bridgeHandler.getThing().getUID());
}
}
@Override
protected synchronized void stopScan() {
super.stopScan();
var bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
removeOlderResults(getTimestampOfLastScan(), bridgeHandler.getThing().getUID());
}
}
private @Nullable ThingTypeUID getThingTypeUID(HomeAppliance appliance) {
@Nullable
ThingTypeUID thingTypeUID = null;
if (THING_TYPE_DISHWASHER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_DISHWASHER;
} else if (THING_TYPE_OVEN.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_OVEN;
} else if (THING_TYPE_FRIDGE_FREEZER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_FRIDGE_FREEZER;
} else if (THING_TYPE_DRYER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_DRYER;
} else if (THING_TYPE_COFFEE_MAKER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_COFFEE_MAKER;
} else if (THING_TYPE_HOOD.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_HOOD;
} else if (THING_TYPE_WASHER_DRYER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_WASHER_DRYER;
} else if (THING_TYPE_COOKTOP.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_COOKTOP;
} else if (THING_TYPE_WASHER.getId().equalsIgnoreCase(appliance.getType())) {
thingTypeUID = THING_TYPE_WASHER;
}
return thingTypeUID;
}
}

View File

@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.factory;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectBridgeHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectCoffeeMakerHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectCooktopHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectDishwasherHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectDryerHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectFridgeFreezerHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectHoodHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectOvenHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectWasherDryerHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectWasherHandler;
import org.openhab.binding.homeconnect.internal.servlet.HomeConnectServlet;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link HomeConnectHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.homeconnect", service = ThingHandlerFactory.class)
public class HomeConnectHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final OAuthFactory oAuthFactory;
private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
private final HomeConnectServlet homeConnectServlet;
@Activate
public HomeConnectHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference ClientBuilder clientBuilder, @Reference SseEventSourceFactory eventSourceFactory,
@Reference OAuthFactory oAuthFactory,
@Reference HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider,
@Reference HomeConnectServlet homeConnectServlet) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.oAuthFactory = oAuthFactory;
this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
this.homeConnectServlet = homeConnectServlet;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_API_BRIDGE.equals(thingTypeUID)) {
return new HomeConnectBridgeHandler((Bridge) thing, httpClient, clientBuilder, eventSourceFactory,
oAuthFactory, homeConnectServlet);
} else if (THING_TYPE_DISHWASHER.equals(thingTypeUID)) {
return new HomeConnectDishwasherHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_OVEN.equals(thingTypeUID)) {
return new HomeConnectOvenHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_WASHER.equals(thingTypeUID)) {
return new HomeConnectWasherHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_WASHER_DRYER.equals(thingTypeUID)) {
return new HomeConnectWasherDryerHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_DRYER.equals(thingTypeUID)) {
return new HomeConnectDryerHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_FRIDGE_FREEZER.equals(thingTypeUID)) {
return new HomeConnectFridgeFreezerHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_COFFEE_MAKER.equals(thingTypeUID)) {
return new HomeConnectCoffeeMakerHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_HOOD.equals(thingTypeUID)) {
return new HomeConnectHoodHandler(thing, dynamicStateDescriptionProvider);
} else if (THING_TYPE_COOKTOP.equals(thingTypeUID)) {
return new HomeConnectCooktopHandler(thing, dynamicStateDescriptionProvider);
}
return null;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
import org.openhab.core.thing.ChannelUID;
/**
* The {@link ChannelUpdateHandler} is responsible for updating channels.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public interface ChannelUpdateHandler {
void handle(ChannelUID channelUID, ExpiringStateMap cache)
throws CommunicationException, ApplianceOfflineException, AuthorizationException;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.model.Event;
/**
* The {@link EventHandler} is responsible for handling events, which were sent via Server-Sent event interface.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public interface EventHandler {
void handle(Event event);
}

View File

@ -0,0 +1,314 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
import org.openhab.binding.homeconnect.internal.client.model.Event;
import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
import org.openhab.binding.homeconnect.internal.discovery.HomeConnectDiscoveryService;
import org.openhab.binding.homeconnect.internal.servlet.HomeConnectServlet;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectBridgeHandler extends BaseBridgeHandler {
private static final int REINITIALIZATION_DELAY_SEC = 120;
private static final String CLIENT_SECRET = "clientSecret";
private static final String CLIENT_ID = "clientId";
private final HttpClient httpClient;
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final OAuthFactory oAuthFactory;
private final HomeConnectServlet homeConnectServlet;
private final Logger logger = LoggerFactory.getLogger(HomeConnectBridgeHandler.class);
private @Nullable ScheduledFuture<?> reinitializationFuture;
private @Nullable List<ApiRequest> apiRequestHistory;
private @Nullable List<Event> eventHistory;
private @NonNullByDefault({}) OAuthClientService oAuthClientService;
private @NonNullByDefault({}) String oAuthServiceHandleId;
private @NonNullByDefault({}) HomeConnectApiClient apiClient;
private @NonNullByDefault({}) HomeConnectEventSourceClient eventSourceClient;
public HomeConnectBridgeHandler(Bridge bridge, HttpClient httpClient, ClientBuilder clientBuilder,
SseEventSourceFactory eventSourceFactory, OAuthFactory oAuthFactory,
HomeConnectServlet homeConnectServlet) {
super(bridge);
this.httpClient = httpClient;
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.oAuthFactory = oAuthFactory;
this.homeConnectServlet = homeConnectServlet;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// not used for bridge
}
@Override
public void initialize() {
// let the bridge configuration servlet know about this handler
homeConnectServlet.addBridgeHandler(this);
// create oAuth service
ApiBridgeConfiguration config = getConfiguration();
String tokenUrl = (config.isSimulator() ? API_SIMULATOR_BASE_URL : API_BASE_URL) + OAUTH_TOKEN_PATH;
String authorizeUrl = (config.isSimulator() ? API_SIMULATOR_BASE_URL : API_BASE_URL) + OAUTH_AUTHORIZE_PATH;
String oAuthServiceHandleId = thing.getUID().getAsString() + (config.isSimulator() ? "simulator" : "");
oAuthClientService = oAuthFactory.createOAuthClientService(oAuthServiceHandleId, tokenUrl, authorizeUrl,
config.getClientId(), config.getClientSecret(), OAUTH_SCOPE, true);
this.oAuthServiceHandleId = oAuthServiceHandleId;
logger.debug(
"Initialize oAuth client service. tokenUrl={}, authorizeUrl={}, oAuthServiceHandleId={}, scope={}, oAuthClientService={}",
tokenUrl, authorizeUrl, oAuthServiceHandleId, OAUTH_SCOPE, oAuthClientService);
// create api client
apiClient = new HomeConnectApiClient(httpClient, oAuthClientService, config.isSimulator(), apiRequestHistory,
config);
eventSourceClient = new HomeConnectEventSourceClient(clientBuilder, eventSourceFactory, oAuthClientService,
config.isSimulator(), scheduler, eventHistory);
updateStatus(ThingStatus.UNKNOWN);
scheduler.submit(() -> {
try {
@Nullable
AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
if (accessTokenResponse == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"Please authenticate your account at http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect (e.g. http://192.168.178.100:8080/homeconnect).");
} else {
apiClient.getHomeAppliances();
updateStatus(ThingStatus.ONLINE);
}
} catch (OAuthException | IOException | OAuthResponseException | CommunicationException
| AuthorizationException e) {
ZonedDateTime nextReinitializeDateTime = ZonedDateTime.now().plusSeconds(REINITIALIZATION_DELAY_SEC);
String offlineMessage = String.format(
"Home Connect service is not reachable or a problem occurred! Retrying at %s (%s). bridge=%s",
nextReinitializeDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME), e.getMessage(),
getThing().getLabel());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
scheduleReinitialize();
}
});
}
@Override
public void dispose() {
stopReinitializer();
cleanup(true);
}
public void reinitialize() {
logger.debug("Reinitialize bridge {}", getThing().getLabel());
stopReinitializer();
cleanup(false);
initialize();
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
if (isModifyingCurrentConfig(configurationParameters)) {
List<String> parameters = configurationParameters.entrySet().stream().map((entry) -> {
if (CLIENT_ID.equals(entry.getKey()) || CLIENT_SECRET.equals(entry.getKey())) {
return entry.getKey() + ": ***";
}
return entry.getKey() + ": " + entry.getValue();
}).collect(Collectors.toList());
logger.debug("Update bridge configuration. bridge={}, parameters={}", getThing().getLabel(), parameters);
validateConfigurationParameters(configurationParameters);
Configuration configuration = editConfiguration();
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
// invalidate oAuth credentials
try {
logger.debug("Clear oAuth credential store. bridge={}", getThing().getLabel());
var oAuthClientService = this.oAuthClientService;
if (oAuthClientService != null) {
oAuthClientService.remove();
}
} catch (OAuthException e) {
logger.error("Could not clear oAuth credentials. bridge={}", getThing().getLabel(), e);
}
if (isInitialized()) {
// persist new configuration and reinitialize handler
dispose();
updateConfiguration(configuration);
initialize();
} else {
// persist new configuration and notify Thing Manager
updateConfiguration(configuration);
@Nullable
ThingHandlerCallback callback = getCallback();
if (callback != null) {
callback.configurationUpdated(this.getThing());
} else {
logger.warn(
"Handler {} tried updating its configuration although the handler was already disposed.",
this.getClass().getSimpleName());
}
}
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(HomeConnectDiscoveryService.class);
}
/**
* Get {@link HomeConnectApiClient}.
*
* @return api client instance
*/
public HomeConnectApiClient getApiClient() {
return apiClient;
}
/**
* Get {@link HomeConnectEventSourceClient}.
*
* @return event source client instance
*/
public HomeConnectEventSourceClient getEventSourceClient() {
return eventSourceClient;
}
/**
* Get children of bridge
*
* @return list of child handlers
*/
public List<AbstractHomeConnectThingHandler> getThingHandler() {
return getThing().getThings().stream()
.filter(thing -> thing.getHandler() instanceof AbstractHomeConnectThingHandler)
.map(thing -> (AbstractHomeConnectThingHandler) thing.getHandler()).collect(Collectors.toList());
}
/**
* Get {@link ApiBridgeConfiguration}.
*
* @return bridge configuration (clientId, clientSecret, etc.)
*/
public ApiBridgeConfiguration getConfiguration() {
return getConfigAs(ApiBridgeConfiguration.class);
}
/**
* Get {@link OAuthClientService} instance.
*
* @return oAuth client service instance
*/
public OAuthClientService getOAuthClientService() {
return oAuthClientService;
}
private void cleanup(boolean immediate) {
ArrayList<ApiRequest> apiRequestHistory = new ArrayList<>();
apiRequestHistory.addAll(apiClient.getLatestApiRequests());
this.apiRequestHistory = apiRequestHistory;
apiClient.getLatestApiRequests().clear();
ArrayList<Event> eventHistory = new ArrayList<>();
eventHistory.addAll(eventSourceClient.getLatestEvents());
this.eventHistory = eventHistory;
eventSourceClient.getLatestEvents().clear();
eventSourceClient.dispose(immediate);
oAuthFactory.ungetOAuthService(oAuthServiceHandleId);
homeConnectServlet.removeBridgeHandler(this);
}
private synchronized void scheduleReinitialize() {
@Nullable
ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture;
if (reinitializationFuture != null && !reinitializationFuture.isDone()) {
logger.debug("Reinitialization is already scheduled. Starting in {} seconds. bridge={}",
reinitializationFuture.getDelay(TimeUnit.SECONDS), getThing().getLabel());
} else {
this.reinitializationFuture = scheduler.schedule(() -> {
cleanup(false);
initialize();
}, HomeConnectBridgeHandler.REINITIALIZATION_DELAY_SEC, TimeUnit.SECONDS);
}
}
private synchronized void stopReinitializer() {
@Nullable
ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture;
if (reinitializationFuture != null) {
reinitializationFuture.cancel(true);
this.reinitializationFuture = null;
}
}
}

View File

@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
/**
* The {@link HomeConnectCoffeeMakerHandler} is responsible for handling commands, which are
* sent to one of the channels of a coffee machine.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectCoffeeMakerHandler extends AbstractHomeConnectThingHandler {
public HomeConnectCoffeeMakerHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE, defaultSelectedProgramStateUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default SSE event handlers
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_SELECTED_PROGRAM, defaultSelectedProgramStateEventHandler());
handlers.put(EVENT_COFFEEMAKER_BEAN_CONTAINER_EMPTY,
defaultEventPresentStateEventHandler(CHANNEL_COFFEEMAKER_BEAN_CONTAINER_EMPTY_STATE));
handlers.put(EVENT_COFFEEMAKER_DRIP_TRAY_FULL,
defaultEventPresentStateEventHandler(CHANNEL_COFFEEMAKER_DRIP_TRAY_FULL_STATE));
handlers.put(EVENT_COFFEEMAKER_WATER_TANK_EMPTY,
defaultEventPresentStateEventHandler(CHANNEL_COFFEEMAKER_WATER_TANK_EMPTY_STATE));
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
// register coffee maker specific SSE event handlers
handlers.put(EVENT_PROGRAM_PROGRESS, event -> {
if (event.getValue() == null || event.getValueAsInt() == 0) {
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
.ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
} else {
defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE).handle(event);
}
});
}
@Override
protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
// turn coffee maker on and standby
if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
apiClient.setPowerState(getThingHaId(),
OnOffType.ON.equals(command) ? STATE_POWER_ON : STATE_POWER_STANDBY);
}
}
@Override
public String toString() {
return "HomeConnectCoffeeMakerHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.UnDefType;
/**
* The {@link HomeConnectCooktopHandler} is responsible for handling commands, which are
* sent to one of the channels of a hood.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectCooktopHandler extends AbstractHomeConnectThingHandler {
public HomeConnectCooktopHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE, defaultSelectedProgramStateUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default SSE event handlers
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_SELECTED_PROGRAM, defaultSelectedProgramStateEventHandler());
handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
// specific SSE event handlers
handlers.put(EVENT_ACTIVE_PROGRAM, (event) -> {
defaultActiveProgramEventHandler().handle(event);
if (event.getValue() != null) {
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent((c) -> updateChannel(c.getUID()));
}
});
}
@Override
public String toString() {
return "HomeConnectCooktopHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
/**
* The {@link HomeConnectDishwasherHandler} is responsible for handling commands, which are
* sent to one of the channels of a dishwasher.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectDishwasherHandler extends AbstractHomeConnectThingHandler {
public HomeConnectDishwasherHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE, defaultSelectedProgramStateUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
handlers.put(CHANNEL_AMBIENT_LIGHT_STATE, defaultAmbientLightChannelUpdateHandler());
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMAINING_PROGRAM_TIME, defaultRemainingProgramTimeEventHandler());
handlers.put(EVENT_PROGRAM_PROGRESS, defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE));
handlers.put(EVENT_SELECTED_PROGRAM, defaultSelectedProgramStateEventHandler());
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_AMBIENT_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_AMBIENT_LIGHT_STATE));
handlers.put(EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE,
defaultPercentHandler(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE));
handlers.put(EVENT_AMBIENT_LIGHT_COLOR_STATE, defaultAmbientLightColorStateEventHandler());
handlers.put(EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE, defaultAmbientLightCustomColorStateEventHandler());
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
if (command instanceof OnOffType) {
if (CHANNEL_POWER_STATE.equals(channelUID.getId())) {
apiClient.setPowerState(getThingHaId(),
OnOffType.ON.equals(command) ? STATE_POWER_ON : STATE_POWER_OFF);
}
}
handleLightCommands(channelUID, command, apiClient);
}
@Override
public String toString() {
return "HomeConnectDishwasherHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectDryerHandler} is responsible for handling commands, which are
* sent to one of the channels of a dryer.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectDryerHandler extends AbstractHomeConnectThingHandler {
private static final List<String> INACTIVE_STATE = List.of(OPERATION_STATE_INACTIVE, OPERATION_STATE_READY);
private final Logger logger = LoggerFactory.getLogger(HomeConnectDryerHandler.class);
public HomeConnectDryerHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE,
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler());
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMAINING_PROGRAM_TIME, defaultRemainingProgramTimeEventHandler());
handlers.put(EVENT_PROGRAM_PROGRESS, defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_SELECTED_PROGRAM, updateProgramOptionsAndSelectedProgramStateEventHandler());
// register dryer specific event handlers
handlers.put(EVENT_DRYER_DRYING_TARGET,
event -> getThingChannel(CHANNEL_DRYER_DRYING_TARGET).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
String operationState = getOperationState();
// only handle these commands if operation state allows it
if (operationState != null && INACTIVE_STATE.contains(operationState)) {
// set drying target option
if (command instanceof StringType && CHANNEL_DRYER_DRYING_TARGET.equals(channelUID.getId())) {
apiClient.setProgramOptions(getThingHaId(), OPTION_DRYER_DRYING_TARGET, command.toFullString(), null,
false, false);
}
} else {
logger.debug("Device can not handle command {} in current operation state ({}). thing={}, haId={}", command,
operationState, getThingLabel(), getThingHaId());
}
}
@Override
public String toString() {
return "HomeConnectDryerHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,178 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.CHANNEL_DOOR_STATE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.CHANNEL_FREEZER_SETPOINT_TEMPERATURE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.CHANNEL_FREEZER_SUPER_MODE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.CHANNEL_REFRIGERATOR_SUPER_MODE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.EVENT_DOOR_STATE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.EVENT_FREEZER_SETPOINT_TEMPERATURE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.EVENT_FREEZER_SUPER_MODE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.EVENT_FRIDGE_SETPOINT_TEMPERATURE;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.EVENT_FRIDGE_SUPER_MODE;
import java.util.Map;
import java.util.Optional;
import javax.measure.UnconvertibleException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.Data;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
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.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectFridgeFreezerHandler} is responsible for handling commands, which are
* sent to one of the channels of a fridge/freezer.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectFridgeFreezerHandler extends AbstractHomeConnectThingHandler {
private final Logger logger = LoggerFactory.getLogger(HomeConnectFridgeFreezerHandler.class);
public HomeConnectFridgeFreezerHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
// register fridge/freezer specific handlers
handlers.put(CHANNEL_FREEZER_SETPOINT_TEMPERATURE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getFreezerSetpointTemperature(getThingHaId());
if (data.getValue() != null) {
return new QuantityType<>(data.getValueAsInt(), mapTemperature(data.getUnit()));
} else {
return UnDefType.UNDEF;
}
}
return UnDefType.UNDEF;
})));
handlers.put(CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getFridgeSetpointTemperature(getThingHaId());
if (data.getValue() != null) {
return new QuantityType<>(data.getValueAsInt(), mapTemperature(data.getUnit()));
} else {
return UnDefType.UNDEF;
}
}
return UnDefType.UNDEF;
})));
handlers.put(CHANNEL_REFRIGERATOR_SUPER_MODE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getFridgeSuperMode(getThingHaId());
if (data.getValue() != null) {
return OnOffType.from(data.getValueAsBoolean());
} else {
return UnDefType.UNDEF;
}
}
return UnDefType.UNDEF;
})));
handlers.put(CHANNEL_FREEZER_SUPER_MODE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getFreezerSuperMode(getThingHaId());
if (data.getValue() != null) {
return OnOffType.from(data.getValueAsBoolean());
} else {
return UnDefType.UNDEF;
}
}
return UnDefType.UNDEF;
})));
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_FREEZER_SUPER_MODE, defaultBooleanEventHandler(CHANNEL_FREEZER_SUPER_MODE));
handlers.put(EVENT_FRIDGE_SUPER_MODE, defaultBooleanEventHandler(CHANNEL_REFRIGERATOR_SUPER_MODE));
// register fridge/freezer specific event handlers
handlers.put(EVENT_FREEZER_SETPOINT_TEMPERATURE,
event -> getThingChannel(CHANNEL_FREEZER_SETPOINT_TEMPERATURE)
.ifPresent(channel -> updateState(channel.getUID(),
new QuantityType<>(event.getValueAsInt(), mapTemperature(event.getUnit())))));
handlers.put(EVENT_FRIDGE_SETPOINT_TEMPERATURE,
event -> getThingChannel(CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE)
.ifPresent(channel -> updateState(channel.getUID(),
new QuantityType<>(event.getValueAsInt(), mapTemperature(event.getUnit())))));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
try {
if (CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE.equals(channelUID.getId())
|| CHANNEL_FREEZER_SETPOINT_TEMPERATURE.equals(channelUID.getId())) {
handleTemperatureCommand(channelUID, command, apiClient);
} else if (command instanceof OnOffType) {
if (CHANNEL_FREEZER_SUPER_MODE.equals(channelUID.getId())) {
apiClient.setFreezerSuperMode(getThingHaId(), OnOffType.ON.equals(command));
} else if (CHANNEL_REFRIGERATOR_SUPER_MODE.equals(channelUID.getId())) {
apiClient.setFridgeSuperMode(getThingHaId(), OnOffType.ON.equals(command));
}
}
} catch (UnconvertibleException e) {
logger.debug("Could not set setpoint! haId={}, error={}", getThingHaId(), e.getMessage());
}
}
@Override
protected void updateSelectedProgramStateDescription() {
// not used
}
@Override
protected void removeSelectedProgramStateDescription() {
// not used
}
@Override
public String toString() {
return "HomeConnectFridgeFreezerHandler [haId: " + getThingHaId() + "]";
}
}

View File

@ -0,0 +1,301 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.Data;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectHoodHandler} is responsible for handling commands, which are
* sent to one of the channels of a hood.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectHoodHandler extends AbstractHomeConnectThingHandler {
private static final String START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE = "\n" + "{\n" + " \"data\": {\n"
+ " \"key\": \"Cooking.Common.Program.Hood.Venting\",\n" + " \"options\": [\n"
+ " {\n" + " \"key\": \"Cooking.Common.Option.Hood.IntensiveLevel\",\n"
+ " \"value\": \"%s\"\n" + " }\n" + " ]\n" + " }\n" + "}";
private static final String START_VENTING_STAGE_PAYLOAD_TEMPLATE = "\n" + "{\n" + " \"data\": {\n"
+ " \"key\": \"Cooking.Common.Program.Hood.Venting\",\n" + " \"options\": [\n"
+ " {\n" + " \"key\": \"Cooking.Common.Option.Hood.VentingLevel\",\n"
+ " \"value\": \"%s\"\n" + " }\n" + " ]\n" + " }\n" + "}";
private final Logger logger = LoggerFactory.getLogger(HomeConnectHoodHandler.class);
public HomeConnectHoodHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
handlers.put(CHANNEL_AMBIENT_LIGHT_STATE, defaultAmbientLightChannelUpdateHandler());
handlers.put(CHANNEL_FUNCTIONAL_LIGHT_STATE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getFunctionalLightState(getThingHaId());
if (data.getValue() != null) {
boolean enabled = data.getValueAsBoolean();
if (enabled) {
Data brightnessData = apiClient.get().getFunctionalLightBrightnessState(getThingHaId());
getThingChannel(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE)
.ifPresent(channel -> updateState(channel.getUID(),
new PercentType(brightnessData.getValueAsInt())));
}
return OnOffType.from(enabled);
} else {
return UnDefType.UNDEF;
}
} else {
return UnDefType.UNDEF;
}
})));
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default SSE event handlers
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
handlers.put(EVENT_FUNCTIONAL_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_FUNCTIONAL_LIGHT_STATE));
handlers.put(EVENT_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE,
defaultPercentHandler(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE));
handlers.put(EVENT_AMBIENT_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_AMBIENT_LIGHT_STATE));
handlers.put(EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE,
defaultPercentHandler(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE));
handlers.put(EVENT_AMBIENT_LIGHT_COLOR_STATE, defaultAmbientLightColorStateEventHandler());
handlers.put(EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE, defaultAmbientLightCustomColorStateEventHandler());
// register hood specific SSE event handlers
handlers.put(EVENT_HOOD_INTENSIVE_LEVEL,
event -> getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(channel -> {
String hoodIntensiveLevel = event.getValue();
if (hoodIntensiveLevel != null) {
updateState(channel.getUID(), new StringType(mapStageStringType(hoodIntensiveLevel)));
} else {
updateState(channel.getUID(), UnDefType.UNDEF);
}
}));
handlers.put(EVENT_HOOD_VENTING_LEVEL,
event -> getThingChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(channel -> {
String hoodVentingLevel = event.getValue();
if (hoodVentingLevel != null) {
updateState(channel.getUID(), new StringType(mapStageStringType(hoodVentingLevel)));
} else {
updateState(channel.getUID(), UnDefType.UNDEF);
}
}));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
if (command instanceof OnOffType) {
if (CHANNEL_POWER_STATE.equals(channelUID.getId())) {
apiClient.setPowerState(getThingHaId(),
OnOffType.ON.equals(command) ? STATE_POWER_ON : STATE_POWER_OFF);
}
}
// light commands
handleLightCommands(channelUID, command, apiClient);
// program options
if (command instanceof StringType && CHANNEL_HOOD_ACTIONS_STATE.equals(channelUID.getId())) {
String operationState = getOperationState();
if (OPERATION_STATE_INACTIVE.equals(operationState) || OPERATION_STATE_RUN.equals(operationState)) {
if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
apiClient.stopProgram(getThingHaId());
}
} else {
logger.debug("Device can not handle command {} in current operation state ({}). thing={}, haId={}",
command, operationState, getThingLabel(), getThingHaId());
}
// These command always start the hood - even if appliance is turned off
if (COMMAND_AUTOMATIC.equalsIgnoreCase(command.toFullString())) {
apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_AUTOMATIC);
} else if (COMMAND_DELAYED_SHUT_OFF.equalsIgnoreCase(command.toFullString())) {
apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_DELAYED_SHUT_OFF);
} else if (COMMAND_VENTING_1.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_01));
} else if (COMMAND_VENTING_2.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_02));
} else if (COMMAND_VENTING_3.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_03));
} else if (COMMAND_VENTING_4.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_04));
} else if (COMMAND_VENTING_5.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_05));
} else if (COMMAND_VENTING_INTENSIVE_1.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_1));
} else if (COMMAND_VENTING_INTENSIVE_2.equalsIgnoreCase(command.toFullString())) {
apiClient.startCustomProgram(getThingHaId(),
format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_2));
} else {
logger.info("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
apiClient.startCustomProgram(getThingHaId(), command.toFullString());
}
}
}
@Override
protected void updateSelectedProgramStateDescription() {
// update hood program actions
if (isBridgeOffline() || !isThingAccessibleViaServerSentEvents()) {
return;
}
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
try {
ArrayList<StateOption> stateOptions = new ArrayList<>();
apiClient.get().getPrograms(getThingHaId()).forEach(availableProgram -> {
if (PROGRAM_HOOD_AUTOMATIC.equals(availableProgram.getKey())) {
stateOptions.add(new StateOption(COMMAND_AUTOMATIC, mapStringType(availableProgram.getKey())));
} else if (PROGRAM_HOOD_DELAYED_SHUT_OFF.equals(availableProgram.getKey())) {
stateOptions.add(
new StateOption(COMMAND_DELAYED_SHUT_OFF, mapStringType(availableProgram.getKey())));
} else if (PROGRAM_HOOD_VENTING.equals(availableProgram.getKey())) {
try {
apiClient.get().getProgramOptions(getThingHaId(), PROGRAM_HOOD_VENTING).forEach(option -> {
if (OPTION_HOOD_VENTING_LEVEL.equalsIgnoreCase(option.getKey())) {
option.getAllowedValues().stream().filter(s -> !STAGE_FAN_OFF.equalsIgnoreCase(s))
.forEach(s -> stateOptions.add(createVentingStateOption(s)));
} else if (OPTION_HOOD_INTENSIVE_LEVEL.equalsIgnoreCase(option.getKey())) {
option.getAllowedValues().stream()
.filter(s -> !STAGE_INTENSIVE_STAGE_OFF.equalsIgnoreCase(s))
.forEach(s -> stateOptions.add(createVentingStateOption(s)));
}
});
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.warn("Could not fetch hood program options. error={}", e.getMessage());
stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_01));
stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_02));
stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_03));
stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_04));
stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_05));
stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_1));
stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_2));
}
}
});
stateOptions.add(new StateOption(COMMAND_STOP, "Stop"));
getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(channel -> getDynamicStateDescriptionProvider()
.setStateOptions(channel.getUID(), stateOptions));
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
getThingHaId(), e.getMessage());
removeSelectedProgramStateDescription();
}
} else {
removeSelectedProgramStateDescription();
}
}
@Override
protected void removeSelectedProgramStateDescription() {
getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(
channel -> getDynamicStateDescriptionProvider().setStateOptions(channel.getUID(), emptyList()));
}
@Override
public String toString() {
return "HomeConnectHoodHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
private StateOption createVentingStateOption(String optionKey) {
String label = mapStringType(PROGRAM_HOOD_VENTING);
if (STAGE_FAN_STAGE_01.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_1,
format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_01)));
} else if (STAGE_FAN_STAGE_02.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_2,
format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_02)));
} else if (STAGE_FAN_STAGE_03.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_3,
format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_03)));
} else if (STAGE_FAN_STAGE_04.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_4,
format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_04)));
} else if (STAGE_FAN_STAGE_05.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_5,
format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_05)));
} else if (STAGE_INTENSIVE_STAGE_1.equalsIgnoreCase(optionKey)) {
return new StateOption(COMMAND_VENTING_INTENSIVE_1,
format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_1)));
} else {
return new StateOption(COMMAND_VENTING_INTENSIVE_2,
format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_2)));
}
}
}

View File

@ -0,0 +1,235 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import static org.openhab.core.library.unit.Units.SECOND;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.IncommensurableException;
import javax.measure.UnconvertibleException;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.Data;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectOvenHandler} is responsible for handling commands, which are
* sent to one of the channels of a oven.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectOvenHandler extends AbstractHomeConnectThingHandler {
private static final List<String> INACTIVE_STATE = Arrays.asList(OPERATION_STATE_INACTIVE, OPERATION_STATE_READY);
private static final int CAVITY_TEMPERATURE_SCHEDULER_INITIAL_DELAY = 30;
private static final int CAVITY_TEMPERATURE_SCHEDULER_PERIOD = 90;
private final Logger logger = LoggerFactory.getLogger(HomeConnectOvenHandler.class);
private @Nullable ScheduledFuture<?> cavityTemperatureFuture;
private boolean manuallyUpdateCavityTemperature;
public HomeConnectOvenHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
manuallyUpdateCavityTemperature = true;
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE, defaultSelectedProgramStateUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
// register oven specific update handlers
handlers.put(CHANNEL_OVEN_CURRENT_CAVITY_TEMPERATURE,
(channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
Data data = apiClient.get().getCurrentCavityTemperature(getThingHaId());
return new QuantityType<>(data.getValueAsInt(), mapTemperature(data.getUnit()));
}
return UnDefType.UNDEF;
})));
handlers.put(CHANNEL_SETPOINT_TEMPERATURE, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
}
});
handlers.put(CHANNEL_DURATION, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
}
});
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default SSE event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_SELECTED_PROGRAM, defaultSelectedProgramStateEventHandler());
handlers.put(EVENT_REMAINING_PROGRAM_TIME, defaultRemainingProgramTimeEventHandler());
handlers.put(EVENT_PROGRAM_PROGRESS, defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE));
handlers.put(EVENT_ELAPSED_PROGRAM_TIME, defaultElapsedProgramTimeEventHandler());
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
// register oven specific SSE event handlers
handlers.put(EVENT_OPERATION_STATE, event -> {
defaultOperationStateEventHandler().handle(event);
if (STATE_OPERATION_RUN.equals(event.getValue())) {
manuallyUpdateCavityTemperature = true;
}
});
handlers.put(EVENT_POWER_STATE, event -> {
getThingChannel(CHANNEL_POWER_STATE).ifPresent(
channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
if (STATE_POWER_ON.equals(event.getValue())) {
updateChannels();
} else {
resetProgramStateChannels();
getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
.ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_SETPOINT_TEMPERATURE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_DURATION).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
});
handlers.put(EVENT_OVEN_CAVITY_TEMPERATURE, event -> {
manuallyUpdateCavityTemperature = false;
getThingChannel(CHANNEL_OVEN_CURRENT_CAVITY_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
new QuantityType<>(event.getValueAsInt(), mapTemperature(event.getUnit()))));
});
handlers.put(EVENT_SETPOINT_TEMPERATURE,
event -> getThingChannel(CHANNEL_SETPOINT_TEMPERATURE)
.ifPresent(channel -> updateState(channel.getUID(),
new QuantityType<>(event.getValueAsInt(), mapTemperature(event.getUnit())))));
handlers.put(EVENT_DURATION, event -> getThingChannel(CHANNEL_DURATION).ifPresent(
channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND))));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
// turn coffee maker on and standby
if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
apiClient.setPowerState(getThingHaId(),
OnOffType.ON.equals(command) ? STATE_POWER_ON : STATE_POWER_STANDBY);
}
String operationState = getOperationState();
if (operationState != null && INACTIVE_STATE.contains(operationState) && command instanceof QuantityType) {
// set setpoint temperature
if (CHANNEL_SETPOINT_TEMPERATURE.equals(channelUID.getId())) {
handleTemperatureCommand(channelUID, command, apiClient);
} else if (CHANNEL_DURATION.equals(channelUID.getId())) {
@SuppressWarnings("unchecked")
QuantityType<Time> quantity = ((QuantityType<Time>) command);
try {
String value = String
.valueOf(quantity.getUnit().getConverterToAny(SECOND).convert(quantity).intValue());
logger.debug("Set duration to {} seconds. haId={}", value, getThingHaId());
apiClient.setProgramOptions(getThingHaId(), OPTION_DURATION, value, "seconds", true, false);
} catch (IncommensurableException | UnconvertibleException e) {
logger.warn("Could not set duration! haId={}, error={}", getThingHaId(), e.getMessage());
}
}
} else {
logger.debug("Device can not handle command {} in current operation state ({}). haId={}", command,
operationState, getThingHaId());
}
}
@Override
public void initialize() {
super.initialize();
cavityTemperatureFuture = scheduler.scheduleWithFixedDelay(() -> {
String operationState = getOperationState();
boolean manuallyUpdateCavityTemperature = this.manuallyUpdateCavityTemperature;
if (STATE_OPERATION_RUN.equals(operationState)) {
getThingChannel(CHANNEL_OVEN_CURRENT_CAVITY_TEMPERATURE).ifPresent(c -> {
if (manuallyUpdateCavityTemperature) {
logger.debug("Update cavity temperature manually via API. haId={}", getThingHaId());
updateChannel(c.getUID());
} else {
logger.debug("Update cavity temperature via SSE, don't need to fetch manually. haId={}",
getThingHaId());
}
});
}
}, CAVITY_TEMPERATURE_SCHEDULER_INITIAL_DELAY, CAVITY_TEMPERATURE_SCHEDULER_PERIOD, TimeUnit.SECONDS);
}
@Override
public void dispose() {
ScheduledFuture<?> cavityTemperatureFuture = this.cavityTemperatureFuture;
if (cavityTemperatureFuture != null) {
cavityTemperatureFuture.cancel(true);
}
super.dispose();
}
@Override
public String toString() {
return "HomeConnectOvenHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectWasherDryerHandler} is responsible for handling commands, which are
* sent to one of the channels of a washer dryer combined machine.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectWasherDryerHandler extends AbstractHomeConnectThingHandler {
private static final List<String> INACTIVE_STATE = Arrays.asList(OPERATION_STATE_INACTIVE, OPERATION_STATE_READY);
private final Logger logger = LoggerFactory.getLogger(HomeConnectWasherDryerHandler.class);
public HomeConnectWasherDryerHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE,
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler());
// register washer specific handlers
handlers.put(CHANNEL_WASHER_SPIN_SPEED, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
.handle(channel.get().getUID(), cache);
}
});
handlers.put(CHANNEL_WASHER_TEMPERATURE, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
.handle(channel.get().getUID(), cache);
}
});
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMAINING_PROGRAM_TIME, defaultRemainingProgramTimeEventHandler());
handlers.put(EVENT_PROGRAM_PROGRESS, defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_SELECTED_PROGRAM, updateProgramOptionsAndSelectedProgramStateEventHandler());
// register washer specific event handlers
handlers.put(EVENT_WASHER_TEMPERATURE,
event -> getThingChannel(CHANNEL_WASHER_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
handlers.put(EVENT_WASHER_SPIN_SPEED,
event -> getThingChannel(CHANNEL_WASHER_SPIN_SPEED).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
handlers.put(EVENT_DRYER_DRYING_TARGET,
event -> getThingChannel(CHANNEL_DRYER_DRYING_TARGET).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
String operationState = getOperationState();
// only handle these commands if operation state allows it
if (operationState != null && INACTIVE_STATE.contains(operationState) && command instanceof StringType) {
switch (channelUID.getId()) {
case CHANNEL_WASHER_TEMPERATURE:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_TEMPERATURE, command.toFullString(), null,
false, false);
break;
case CHANNEL_WASHER_SPIN_SPEED:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_SPIN_SPEED, command.toFullString(), null,
false, false);
break;
case CHANNEL_DRYER_DRYING_TARGET:
apiClient.setProgramOptions(getThingHaId(), OPTION_DRYER_DRYING_TARGET, command.toFullString(),
null, false, false);
break;
}
} else {
logger.debug("Device can not handle command {} in current operation state ({}). haId={}", command,
operationState, getThingHaId());
}
}
@Override
public String toString() {
return "HomeConnectWasherDryerHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_WASHER_TEMPERATURE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_WASHER_SPIN_SPEED).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeConnectWasherHandler} is responsible for handling commands, which are
* sent to one of the channels of a washing machine.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler {
private static final List<String> INACTIVE_STATE = Arrays.asList(OPERATION_STATE_INACTIVE, OPERATION_STATE_READY);
private final Logger logger = LoggerFactory.getLogger(HomeConnectWasherHandler.class);
public HomeConnectWasherHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
super(thing, dynamicStateDescriptionProvider);
}
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
// register default update handlers
handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler());
handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
handlers.put(CHANNEL_SELECTED_PROGRAM_STATE,
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler());
// register washer specific handlers
handlers.put(CHANNEL_WASHER_SPIN_SPEED, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
.handle(channel.get().getUID(), cache);
}
});
handlers.put(CHANNEL_WASHER_TEMPERATURE, (channelUID, cache) -> {
Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
if (channel.isPresent()) {
updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
.handle(channel.get().getUID(), cache);
}
});
}
@Override
protected void configureEventHandlers(Map<String, EventHandler> handlers) {
// register default event handlers
handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler());
handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
handlers.put(EVENT_REMAINING_PROGRAM_TIME, defaultRemainingProgramTimeEventHandler());
handlers.put(EVENT_PROGRAM_PROGRESS, defaultPercentQuantityTypeEventHandler(CHANNEL_PROGRAM_PROGRESS_STATE));
handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
handlers.put(EVENT_SELECTED_PROGRAM, updateProgramOptionsAndSelectedProgramStateEventHandler());
// register washer specific event handlers
handlers.put(EVENT_WASHER_TEMPERATURE,
event -> getThingChannel(CHANNEL_WASHER_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
handlers.put(EVENT_WASHER_SPIN_SPEED,
event -> getThingChannel(CHANNEL_WASHER_SPIN_SPEED).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
handlers.put(EVENT_WASHER_IDOS_1_DOSING_LEVEL,
event -> getThingChannel(CHANNEL_WASHER_IDOS1).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
handlers.put(EVENT_WASHER_IDOS_2_DOSING_LEVEL,
event -> getThingChannel(CHANNEL_WASHER_IDOS2).ifPresent(channel -> updateState(channel.getUID(),
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command,
final HomeConnectApiClient apiClient)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
super.handleCommand(channelUID, command, apiClient);
String operationState = getOperationState();
// only handle these commands if operation state allows it
if (operationState != null && INACTIVE_STATE.contains(operationState) && command instanceof StringType) {
switch (channelUID.getId()) {
case CHANNEL_WASHER_TEMPERATURE:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_TEMPERATURE, command.toFullString(), null,
false, false);
break;
case CHANNEL_WASHER_SPIN_SPEED:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_SPIN_SPEED, command.toFullString(), null,
false, false);
break;
case CHANNEL_WASHER_IDOS1:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_IDOS_1_DOSING_LEVEL,
command.toFullString(), null, false, false);
break;
case CHANNEL_WASHER_IDOS2:
apiClient.setProgramOptions(getThingHaId(), OPTION_WASHER_IDOS_2_DOSING_LEVEL,
command.toFullString(), null, false, false);
break;
}
} else {
logger.debug("Device can not handle command {} in current operation state ({}). haId={}", command,
operationState, getThingHaId());
}
}
@Override
public String toString() {
return "HomeConnectWasherHandler [haId: " + getThingHaId() + "]";
}
@Override
protected void resetProgramStateChannels() {
super.resetProgramStateChannels();
getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_WASHER_TEMPERATURE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
getThingChannel(CHANNEL_WASHER_SPIN_SPEED).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
/**
* Custom supplier implementation with exceptions.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
@FunctionalInterface
public interface SupplierWithException<T> {
T get() throws CommunicationException, ApplianceOfflineException, AuthorizationException;
}

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler.cache;
import java.lang.ref.SoftReference;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.handler.SupplierWithException;
import org.openhab.core.types.State;
/**
*
* Expiring state model. Holds a state and the corresponding expiration time.
*
* @author Jonas Brüstel - Initial Contribution
*/
@NonNullByDefault
public class ExpiringStateCache {
private final SupplierWithException<State> stateSupplier;
private final long expiry;
private SoftReference<@Nullable State> state = new SoftReference<>(null);
private long expiresAt;
/**
* Create a new instance.
*
* @param expiry the duration for how long the state should be cached
* @param stateSupplier supplier to get current state
*/
public ExpiringStateCache(Duration expiry, SupplierWithException<State> stateSupplier) {
this.stateSupplier = stateSupplier;
this.expiry = expiry.toNanos();
}
/**
* Returns the cached or newly fetched state.
*
* @return State
* @throws CommunicationException API communication exception
* @throws AuthorizationException oAuth authorization exception
* @throws ApplianceOfflineException appliance is not connected to the cloud
*/
public synchronized State getState()
throws AuthorizationException, ApplianceOfflineException, CommunicationException {
State cachedValue = state.get();
if (cachedValue == null || isExpired()) {
return refreshState();
}
return cachedValue;
}
private State refreshState() throws AuthorizationException, ApplianceOfflineException, CommunicationException {
State freshValue = stateSupplier.get();
state = new SoftReference<>(freshValue);
expiresAt = calcExpiresAt();
return freshValue;
}
private boolean isExpired() {
return expiresAt < System.nanoTime();
}
private long calcExpiresAt() {
return System.nanoTime() + expiry;
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.handler.cache;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.handler.SupplierWithException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* This is a simple expiring state cache implementation. The state value expires after the
* specified duration has passed since the item was created.
*
* @author Jonas Brüstel - Initial contribution
*/
@NonNullByDefault
public class ExpiringStateMap {
private final Duration expiry;
private final ConcurrentMap<ChannelUID, ExpiringStateCache> items;
/**
* Expiring state map.
*
* @param expiry expiry duration
*/
public ExpiringStateMap(Duration expiry) {
this.expiry = expiry;
this.items = new ConcurrentHashMap<>();
}
/**
* Get cached value or retrieve new state value via supplier.
*
* @param channelUID cache key / channel uid
* @param supplier supplier
* @return current state
* @throws CommunicationException API communication exception
* @throws AuthorizationException oAuth authorization exception
* @throws ApplianceOfflineException appliance is not connected to the cloud
*/
public State putIfAbsentAndGet(ChannelUID channelUID, SupplierWithException<State> supplier)
throws AuthorizationException, ApplianceOfflineException, CommunicationException {
items.putIfAbsent(channelUID, new ExpiringStateCache(expiry, supplier));
final ExpiringStateCache item = items.get(channelUID);
if (item == null) {
return UnDefType.UNDEF;
} else {
return item.getState();
}
}
}

View File

@ -0,0 +1,563 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.servlet;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.ZonedDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
import org.openhab.binding.homeconnect.internal.handler.AbstractHomeConnectThingHandler;
import org.openhab.binding.homeconnect.internal.handler.HomeConnectBridgeHandler;
import org.openhab.core.OpenHAB;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.osgi.framework.FrameworkUtil;
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.component.annotations.ServiceScope;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
/**
*
* Home Connect servlet.
*
* @author Jonas Brüstel - Initial Contribution
*/
@NonNullByDefault
@Component(service = HomeConnectServlet.class, scope = ServiceScope.SINGLETON, immediate = true)
public class HomeConnectServlet extends HttpServlet {
private static final String SLASH = "/";
private static final String SERVLET_NAME = "homeconnect";
private static final String SERVLET_PATH = SLASH + SERVLET_NAME;
private static final String ASSETS_PATH = SERVLET_PATH + "/asset";
private static final String ROOT_PATH = SLASH;
private static final String APPLIANCES_PATH = "/appliances";
private static final String REQUEST_LOG_PATH = "/log/requests";
private static final String EVENT_LOG_PATH = "/log/events";
private static final String DEFAULT_CONTENT_TYPE = "text/html; charset=UTF-8";
private static final String PARAM_CODE = "code";
private static final String PARAM_STATE = "state";
private static final String PARAM_EXPORT = "export";
private static final String PARAM_ACTION = "action";
private static final String PARAM_BRIDGE_ID = "bridgeId";
private static final String PARAM_THING_ID = "thingId";
private static final String PARAM_PATH = "path";
private static final String ACTION_AUTHORIZE = "authorize";
private static final String ACTION_CLEAR_CREDENTIALS = "clearCredentials";
private static final String ACTION_SHOW_DETAILS = "show-details";
private static final String ACTION_ALL_PROGRAMS = "all-programs";
private static final String ACTION_AVAILABLE_PROGRAMS = "available-programs";
private static final String ACTION_SELECTED_PROGRAM = "selected-program";
private static final String ACTION_ACTIVE_PROGRAM = "active-program";
private static final String ACTION_OPERATION_STATE = "operation-state";
private static final String ACTION_POWER_STATE = "power-state";
private static final String ACTION_DOOR_STATE = "door-state";
private static final String ACTION_REMOTE_START_ALLOWED = "remote-control-start-allowed";
private static final String ACTION_REMOTE_CONTROL_ACTIVE = "remote-control-active";
private static final String ACTION_PUT_RAW = "put-raw";
private static final String ACTION_GET_RAW = "get-raw";
private static final DateTimeFormatter FILE_EXPORT_DTF = ISO_OFFSET_DATE_TIME;
private static final String EMPTY_RESPONSE = "{}";
private static final long serialVersionUID = -2449763690208703307L;
private final Logger logger = LoggerFactory.getLogger(HomeConnectServlet.class);
private final HttpService httpService;
private final TemplateEngine templateEngine;
private final Set<HomeConnectBridgeHandler> bridgeHandlers;
private final Gson gson;
@Activate
public HomeConnectServlet(@Reference HttpService httpService) {
bridgeHandlers = new CopyOnWriteArraySet<>();
gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, (JsonSerializer<ZonedDateTime>) (src,
typeOfSrc, context) -> new JsonPrimitive(src.format(DateTimeFormatter.ISO_DATE_TIME))).create();
this.httpService = httpService;
// register servlet
try {
logger.debug("Initialize Home Connect configuration servlet ({})", SERVLET_PATH);
httpService.registerServlet(SERVLET_PATH, this, null, httpService.createDefaultHttpContext());
httpService.registerResources(ASSETS_PATH, "assets", null);
} catch (ServletException | NamespaceException e) {
logger.warn("Could not register Home Connect servlet! ({})", SERVLET_PATH, e);
}
// setup template engine
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(getServletContext());
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setPrefix("/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCacheable(true);
templateEngine = new TemplateEngine();
templateEngine.addDialect(new Java8TimeDialect());
templateEngine.setTemplateResolver(templateResolver);
}
@Deactivate
protected void dispose() {
httpService.unregister(SERVLET_PATH);
httpService.unregister(ASSETS_PATH);
}
@Override
protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws IOException {
if (request == null || response == null) {
return;
}
response.setContentType(DEFAULT_CONTENT_TYPE);
response.setCharacterEncoding(UTF_8.name());
String path = request.getPathInfo();
if (path == null || path.isEmpty() || path.equals(ROOT_PATH)) {
@Nullable
String code = request.getParameter(PARAM_CODE);
@Nullable
String state = request.getParameter(PARAM_STATE);
if (code != null && state != null && !code.trim().isEmpty() && !state.trim().isEmpty()) {
getBridgeAuthenticationPage(request, response, code, state);
} else {
getBridgesPage(request, response);
}
} else if (pathMatches(path, APPLIANCES_PATH)) {
@Nullable
String action = request.getParameter(PARAM_ACTION);
@Nullable
String thingId = request.getParameter(PARAM_THING_ID);
if (action != null && thingId != null && !action.trim().isEmpty() && !thingId.trim().isEmpty()) {
processApplianceActions(response, action, thingId);
} else {
getAppliancesPage(request, response);
}
} else if (pathMatches(path, REQUEST_LOG_PATH)) {
@Nullable
String export = request.getParameter(PARAM_EXPORT);
@Nullable
String bridgeId = request.getParameter(PARAM_BRIDGE_ID);
if (export != null && bridgeId != null && !export.trim().isEmpty() && !bridgeId.trim().isEmpty()) {
getRequestLogExport(response, bridgeId);
} else {
getRequestLogPage(request, response);
}
} else if (pathMatches(path, EVENT_LOG_PATH)) {
@Nullable
String export = request.getParameter(PARAM_EXPORT);
@Nullable
String bridgeId = request.getParameter(PARAM_BRIDGE_ID);
if (export != null && bridgeId != null && !export.trim().isEmpty() && !bridgeId.trim().isEmpty()) {
getEventLogExport(response, bridgeId);
} else {
getEventLogPage(request, response);
}
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doPost(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws IOException {
if (request == null || response == null) {
return;
}
response.setContentType("text/html; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
String path = request.getPathInfo();
if (path == null || path.isEmpty() || path.equals(ROOT_PATH)) {
if (request.getParameter(PARAM_ACTION) != null && request.getParameter(PARAM_BRIDGE_ID) != null) {
postBridgesPage(request, response);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} else if (pathMatches(path, APPLIANCES_PATH)) {
String requestPayload = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
@Nullable
String action = request.getParameter(PARAM_ACTION);
@Nullable
String thingId = request.getParameter(PARAM_THING_ID);
@Nullable
String targetPath = request.getParameter(PARAM_PATH);
if ((ACTION_PUT_RAW.equals(action) || ACTION_GET_RAW.equals(action)) && thingId != null
&& targetPath != null && action != null) {
processRawApplianceActions(response, action, thingId, targetPath, requestPayload);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Add Home Connect bridge handler to configuration servlet, to allow user to authenticate against Home Connect API.
*
* @param bridgeHandler bridge handler
*/
public void addBridgeHandler(HomeConnectBridgeHandler bridgeHandler) {
bridgeHandlers.add(bridgeHandler);
}
/**
* Remove Home Connect bridge handler from configuration servlet.
*
* @param bridgeHandler bridge handler
*/
public void removeBridgeHandler(HomeConnectBridgeHandler bridgeHandler) {
bridgeHandlers.remove(bridgeHandler);
}
private void getAppliancesPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("bridgeHandlers", bridgeHandlers);
templateEngine.process("appliances", context, response.getWriter());
}
private void processApplianceActions(HttpServletResponse response, String action, String thingId)
throws IOException {
Optional<HomeConnectBridgeHandler> bridgeHandler = getBridgeHandlerForThing(thingId);
Optional<AbstractHomeConnectThingHandler> thingHandler = getThingHandler(thingId);
if (bridgeHandler.isPresent() && thingHandler.isPresent()) {
try {
response.setContentType(MediaType.APPLICATION_JSON);
String haId = thingHandler.get().getThingHaId();
switch (action) {
case ACTION_SHOW_DETAILS: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_ALL_PROGRAMS: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/programs");
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_AVAILABLE_PROGRAMS: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/programs/available");
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_SELECTED_PROGRAM: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/programs/selected");
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_ACTIVE_PROGRAM: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/programs/active");
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_OPERATION_STATE: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/status/" + EVENT_OPERATION_STATE);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_POWER_STATE: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/settings/" + EVENT_POWER_STATE);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_DOOR_STATE: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/status/" + EVENT_DOOR_STATE);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_REMOTE_START_ALLOWED: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/status/" + EVENT_REMOTE_CONTROL_START_ALLOWED);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
case ACTION_REMOTE_CONTROL_ACTIVE: {
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId,
"/api/homeappliances/" + haId + "/status/" + EVENT_REMOTE_CONTROL_ACTIVE);
response.getWriter().write(actionResponse != null ? actionResponse : EMPTY_RESPONSE);
break;
}
default:
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown action");
break;
}
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.debug("Could not execute request! thingId={}, action={}, error={}", thingId, action,
e.getMessage());
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, e.getMessage());
}
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Thing or bridge not found!");
}
}
private void processRawApplianceActions(HttpServletResponse response, String action, String thingId, String path,
String body) throws IOException {
Optional<HomeConnectBridgeHandler> bridgeHandler = getBridgeHandlerForThing(thingId);
Optional<AbstractHomeConnectThingHandler> thingHandler = getThingHandler(thingId);
if (bridgeHandler.isPresent() && thingHandler.isPresent()) {
try {
response.setContentType(MediaType.APPLICATION_JSON);
String haId = thingHandler.get().getThingHaId();
if (ACTION_PUT_RAW.equals(action)) {
String actionResponse = bridgeHandler.get().getApiClient().putRaw(haId, path, body);
response.getWriter().write(actionResponse);
} else if (ACTION_GET_RAW.equals(action)) {
@Nullable
String actionResponse = bridgeHandler.get().getApiClient().getRaw(haId, path, true);
if (actionResponse == null) {
response.getWriter().write("{\"status\": \"No response\"}");
} else {
response.getWriter().write(actionResponse);
}
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown action");
}
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.debug("Could not execute request! thingId={}, action={}, error={}", thingId, action,
e.getMessage());
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, e.getMessage());
}
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Bridge or Thing not found!");
}
}
private void getBridgesPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("bridgeHandlers", bridgeHandlers);
templateEngine.process("bridges", context, response.getWriter());
}
private void postBridgesPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
@Nullable
String action = request.getParameter(PARAM_ACTION);
@Nullable
String bridgeId = request.getParameter(PARAM_BRIDGE_ID);
Optional<HomeConnectBridgeHandler> bridgeHandlerOptional = bridgeHandlers.stream().filter(
homeConnectBridgeHandler -> homeConnectBridgeHandler.getThing().getUID().toString().equals(bridgeId))
.findFirst();
if (bridgeHandlerOptional.isPresent()
&& (ACTION_AUTHORIZE.equals(action) || ACTION_CLEAR_CREDENTIALS.equals(action))) {
HomeConnectBridgeHandler bridgeHandler = bridgeHandlerOptional.get();
if (ACTION_AUTHORIZE.equals(action)) {
try {
String authorizationUrl = bridgeHandler.getOAuthClientService().getAuthorizationUrl(null, null,
bridgeHandler.getThing().getUID().getAsString());
logger.debug("Generated authorization url: {}", authorizationUrl);
response.sendRedirect(authorizationUrl);
} catch (OAuthException e) {
logger.error("Could not create authorization url!", e);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "Could not create authorization url!");
}
} else {
logger.info("Remove access token for '{}' bridge.", bridgeHandler.getThing().getLabel());
try {
bridgeHandler.getOAuthClientService().remove();
} catch (OAuthException e) {
logger.debug("Could not clear oAuth credentials. error={}", e.getMessage());
}
bridgeHandler.reinitialize();
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("action",
bridgeHandler.getThing().getUID().getAsString() + ACTION_CLEAR_CREDENTIALS);
context.setVariable("bridgeHandlers", bridgeHandlers);
templateEngine.process("bridges", context, response.getWriter());
}
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown bridge or action is missing!");
}
}
private void getRequestLogPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
ArrayList<ApiRequest> requests = new ArrayList<>();
bridgeHandlers.forEach(homeConnectBridgeHandler -> requests
.addAll(homeConnectBridgeHandler.getApiClient().getLatestApiRequests()));
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("bridgeHandlers", bridgeHandlers);
context.setVariable("requests", gson.toJson(requests));
templateEngine.process("log-requests", context, response.getWriter());
}
private void getRequestLogExport(HttpServletResponse response, String bridgeId) throws IOException {
Optional<HomeConnectBridgeHandler> bridgeHandler = getBridgeHandler(bridgeId);
if (bridgeHandler.isPresent()) {
response.setContentType(MediaType.APPLICATION_JSON);
String fileName = String.format("%s__%s__requests.json", now().format(FILE_EXPORT_DTF),
bridgeId.replaceAll("[^a-zA-Z0-9]", "_"));
response.setHeader("Content-disposition", "attachment; filename=" + fileName);
HashMap<String, Object> responsePayload = new HashMap<>();
responsePayload.put("openHAB", OpenHAB.getVersion());
responsePayload.put("bundle", FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
List<ApiRequest> apiRequestList = bridgeHandler.get().getApiClient().getLatestApiRequests().stream()
.peek(apiRequest -> {
Map<String, String> headers = apiRequest.getRequest().getHeader();
if (headers.containsKey("authorization")) {
headers.put("authorization", "*replaced*");
} else if (headers.containsKey("Authorization")) {
headers.put("Authorization", "*replaced*");
}
}).collect(Collectors.toList());
responsePayload.put("requests", apiRequestList);
response.getWriter().write(gson.toJson(responsePayload));
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown bridge");
}
}
private void getEventLogPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("bridgeHandlers", bridgeHandlers);
templateEngine.process("log-events", context, response.getWriter());
}
private void getEventLogExport(HttpServletResponse response, String bridgeId) throws IOException {
Optional<HomeConnectBridgeHandler> bridgeHandler = getBridgeHandler(bridgeId);
if (bridgeHandler.isPresent()) {
response.setContentType(MediaType.APPLICATION_JSON);
String fileName = String.format("%s__%s__events.json", now().format(FILE_EXPORT_DTF),
bridgeId.replaceAll("[^a-zA-Z0-9]", "_"));
response.setHeader("Content-disposition", "attachment; filename=" + fileName);
HashMap<String, Object> responsePayload = new HashMap<>();
responsePayload.put("openHAB", OpenHAB.getVersion());
responsePayload.put("bundle", FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
responsePayload.put("events", bridgeHandler.get().getEventSourceClient().getLatestEvents());
response.getWriter().write(gson.toJson(responsePayload));
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown bridge");
}
}
private void getBridgeAuthenticationPage(HttpServletRequest request, HttpServletResponse response, String code,
String state) throws IOException {
// callback handling from authorization server
logger.debug("[oAuth] redirect from authorization server (code={}, state={}).", code, state);
Optional<HomeConnectBridgeHandler> bridgeHandler = getBridgeHandler(state);
if (bridgeHandler.isPresent()) {
try {
AccessTokenResponse accessTokenResponse = bridgeHandler.get().getOAuthClientService()
.getAccessTokenResponseByAuthorizationCode(code, null);
logger.debug("access token response: {}", accessTokenResponse);
// inform bridge
bridgeHandler.get().reinitialize();
WebContext context = new WebContext(request, response, request.getServletContext());
context.setVariable("action", bridgeHandler.get().getThing().getUID().getAsString() + ACTION_AUTHORIZE);
context.setVariable("bridgeHandlers", bridgeHandlers);
templateEngine.process("bridges", context, response.getWriter());
} catch (OAuthException | OAuthResponseException e) {
logger.error("Could not fetch token!", e);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "Could not fetch token!");
}
} else {
response.sendError(HttpStatus.BAD_REQUEST_400, "Unknown bridge");
}
}
private boolean pathMatches(String path, String targetPath) {
return targetPath.equals(path) || (targetPath + SLASH).equals(path);
}
private Optional<HomeConnectBridgeHandler> getBridgeHandler(String bridgeUid) {
for (HomeConnectBridgeHandler handler : bridgeHandlers) {
if (handler.getThing().getUID().getAsString().equals(bridgeUid)) {
return Optional.of(handler);
}
}
return Optional.empty();
}
private Optional<AbstractHomeConnectThingHandler> getThingHandler(String thingUid) {
for (HomeConnectBridgeHandler handler : bridgeHandlers) {
for (AbstractHomeConnectThingHandler thingHandler : handler.getThingHandler()) {
if (thingHandler.getThing().getUID().getAsString().equals(thingUid)) {
return Optional.of(thingHandler);
}
}
}
return Optional.empty();
}
private Optional<HomeConnectBridgeHandler> getBridgeHandlerForThing(String thingUid) {
for (HomeConnectBridgeHandler handler : bridgeHandlers) {
for (AbstractHomeConnectThingHandler thingHandler : handler.getThingHandler()) {
if (thingHandler.getThing().getUID().getAsString().equals(thingUid)) {
return Optional.of(handler);
}
}
}
return Optional.empty();
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.homeconnect.internal.type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link HomeConnectDynamicStateDescriptionProvider} is responsible for handling dynamic values.
*
* @author Jonas Brüstel - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, HomeConnectDynamicStateDescriptionProvider.class })
@NonNullByDefault
public class HomeConnectDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="homeconnect" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Home Connect Binding</name>
<description>The binding integrates the Home Connect (https://www.home-connect.com/) system into openHAB. It connects
to household devices from brands like Bosch and Siemens.</description>
</binding:binding>

View File

@ -0,0 +1,658 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="homeconnect"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Home Connect API Bridge -->
<bridge-type id="api_bridge">
<label>Home Connect API</label>
<description>This bridge represents the gateway to the Home Connect API.</description>
<config-description>
<parameter name="clientId" type="text" required="true">
<label>Client Id</label>
<description>Application client id</description>
</parameter>
<parameter name="clientSecret" type="text" required="true">
<label>Client Secret</label>
<description>Application client secret</description>
<context>password</context>
</parameter>
<parameter name="simulator" type="boolean" required="true">
<label>Use Simulator Environment</label>
<description>Use simulated environment at https://developer.home-connect.com/simulator/</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<!-- Dishwasher -->
<thing-type id="dishwasher">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Dishwasher</label>
<description>Home Connect connected dishwasher (e.g. Bosch or Siemens).</description>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
<channel id="ambient_light_state" typeId="ambient_light_state"/>
<channel id="ambient_light_brightness_state" typeId="system.brightness"/>
<channel id="ambient_light_color_state" typeId="ambient_light_color_state"/>
<channel id="ambient_light_custom_color_state" typeId="ambient_light_custom_color_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Oven -->
<thing-type id="oven">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Oven</label>
<description>Home Connect connected oven (e.g. Bosch or Siemens).</description>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="setpoint_temperature" typeId="setpoint_temperature"/>
<channel id="duration" typeId="duration"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
<channel id="oven_current_cavity_temperature" typeId="oven_current_cavity_temperature"/>
<channel id="elapsed_program_time" typeId="elapsed_program_time"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Washer -->
<thing-type id="washer">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Washer</label>
<description>Home Connect connected washing machine (e.g. Bosch or Siemens).</description>
<channels>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="laundry_care_washer_temperature" typeId="laundry_care_washer_temperature"/>
<channel id="laundry_care_washer_spin_speed" typeId="laundry_care_washer_spin_speed"/>
<channel id="laundry_care_washer_idos1" typeId="laundry_care_washer_idos1"/>
<channel id="laundry_care_washer_idos2" typeId="laundry_care_washer_idos2"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Washer dryer combination -->
<thing-type id="washerdryer">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Washer Dryer Combination</label>
<description>Home Connect connected combined washer dryer appliance.</description>
<channels>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="laundry_care_washer_temperature" typeId="laundry_care_washer_temperature"/>
<channel id="laundry_care_washer_spin_speed" typeId="laundry_care_washer_spin_speed"/>
<channel id="dryer_drying_target" typeId="dryer_drying_target"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Dryer -->
<thing-type id="dryer">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Dryer</label>
<description>Home Connect connected dryer (e.g. Bosch or Siemens).</description>
<channels>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="dryer_drying_target" typeId="dryer_drying_target"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Fridge Freezer -->
<thing-type id="fridgefreezer">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Refrigerator / Freezer</label>
<description>Home Connect connected refrigerator/freezer (e.g. Bosch or Siemens).</description>
<channels>
<channel id="door_state" typeId="door_state"/>
<channel id="setpoint_temperature_refrigerator" typeId="setpoint_temperature_refrigerator"/>
<channel id="super_mode_refrigerator" typeId="super_mode_refrigerator"/>
<channel id="setpoint_temperature_freezer" typeId="setpoint_temperature_freezer"/>
<channel id="super_mode_freezer" typeId="super_mode_freezer"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Coffee Machine -->
<thing-type id="coffeemaker">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Coffee Machine</label>
<description>Home Connect connected coffee machine (e.g. Bosch or Siemens).</description>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
<channel id="basic_actions_state" typeId="basic_actions_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
<channel id="coffeemaker_drip_tray_full_state" typeId="coffeemaker_drip_tray_full_state"/>
<channel id="coffeemaker_water_tank_empty_state" typeId="coffeemaker_water_tank_empty_state"/>
<channel id="coffeemaker_bean_container_empty_state" typeId="coffeemaker_bean_container_empty_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Hood -->
<thing-type id="hood">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Hood</label>
<description>Home Connect connected kitchen hood.</description>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="hood_venting_level" typeId="hood_venting_level"/>
<channel id="hood_intensive_level" typeId="hood_intensive_level"/>
<channel id="hood_program_state" typeId="hood_program_state"/>
<channel id="ambient_light_state" typeId="ambient_light_state"/>
<channel id="ambient_light_brightness_state" typeId="system.brightness"/>
<channel id="ambient_light_color_state" typeId="ambient_light_color_state"/>
<channel id="ambient_light_custom_color_state" typeId="ambient_light_custom_color_state"/>
<channel id="functional_light_state" typeId="functional_light_state"/>
<channel id="functional_light_brightness_state" typeId="system.brightness"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Cooktop -->
<thing-type id="hob">
<supported-bridge-type-refs>
<bridge-type-ref id="api_bridge"/>
</supported-bridge-type-refs>
<label>Cooktop</label>
<description>Home Connect connected kitchen cooktop (hob).</description>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_control_active_state" typeId="remote_control_active_state"/>
<channel id="local_control_active_state" typeId="local_control_active_state"/>
<channel id="active_program_state" typeId="active_program_state"/>
<channel id="selected_program_state" typeId="selected_program_state"/>
</channels>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
<label>haId</label>
<description>Unique identifier representing a specific home appliance.</description>
</parameter>
</config-description>
</thing-type>
<!-- Channel types -->
<channel-type id="basic_actions_state">
<item-type>String</item-type>
<label>Program Actions</label>
<state>
<options>
<option value="start">Start program</option>
<option value="stop">Stop program</option>
</options>
</state>
</channel-type>
<channel-type id="local_control_active_state">
<item-type>Switch</item-type>
<label>Local Control State</label>
<description>This status indicates whether the home appliance is currently manually controlled by the user operating
the home appliance, e.g. opening the door or pressing a button.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="coffeemaker_drip_tray_full_state">
<item-type>Switch</item-type>
<label>Drip Tray Full</label>
<description>Is coffee maker drip tray full?</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="coffeemaker_water_tank_empty_state">
<item-type>Switch</item-type>
<label>Water Tank Empty</label>
<description>Is coffee maker water tank empty?</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="coffeemaker_bean_container_empty_state">
<item-type>Switch</item-type>
<label>Bean Container Empty</label>
<description>Is coffee maker bean container is empty?</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="door_state">
<item-type>Contact</item-type>
<label>Door State</label>
<description>This status describes the door state of a home appliance. A status change is either triggered by the user
operating the home appliance locally (i.e. opening/closing door) or automatically by the home appliance (i.e. locking
the door).</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="operation_state">
<item-type>String</item-type>
<label>Operation State</label>
<description>This status describes the operation state of the home appliance.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="remote_start_allowance_state">
<item-type>Switch</item-type>
<label>Remote Start Allowance State</label>
<description>This status indicates whether the remote program start is enabled. This can happen due to a programmatic
change (only disabling), or manually by the user changing the flag locally on the home appliance, or automatically
after a certain duration - usually 24 hours.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="remote_control_active_state">
<item-type>Switch</item-type>
<label>Remote Control Activation State</label>
<description>This status indicates whether the allowance for remote controlling is enabled.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="active_program_state">
<item-type>String</item-type>
<label>Active Program</label>
<description>This status describes the active program of the home appliance.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="selected_program_state">
<item-type>String</item-type>
<label>Selected Program</label>
<description>This state describes the selected program of the home appliance.</description>
</channel-type>
<channel-type id="remaining_program_time_state">
<item-type>Number:Time</item-type>
<label>Remaining Program Time</label>
<description>This status indicates the remaining program time of the home appliance.</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="program_progress_state">
<item-type>Number:Dimensionless</item-type>
<label>Progress State</label>
<description>This status describes the program progress of the home appliance.</description>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="oven_current_cavity_temperature">
<item-type>Number:Temperature</item-type>
<label>Cavity Temperature</label>
<description>This status describes the oven cavity temperature of the home appliance.</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="elapsed_program_time">
<item-type>Number:Time</item-type>
<label>Elapsed Program Time</label>
<description>This status describes the elapsed program time of the home appliance.</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="setpoint_temperature">
<item-type>Number:Temperature</item-type>
<label>Setpoint Temperature</label>
<description>This status describes the intended cooking compartment temperature of the home appliance.</description>
<state pattern="%.0f %unit%" step="1"/>
</channel-type>
<channel-type id="duration">
<item-type>Number:Time</item-type>
<label>Selected Duration</label>
<description>This status describes the duration of the program of the home appliance.</description>
<state pattern="%d %unit%" step="60" min="60"/>
</channel-type>
<channel-type id="laundry_care_washer_temperature">
<item-type>String</item-type>
<label>Washing Program Temperature</label>
<description>This status describes the temperature of the washing program of the home appliance.</description>
<state>
<options>
<option value="LaundryCare.Washer.EnumType.Temperature.Cold">Cold water</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC20">20 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC30">30 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC40">40 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC50">50 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC60">60 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC70">70 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC80">80 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.GC90">90 °C</option>
<option value="LaundryCare.Washer.EnumType.Temperature.Auto">Auto</option>
<option value="LaundryCare.Washer.EnumType.Temperature.UlCold">Cold (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.Temperature.UlWarm">Warm (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.Temperature.UlHot">Hot (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.Temperature.UlExtraHot">Extra hot (US/CA)</option>
</options>
</state>
</channel-type>
<channel-type id="laundry_care_washer_spin_speed">
<item-type>String</item-type>
<label>Spin Speed</label>
<description>This status defines the spin speed of a washer program of the home appliance.</description>
<state>
<options>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.Off">No spinning</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM400">400 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM600">600 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM800">800 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM1000">1000 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM1200">1200 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM1400">1400 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.RPM1600">1600 rpm</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.Auto">Auto</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.UlNo">No spinning (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.UlLow">Low (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.UlMedium">Medium (US/CA)</option>
<option value="LaundryCare.Washer.EnumType.SpinSpeed.UlHigh">High (US/CA)</option>
</options>
</state>
</channel-type>
<channel-type id="laundry_care_washer_idos1">
<item-type>String</item-type>
<label>i-Dos 1 Dosing Level</label>
<description>This status defines the i-Dos dosing level of a washer program of the home appliance. (If appliance
supports i-Dos)</description>
<state>
<options>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Off">Off</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Light">Light</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Normal">Normal</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Strong">Strong</option>
</options>
</state>
</channel-type>
<channel-type id="laundry_care_washer_idos2">
<item-type>String</item-type>
<label>i-Dos 2 Dosing Level</label>
<description>This status defines the i-Dos dosing level of a washer program of the home appliance. (If appliance
supports i-Dos)</description>
<state>
<options>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Off">Off</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Light">Light</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Normal">Normal</option>
<option value="LaundryCare.Washer.EnumType.IDosingLevel.Strong">Strong</option>
</options>
</state>
</channel-type>
<channel-type id="setpoint_temperature_refrigerator">
<item-type>Number:Temperature</item-type>
<label>Refrigerator Temperature</label>
<description>Target temperature of the refrigerator compartment (Range depends on appliance - common range 2 to 8°C).</description>
<state step="1" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="setpoint_temperature_freezer">
<item-type>Number:Temperature</item-type>
<label>Freezer temperature</label>
<description>Target temperature of the freezer compartment (Range depends on appliance - common range -16 to -24°C).</description>
<state step="1" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="super_mode_refrigerator">
<item-type>Switch</item-type>
<label>Refrigerator Super Mode</label>
<description>The setting has no impact on setpoint temperatures but will make the fridge compartment cool to the
lowest possible temperature until it is disabled manually by the customer or by the HA because of a timeout.</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="super_mode_freezer">
<item-type>Switch</item-type>
<label>Freezer Super Mode</label>
<description>This setting has no impact on setpoint temperatures but will make the freezer compartment cool to the
lowest possible temperature until it is disabled manually by the customer or by the home appliance because of a
timeout.</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="dryer_drying_target">
<item-type>String</item-type>
<label>Drying Target</label>
<description>Specifies the desired dryness setting.</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="hood_venting_level">
<item-type>String</item-type>
<label>Venting Level</label>
<description>Current venting level of the hood.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="hood_intensive_level">
<item-type>String</item-type>
<label>Intensive level</label>
<description>Current venting intensive level of the hood.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="hood_program_state">
<item-type>String</item-type>
<label>Program Actions</label>
<description>Start hood program.</description>
</channel-type>
<channel-type id="functional_light_state">
<item-type>Switch</item-type>
<label>Functional Light State</label>
<description>This setting describes the current functional light state of the home appliance.</description>
</channel-type>
<channel-type id="ambient_light_state">
<item-type>Switch</item-type>
<label>Ambient Light State</label>
<description>This setting describes the current ambient light state of the home appliance.</description>
</channel-type>
<channel-type id="ambient_light_color_state">
<item-type>String</item-type>
<label>Ambient Light Color</label>
<description>This setting describes the color state of the ambient light.</description>
<state readOnly="false">
<options>
<option value="BSH.Common.EnumType.AmbientLightColor.CustomColor">Custom Color</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color1">Color 1</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color2">Color 2</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color3">Color 3</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color4">Color 4</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color5">Color 5</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color6">Color 6</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color7">Color 7</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color8">Color 8</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color9">Color 9</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color10">Color 10</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color11">Color 11</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color12">Color 12</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color13">Color 13</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color14">Color 14</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color15">Color 15</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color16">Color 16</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color17">Color 17</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color18">Color 18</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color19">Color 19</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color20">Color 20</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color21">Color 21</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color22">Color 22</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color23">Color 23</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color24">Color 24</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color25">Color 25</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color26">Color 26</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color27">Color 27</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color28">Color 28</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color29">Color 29</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color30">Color 30</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color31">Color 31</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color32">Color 32</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color33">Color 33</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color34">Color 34</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color35">Color 35</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color36">Color 36</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color37">Color 37</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color38">Color 38</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color39">Color 39</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color40">Color 40</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color41">Color 41</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color42">Color 42</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color43">Color 43</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color44">Color 44</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color45">Color 45</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color46">Color 46</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color47">Color 47</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color48">Color 48</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color49">Color 49</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color50">Color 50</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color51">Color 51</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color52">Color 52</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color53">Color 53</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color54">Color 54</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color55">Color 55</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color56">Color 56</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color57">Color 57</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color58">Color 58</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color59">Color 59</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color60">Color 60</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color61">Color 61</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color62">Color 62</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color63">Color 63</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color64">Color 64</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color65">Color 65</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color66">Color 66</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color67">Color 67</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color68">Color 68</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color69">Color 69</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color70">Color 70</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color71">Color 71</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color72">Color 72</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color73">Color 73</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color74">Color 74</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color75">Color 75</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color76">Color 76</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color77">Color 77</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color78">Color 78</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color79">Color 79</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color80">Color 80</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color81">Color 81</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color82">Color 82</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color83">Color 83</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color84">Color 84</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color85">Color 85</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color86">Color 86</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color87">Color 87</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color88">Color 88</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color89">Color 89</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color90">Color 90</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color91">Color 91</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color92">Color 92</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color93">Color 93</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color94">Color 94</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color95">Color 95</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color96">Color 96</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color97">Color 97</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color98">Color 98</option>
<option value="BSH.Common.EnumType.AmbientLightColor.Color99">Color 99</option>
</options>
</state>
</channel-type>
<channel-type id="ambient_light_custom_color_state">
<item-type>Color</item-type>
<label>Ambient Light (Custom)</label>
<description>This setting describes the custom color state of the ambient light.</description>
<category>Colorpicker</category>
<tags>
<tag>Lighting</tag>
</tags>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,97 @@
body {
font-size: .875rem;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@supports ((position: -webkit-sticky) or (position: sticky)) {
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/homeconnect/assets/favicon/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1176 2309 c-360 -38 -677 -264 -831 -591 -65 -138 -87 -235 -92
-403 -4 -125 -1 -167 15 -249 12 -54 24 -101 27 -104 3 -3 35 23 71 59 l65 65
-12 74 c-24 154 9 357 82 503 159 316 487 506 838 484 379 -24 702 -297 791
-667 24 -102 27 -281 6 -382 -70 -330 -344 -604 -674 -674 -103 -21 -281 -18
-383 7 -154 39 -289 113 -403 223 l-59 57 -59 -58 -58 -57 43 -44 c218 -225
565 -345 872 -302 619 86 1023 682 871 1282 -84 330 -328 603 -643 718 -147
53 -318 75 -467 59z"/>
<path d="M816 1299 c-251 -254 -456 -466 -456 -472 0 -13 72 -137 80 -137 3 0
194 189 425 420 l420 420 321 -321 322 -322 22 43 c12 24 28 60 35 80 l13 37
-357 357 c-196 196 -359 356 -363 356 -3 0 -211 -208 -462 -461z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,218 @@
/* globals Chart:false, feather:false, Plotly:false, requests:false */
(function () {
'use strict'
feather.replace();
$(".redirectUri").text(window.location.href.substring(0, window.location.href.lastIndexOf('/homeconnect') + 12));
$('#apiDetailModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var thingId = button.data('thing-id');
var action = button.data('api-action');
var titleText = button.data('title');
var modal = $(this);
var title = modal.find('.modal-title');
var subTitle = modal.find('.modal-subtitle');
var responseBodyElement = modal.find('.modal-response-body');
responseBodyElement.text('Loading...');
title.text(titleText);
subTitle.text(thingId);
modal.modal('handleUpdate');
let jqxhr = $.get('appliances?thingId=' + thingId + '&action=' + action, function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
});
jqxhr.fail(function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
})
jqxhr.always(function () {
modal.modal('handleUpdate');
});
})
$('#rawCommandDetailModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var thingId = button.data('thing-id');
var haId = button.data('ha-id');
var modal = $(this);
var subTitle = modal.find('.modal-subtitle');
var inputPath = modal.find('#raw-path');
var inputBody = modal.find('#raw-request-body');
var submit = modal.find('#raw-submit');
var responseBodyElement = modal.find('.modal-response-body');
var responseTitle = modal.find('.raw-response-header');
subTitle.text(thingId);
responseBodyElement.text('');
responseTitle.hide();
inputPath.val('/api/homeappliances/' + haId + '/programs/active')
modal.modal('handleUpdate');
submit.click(function () {
responseBodyElement.text('Loading...');
let jqxhr = $.post('appliances?thingId=' + thingId + '&action=put-raw&path=' + inputPath.val(),
inputBody.val(), function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
responseTitle.show();
});
jqxhr.fail(function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
responseTitle.show();
})
jqxhr.always(function () {
modal.modal('handleUpdate');
});
});
})
$('#rawGetDetailModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var thingId = button.data('thing-id');
var haId = button.data('ha-id');
var modal = $(this);
var subTitle = modal.find('.modal-subtitle');
var inputPath = modal.find('#raw-get-path');
var submit = modal.find('#raw-get-submit');
var responseBodyElement = modal.find('.modal-response-body');
var responseTitle = modal.find('.raw-response-header');
subTitle.text(thingId);
responseBodyElement.text('');
responseTitle.hide();
inputPath.val('/api/homeappliances/' + haId + '/programs')
modal.modal('handleUpdate');
submit.click(function () {
responseBodyElement.text('Loading...');
let jqxhr = $.post('appliances?thingId=' + thingId + '&action=get-raw&path=' + inputPath.val(), function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
responseTitle.show();
});
jqxhr.fail(function (data) {
responseBodyElement.text(JSON.stringify(data, null, '\t'));
responseTitle.show();
})
jqxhr.always(function () {
modal.modal('handleUpdate');
});
});
})
$('#requestDetailModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var requestId = button.data('request-id');
var request = requests.find(item => item.id == requestId);
var requestHeader = request.homeConnectRequest.header;
var requestBody = request.homeConnectRequest.body;
var modal = $(this);
var requestBodyElement = modal.find('.modal-request-body');
var title = modal.find('.modal-title');
var responseBodyElement = modal.find('.modal-response-body');
var requestHeaderElement = modal.find('.modal-request-header');
var responseHeaderElement = modal.find('.modal-response-header');
title.text(request.homeConnectRequest.method + ' ' + request.homeConnectRequest.url);
if (requestBody) {
requestBodyElement.text(requestBody);
requestBodyElement.removeClass('text-muted')
} else {
requestBodyElement.text('Empty request body');
requestBodyElement.addClass('text-muted')
}
if (request.homeConnectResponse && request.homeConnectResponse.body) {
responseBodyElement.text(request.homeConnectResponse.body);
responseBodyElement.removeClass('text-muted')
} else {
responseBodyElement.text('Empty response body');
responseBodyElement.addClass('text-muted')
}
responseHeaderElement.empty();
if (request.homeConnectResponse && request.homeConnectResponse.header) {
var responseHeader = request.homeConnectResponse.header;
Object.keys(responseHeader).forEach(key => {
console.log(`key=${key} value=${responseHeader[key]}`);
responseHeaderElement.append($(`<dt class="col-sm-4">${key}</dt>`));
responseHeaderElement.append($(`<dd class="col-sm-8 text-break">${responseHeader[key]}</dd>`));
responseHeaderElement.append($('<div class="w-100"></div>'));
});
}
requestHeaderElement.empty();
Object.keys(requestHeader).forEach(key => {
console.log(`key=${key} value=${requestHeader[key]}`);
requestHeaderElement.append($(`<dt class="col-sm-4">${key}</dt>`));
requestHeaderElement.append($(`<dd class="col-sm-8 text-break">${requestHeader[key]}</dd>`));
requestHeaderElement.append($('<div class="w-100"></div>'));
});
modal.modal('handleUpdate');
})
$('.reload-page').click(function () {
location.reload();
});
$('.request-chart').each(function (index, element) {
var bridgeId = $(this).data('bridge-id');
var chartElement = element;
function makeplot (bridgeId, chartElement) {
Plotly.d3.csv('requests?bridgeId=' + bridgeId + '&action=request-csv', function (data) {
processData(data, chartElement)
});
}
function processData (allRows, chartElement) {
console.log(allRows);
var x = [], y = [], standardDeviation = [];
for (var i = 0; i < allRows.length; i++) {
var row = allRows[i];
x.push(row['time']);
y.push(row['requests']);
}
console.log('X', x, 'Y', y, 'SD', standardDeviation);
makePlotly(x, y, standardDeviation, chartElement);
}
function makePlotly (x, y, standard_deviation, chartElement){
var traces = [{
x: x,
y: y,
type: 'histogram',
histfunc: 'sum',
xbins: {
size: 1000
}
}];
Plotly.newPlot(chartElement, traces,
{
xaxis: {
rangemode: 'nonnegative',
autorange: true,
title: '',
type: 'date'
},
yaxis: {
title: 'requests',
rangemode: 'nonnegative'
}
},
{
displayModeBar: false,
responsive: true
});
}
makeplot(bridgeId, chartElement);
});
}())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="base :: head"></head>
<body>
<nav th:replace="base :: topNav"></nav>
<div class="container-fluid">
<div class="row">
<nav th:replace="base :: sidebarMenu (current='appliances')"></nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Appliances</h1>
</div>
<div th:if="${bridgeHandlers.size() == 0}">No Home Connect bridge found. Please manually add 'Home Connect API' bridge and authorize it here.</div>
<div th:each="bridgeHandler: ${bridgeHandlers}">
<div th:each="thingHandler: ${bridgeHandler.getThingHandler()}">
<div class="card mb-3" th:with="events=${bridgeHandler.getEventSourceClient().getLatestEvents(thingHandler.getThingHaId())}, thing=${thingHandler.getThing()}, status=${thingHandler.getThing().getStatus().toString()}, uid=${thingHandler.getThing().getUID().getAsString()}, haId=${thingHandler.getThingHaId()}">
<div class="card-body">
<section th:id="${uid}">
<h5 class="card-title" th:text="${thing.getLabel()}">Card title</h5>
</section>
<h6 class="card-subtitle mb-2 text-muted" th:text="${uid}">Card subtitle</h6>
<dl class="row">
<dt class="col-sm-2">Bridge</dt>
<dd class="col-sm-8" th:text="${bridgeHandler.getThing().getLabel() + ' (' + bridgeHandler.getThing().getUID().getAsString() + ')'}">1234567890</dd>
<div class="w-100"></div>
<dt class="col-sm-2">HaId</dt>
<dd class="col-sm-8" th:text="${haId}">1234567890</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Status</dt>
<dd class="col-sm-8">
<span class="badge" th:classappend="|${status == 'UNINITIALIZED' ? 'badge-warning' : ''} ${status == 'OFFLINE' ? 'badge-danger' : ''} ${status == 'ONLINE' ? 'badge-success' : ''}|" th:text="${status}">OFFLINE</span>
</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Accessible (SSE)</dt>
<dd class="col-sm-8">
<span class="badge" th:classappend="${thingHandler.isThingAccessibleViaServerSentEvents() ? 'badge-success' : 'badge-danger'}" th:text="${thingHandler.isThingAccessibleViaServerSentEvents() ? 'TRUE' : 'FALSE'}">OFFLINE</span>
</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Last Event received (SSE)</dt>
<dd class="col-sm-8" th:if="${events.size() > 0}" th:text="${#temporals.format(events.get(events.size()-1).creation, 'yyyy-MM-dd HH:mm:ss.SSS Z')}">1234567890</dd>
<dd class="col-sm-8" th:unless="${events.size() > 0}">unknown</dd>
<div class="w-100"></div>
<dt class="col-sm-2">API Actions</dt>
<dd class="col-sm-8">
<div class="mb-1">
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-primary btn-sm py-0" data-title="Appliance Details" data-api-action="show-details" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Appliance Details
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-primary btn-sm py-0" data-title="Operation State" data-api-action="operation-state" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Operation State
</button>
<div class="w-100"></div>
</div>
<div class="mb-1">
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-secondary btn-sm py-0" data-title="All Programs" data-api-action="all-programs" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
All Programs
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-secondary btn-sm py-0" data-title="Available Programs" data-api-action="available-programs" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Available Programs
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-secondary btn-sm py-0" data-title="Selected Program" data-api-action="selected-program" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Selected Program
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-secondary btn-sm py-0" data-title="Active Program" data-api-action="active-program" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Active Program
</button>
<div class="w-100"></div>
</div>
<div class="mb-1">
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-info btn-sm py-0" data-title="Power State" data-api-action="power-state" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Power State
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-info btn-sm py-0" data-title="Door State" data-api-action="door-state" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Door State
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-info btn-sm py-0" data-title="Remote Control Start Allowed" data-api-action="remote-control-start-allowed" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Remote Control Start Allowed
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-info btn-sm py-0" data-title="Remote Control Active" data-api-action="remote-control-active" th:attr="data-thing-id=${uid}" data-toggle="modal" data-target="#apiDetailModal">
Remote Control Active
</button>
</div>
<div>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-warning btn-sm py-0" data-title="Send Raw Command (PUT)" th:attr="data-thing-id=${uid}, data-ha-id=${haId}" data-toggle="modal" data-target="#rawCommandDetailModal">
Send Raw Command (PUT)
</button>
<button style="display: inline-block;font-size: 0.8em;" type="button" class="btn btn-warning btn-sm py-0" data-title="Send Raw Request (GET)" th:attr="data-thing-id=${uid}, data-ha-id=${haId}" data-toggle="modal" data-target="#rawGetDetailModal">
Send Raw Request (GET)
</button>
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- API action detail modal -->
<div class="modal fade" id="apiDetailModal" tabindex="-1" role="dialog" aria-labelledby="apiDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title text-truncate" id="apiDetailModalLabel">API</h5>
<h6 class="modal-subtitle mb-2 text-muted text-truncate">Card subtitle</h6>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<pre class="modal-response-body"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Send Raw Command modal -->
<div class="modal fade" id="rawCommandDetailModal" tabindex="-1" role="dialog" aria-labelledby="rawCommandDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title text-truncate" id="rawCommandDetailModalLabel">Send Raw Command</h5>
<h6 class="modal-subtitle mb-2 text-muted text-truncate">Card subtitle</h6>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="raw-path" class="col-form-label">Path:</label>
<input type="text" class="form-control" id="raw-path">
</div>
<div class="form-group">
<label for="raw-request-body" class="col-form-label">Request Body:</label>
<textarea cols=100 rows=12 class="form-control" id="raw-request-body"></textarea>
</div>
</form>
<label class="raw-response-header">Response:</label>
<pre class="modal-response-body"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="raw-submit">Submit Request</button>
</div>
</div>
</div>
</div>
<!-- Send Raw Request modal -->
<div class="modal fade" id="rawGetDetailModal" tabindex="-1" role="dialog" aria-labelledby="rawGetDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title text-truncate" id="rawGetDetailModalLabel">Send Request (GET)</h5>
<h6 class="modal-subtitle mb-2 text-muted text-truncate">Card subtitle</h6>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="raw-path" class="col-form-label">Path:</label>
<input type="text" class="form-control" id="raw-get-path">
</div>
</form>
<label class="raw-response-header">Response:</label>
<pre class="modal-response-body"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="raw-get-submit">Submit Request</button>
</div>
</div>
</div>
</div>
</div>
<!--/*/ <th:block th:include="base :: js">
</th:block> /*/-->
</body>
</html>

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Home Connect Console</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/homeconnect/asset/css/bootstrap.min.css}" rel="stylesheet">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" th:href="@{/homeconnect/asset/favicon/apple-touch-icon.png}">
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/homeconnect/asset/favicon/favicon-32x32.png}">
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/homeconnect/asset/favicon/favicon-16x16.png}">
<link rel="manifest" th:href="@{/homeconnect/asset/favicon/site.webmanifest}">
<link rel="mask-icon" th:href="@{/homeconnect/asset/favicon/safari-pinned-tab.svg}" color="#5bbad5">
<link rel="shortcut icon" th:href="@{/homeconnect/asset/favicon/favicon.ico}">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" th:content="@{/homeconnect/asset/favicon/browserconfig.xml}" content="browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Home Connect binding CSS -->
<link th:href="@{/homeconnect/asset/css/homeconnect.css}" rel="stylesheet">
</head>
<body>
<nav th:fragment="topNav" class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">Home Connect Console</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
<div class="container-fluid">
<div class="row">
<nav th:fragment="sidebarMenu (current)" th:assert="${!#strings.isEmpty(current)}" id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" th:href="@{/homeconnect}" href="#" th:classappend="${current == 'bridges' ? 'active' : ''}">
<span data-feather="link-2"></span>
Bridges
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/homeconnect/appliances}" href="#" th:classappend="${current == 'appliances' ? 'active' : ''}">
<span data-feather="grid"></span>
Appliances
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Reports</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" th:href="@{/homeconnect/log/requests}" href="#" th:classappend="${current == 'log-requests' ? 'active' : ''}">
<span data-feather="file-text"></span>
API Requests
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/homeconnect/log/events}" href="#" th:classappend="${current == 'log-events' ? 'active' : ''}">
<span data-feather="file-text"></span>
Events (SSE)
</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
</main>
</div>
</div>
<th:block th:fragment="js">
<script th:src="@{/homeconnect/asset/js/jquery-3.5.1.min.js}"></script>
<script th:src="@{/homeconnect/asset/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/homeconnect/asset/js/feather.min.js}"></script>
<script th:src="@{/homeconnect/asset/js/homeconnect.js}"></script>
</th:block>
</body>
</html>

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="base :: head"></head>
<body>
<nav th:replace="base :: topNav"></nav>
<div class="container-fluid">
<div class="row">
<nav th:replace="base :: sidebarMenu (current='bridges')"></nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Bridges</h1>
</div>
<div th:if="${bridgeHandlers.size() == 0}">No Home Connect bridge found. Please manually add 'Home Connect API' bridge and authorize it here.</div>
<div th:each="bridgeHandler: ${bridgeHandlers}">
<div class="card mb-3" th:with="thing=${bridgeHandler.getThing()}, status=${bridgeHandler.getThing().getStatus().toString()}, uid=${thing.getUID().getAsString()}">
<div class="card-body">
<section th:id="${uid}">
<h5 class="card-title" th:text="${thing.getLabel()}">Card title</h5>
</section>
<h6 class="card-subtitle mb-2 text-muted" th:text="${uid}">Card subtitle</h6>
<div th:if="${action == uid + 'clearCredentials'}" class="alert alert-success" role="alert">
Successfully cleared credentials.
</div>
<div th:if="${action == uid + 'authorize'}" class="alert alert-success" role="alert">
Successfully authorized bridge.
</div>
<dl class="row">
<dt class="col-sm-2">OAuth Flow</dt>
<dd class="col-sm-8">Authorization Code Grant Flow</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Client ID</dt>
<dd class="col-sm-8" th:text="${bridgeHandler.getConfiguration().getClientId()}">1234567890</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Client Secret</dt>
<dd class="col-sm-8" th:text="${bridgeHandler.getConfiguration().getClientSecret()}">xyz</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Redirect URI</dt>
<dd class="col-sm-8">
<span class="redirectUri">http://xy/homeconnect</span><br />
<small class="text-muted">Please make sure the redirect URI is matching the one specified at https://developer.home-connect.com/applications/.</small>
</dd>
<div class="w-100"></div>
<dt class="col-sm-2">API Endpoint</dt>
<dd class="col-sm-8" th:text="${bridgeHandler.getConfiguration().isSimulator() ? @org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants@API_SIMULATOR_BASE_URL : @org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants@API_BASE_URL}">xyz</dd>
<div class="w-100"></div>
<dt class="col-sm-2">SSE connections</dt>
<dd class="col-sm-8" th:text="${bridgeHandler.getEventSourceClient().connectionSize()}">xyz</dd>
<div class="w-100"></div>
<dt class="col-sm-2">Status</dt>
<dd class="col-sm-8">
<span class="badge" th:classappend="|${status == 'UNINITIALIZED' ? 'badge-warning' : ''} ${status == 'OFFLINE' ? 'badge-danger' : ''} ${status == 'ONLINE' ? 'badge-success' : ''}|" th:text="${status}">OFFLINE</span>
</dd>
<div class="w-100"></div>
<dt class="col-sm-2"></dt>
<dd class="col-sm-8">
<form method="post" style="display: inline-block">
<input type="hidden" name="bridgeId" th:value="${uid}"/>
<input type="hidden" name="action" value="clearCredentials">
<button type="submit" class="btn btn-secondary btn-sm">Clear stored credentials</button>
</form>
<form method="post" style="display: inline-block">
<input type="hidden" name="bridgeId" th:value="${uid}"/>
<input type="hidden" name="action" value="authorize">
<button type="submit" class="btn btn-primary btn-sm">Authorize bridge</button>
</form>
</dd>
</dl>
</div>
</div>
</div>
</main>
</div>
</div>
<!--/*/ <th:block th:include="base :: js">
</th:block> /*/-->
</body>
</html>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="base :: head"></head>
<body>
<nav th:replace="base :: topNav"></nav>
<div class="container-fluid">
<div class="row">
<nav th:replace="base :: sidebarMenu (current='log-events')"></nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Events</h1>
</div>
<div th:if="${bridgeHandlers.size() == 0}" class="mb-4"><span class="text-muted">No events</span></div>
<div class="mb-4" th:each="bridgeHandler: ${bridgeHandlers}" th:with="thing=${bridgeHandler.getThing()}, queue=${bridgeHandler.getEventSourceClient().getLatestEvents()}, status=${bridgeHandler.getThing().getStatus().toString()}, uid=${bridgeHandler.getThing().getUID().getAsString()}">
<h2 th:text="${thing.getLabel() + ' (' + uid + ')'}" style="display: inline-block;">Section title</h2>
<div class="btn-toolbar float-right">
<button type="button" class="btn btn-sm btn-outline-secondary reload-page mr-2"><span data-feather="refresh-cw"></span></button>
<a th:href="@{/homeconnect/log/events(export='json',bridgeId=${uid})}" class="btn btn-sm btn-outline-secondary">Export</a>
</div>
<div th:if="${queue.size() == 0}"><span class="text-muted">No events</span></div>
<div th:if="${queue.size() > 0}" class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Timestamp</th>
<th>HaId</th>
<th>Type</th>
<th>Level</th>
<th>Handling</th>
<th>URI</th>
<th>Name</th>
<th>Value</th>
<th>Unit</th>
</tr>
</thead>
<tbody>
<tr th:each="entry: ${queue}">
<td th:text="${#temporals.format(entry.creation, 'yyyy-MM-dd HH:mm:ss.SSS Z')}">27.04.2020 14:30</td>
<td><span th:text="${entry.haId}" class="badge badge-light">align-middle</span></td>
<td><span th:text="${entry.type}" class="badge badge-secondary"></span></td>
<td><span th:if="${entry.level}" th:text="${entry.level}" class="badge" th:classappend="|${entry.level.getLevel() == 'critical' || entry.level.getLevel() == 'alert' ? 'badge-danger' : ''} ${entry.level.getLevel() == 'info' || entry.level.getLevel() == 'hint' ? 'badge-success' : ''} ${entry.level.getLevel() == 'warning' ? 'badge-warning' : ''}|">hint</span></td>
<td><span class="badge badge-light" th:text="${entry.handling}">none</span></td>
<td class="text-break" th:text="${entry.uri}">/api/homeappliances/BOSCH-HNG6764B6-0000000011FF/programs/active/options/Cooking.Oven.Option.SetpointTemperature</td>
<td th:text="${entry.name}">Target temperature for the oven</td>
<td th:text="${entry.value}">200</td>
<td th:text="${entry.unit}">°C</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<!--/*/ <th:block th:include="base :: js">
</th:block> /*/-->
</body>
</html>

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="base :: head"></head>
<body>
<nav th:replace="base :: topNav"></nav>
<div class="container-fluid">
<div class="row">
<nav th:replace="base :: sidebarMenu (current='log-requests')"></nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">API Requests</h1>
</div>
<div th:if="${bridgeHandlers.size() == 0}" class="mb-4"><span class="text-muted">No requests</span></div>
<div class="mb-4" th:each="bridgeHandler: ${bridgeHandlers}" th:with="thing=${bridgeHandler.getThing()}, queue=${bridgeHandler.getApiClient().getLatestApiRequests()}, status=${bridgeHandler.getThing().getStatus().toString()}, uid=${bridgeHandler.getThing().getUID().getAsString()}">
<h2 th:text="${thing.getLabel() + ' (' + uid + ')'}" style="display: inline-block;">Section title</h2>
<div class="btn-toolbar float-right">
<button type="button" class="btn btn-sm btn-outline-secondary reload-page mr-2"><span data-feather="refresh-cw"></span></button>
<a th:href="@{/homeconnect/log/requests(export='json',bridgeId=${uid})}" class="btn btn-sm btn-outline-secondary">Export</a>
</div>
<div th:if="${queue.size() == 0}"><span class="text-muted">No requests</span></div>
<div th:unless="${queue.size() == 0}" class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Request Time</th>
<th class="text-center">Response Code</th>
<th class="text-center">Method</th>
<th>URL</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="entry: ${queue}">
<td th:text="${#temporals.format(entry.time, 'yyyy-MM-dd HH:mm:ss.SSS Z')}">27.04.2020 14:30</td>
<td class="text-center"><span th:if="${entry.response}" class="badge" th:classappend="|${entry.response.code >= 300 && entry.response.code != 404 ? 'badge-danger' : ''} ${entry.response.code >=200 && entry.response.code < 300 ? 'badge-success' : ''} ${entry.response.code == 404 ? 'badge-warning' : ''}|" th:text="${entry.response.code}">200</span></td>
<td class="text-center" th:text="${entry.request.method}">GET</td>
<td th:text="${entry.request.getShortUrl()}">/bla/bla?w92374=34</td>
<td class="text-center">
<button type="button" class="btn btn-info btn-sm py-0" style="font-size: 0.8em;" th:attr="data-request-id=${entry.id}" data-toggle="modal" data-target="#requestDetailModal">
Details
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<!-- Request detail modal -->
<div class="modal fade" id="requestDetailModal" tabindex="-1" role="dialog" aria-labelledby="requestDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title .text-truncate" id="requestDetailModalLabel">Request Details</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h5>Request Body</h5>
<pre class="modal-request-body"></pre>
<hr>
<h5>Response Body</h5>
<pre class="modal-response-body"></pre>
<hr>
<h5>Response Header</h5>
<dl class="row modal-response-header"></dl>
<hr>
<h5>Request Header</h5>
<dl class="row modal-request-header"></dl>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:inline="javascript">
var requests = JSON.parse(/*[[${requests}]]*/);
</script>
</div>
<!--/*/ <th:block th:include="base :: js">
</th:block> /*/-->
</body>
</html>

View File

@ -141,6 +141,7 @@
<module>org.openhab.binding.helios</module>
<module>org.openhab.binding.heliosventilation</module>
<module>org.openhab.binding.heos</module>
<module>org.openhab.binding.homeconnect</module>
<module>org.openhab.binding.homematic</module>
<module>org.openhab.binding.homewizard</module>
<module>org.openhab.binding.hpprinter</module>