[mielecloud] Initial contribution of the Miele Cloud binding (#9146)

Also-by: Bert Plonus <bert.plonus@miele.com>
Also-by: Martin Lepsy <martin.lepsy@miele.com>
Also-by: Benjamin Bolte <benjamin.bolte@itemis.de>
Signed-off-by: Björn Lange <bjoern.lange@itemis.de>
This commit is contained in:
Björn Lange 2021-05-25 22:06:49 +02:00 committed by GitHub
parent 5ec535f37b
commit 705f5c577c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
230 changed files with 61858 additions and 0 deletions

View File

@ -165,6 +165,7 @@
/bundles/org.openhab.binding.meteoblue/ @9037568
/bundles/org.openhab.binding.meteostick/ @cdjackson
/bundles/org.openhab.binding.miele/ @kgoderis
/bundles/org.openhab.binding.mielecloud/ @BjoernLange
/bundles/org.openhab.binding.mihome/ @pboos
/bundles/org.openhab.binding.miio/ @marcelrv
/bundles/org.openhab.binding.milight/ @davidgraeff

View File

@ -811,6 +811,11 @@
<artifactId>org.openhab.binding.miele</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mielecloud</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mihome</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,623 @@
# Miele Cloud Binding
This binding integrates [Miele@home](https://www.miele.de/brand/smarthome-42801.htm) appliances via a cloud connection.
A Miele cloud account and a set of developer credentials is required to use the binding.
The latter can be requested from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
## Supported Things
Most Miele appliances that directly connect to the cloud via a Wi-Fi module are supported.
Appliances connecting to the XGW3000 gateway via ZigBee are also supported when registered with the cloud account.
However they might be better supported by the [gateway-based Miele binding](https://www.openhab.org/addons/bindings/miele/).
Depending on the age of your appliance the functionality of the binding might be limited.
Appliances from recent generations will support all functionality.
The following types of appliances are supported:
| Appliance type | Thing type |
| -------------------------------- | ------------------------ |
| Coffee Machine | `coffee_system` |
| Dishwasher | `dishwasher` |
| Dish Warmer | `dish_warmer` |
| Freezer | `freezer` |
| Fridge | `fridge` |
| Fridge-Freezer Combination | `fridge_freezer` |
| Hob | `hob` |
| Hood | `hood` |
| Microwave Oven | `oven` |
| Oven | `oven` |
| Robotic Vacuum Cleaner | `robotic_vacuum_cleaner` |
| Tumble Dryer | `dryer` |
| Washer Dryer | `washer_dryer` |
| Washing Machine | `washing_machine` |
| Wine Cabinet | `wine_storage` |
| Wine Cabinet Freezer Combination | `wine_storage` |
## Discovery
Please take the following steps prior to using the binding. Create a Miele cloud account in the Miele@mobile app for [Android](https://play.google.com/store/apps/details?id=de.miele.infocontrol&hl=en_US) or [iOS](https://apps.apple.com/de/app/miele-mobile/id930406907?l=en) (if not already done).
Afterwards, pair your appliances.
Once your appliances are set up, register at the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
You will receive a pair of client ID and client secret which will be used to pair your Miele cloud account to the Miele cloud openHAB binding.
Keep these credentials to yourself and treat them like a password!
It may take some time until the registration e-mail arrives.
There is no auto discovery for the Miele cloud account.
The account is paired using OAuth2 with your Miele login and the developer credentials obtained from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
To pair the account go to the binding's configuration UI at `https://<your openHAB address>/mielecloud`.
For a standard openHABian Pi installation the address is [https://openhabianpi:8443/mielecloud](https://openhabianpi:8443/mielecloud).
Note that your browser will file a warning that the certificate is self-signed.
This is fine and you can safely continue.
It is also possible to use an unsecured connection for pairing but it is strongly recommended to use a secured connection because your credentials will otherwise be transferred without encryption over the local network.
For more information on this topic, see [Securing access to openHAB](https://www.openhab.org/docs/installation/security.html#encrypted-communication).
For a detailed walk through the account configuration, see [Account Configuration Example](#account-configuration-example).
Once a Miele account is paired, all supported appliances are automatically discovered as individual things and placed in the inbox.
They can then be paired with your favorite management UI.
As an alternative, the binding configuration UI provides a things-file template per paired account that can be used to pair the appliances.
## Thing Configuration
A Miele cloud account needs to be configured to get access to your appliances.
After that appliances can be configured.
### Account Configuration
The Miele cloud account must be paired via the binding configuration UI before a bridge that relies on it can be configured in openHAB.
For details on the configuration UI see [Discovery](#discovery) and [Account Configuration Example](#account-configuration-example).
The account serves as a bridge for the things representing your appliances.
On success the configuration assistant will directly configure the account without requiring further actions.
As an alternative, it provides a things-file template.
The account has the following parameters:
| Name | Type | Description |
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| email | required | E-mail address identifying this account. This exists only to distinguish accounts. If the address is changed after authorization then the account needs to be authorized again. |
| locale | optional | The locale to use for full text channels of things from this account. Possible values are `en`, `de`, `da`, `es`, `fr`, `it`, `nl`, `nb`. Default is `en`. |
### Appliance Configuration
The binding configuration UI will show a things-file template containing things for all supported appliances from the paired account.
This can be used as a starting point for a custom things-file.
All Miele cloud appliance things have the following parameters:
| Name | Type | Description |
| ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| deviceIdentifier | required | Technical device identifier uniquely identifying the Miele appliance. Use the discovery result or the things-file template to obtain it. |
## Channels
The following table lists all available channels.
See the following chapters for detailed information about which appliance supports which channels.
Depending on the exact appliance configuration not all channels might be supported, e.g. a hob with four plates will only fill the channels for plates 1-4.
Channel ID and channel type ID match unless noted.
| Channel Type ID | Item Type | Description | Read only |
| ----------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| remote_control_can_be_started | Switch | Indicates if this device can be started remotely. | Yes |
| remote_control_can_be_stopped | Switch | Indicates if this device can be stopped remotely. | Yes |
| remote_control_can_be_paused | Switch | Indicates if this device can be paused remotely. | Yes |
| remote_control_can_be_switched_on | Switch | Indicates if the device can be switched on remotely. | Yes |
| remote_control_can_be_switched_off | Switch | Indicates if the device can be switched off remotely. | Yes |
| remote_control_can_set_program_active | Switch | Indicates if the active program of the device can be set remotely. | Yes |
| spinning_speed | String | The spinning speed of the active program. | Yes |
| spinning_speed_raw | Number | The raw spinning speed of the active program. | Yes |
| program_active | String | The active program of the device. | Yes |
| program_active_raw | Number | The raw active program of the device. | Yes |
| dish_warmer_program_active | String | The active program of the device. | No |
| vacuum_cleaner_program_active | String | The active program of the device. | No |
| program_phase | String | The phase of the active program. | Yes |
| program_phase_raw | Number | The raw phase of the active program. | Yes |
| operation_state | String | The operation state of the device. | Yes |
| operation_state_raw | Number | The raw operation state of the device. | Yes |
| program_start | Switch | Starts the currently selected program. | No |
| program_stop | Switch | Stops the currently selected program. | No |
| program_start_stop | String | Starts or stops the currently selected program. | No |
| program_start_stop_pause | String | Starts, stops or pauses the currently selected program. | No |
| power_state_on_off | String | Switches the device On or Off. | No |
| finish_state | Switch | Indicates whether the most recent program finished. | Yes |
| delayed_start_time | Number | The delayed start time of the selected program. | Yes |
| program_remaining_time | Number | The remaining time of the active program. | Yes |
| program_elapsed_time | Number | The elapsed time of the active program. | Yes |
| program_progress | Number | The progress of the active program. | Yes |
| drying_target | String | The target drying step of the laundry. | Yes |
| drying_target_raw | Number | The raw target drying step of the laundry. | Yes |
| pre_heat_finished | Switch | Indicates whether the pre-heating finished. | Yes |
| temperature_target | Number | The target temperature of the device. | Yes |
| temperature_current | Number | The currently measured temperature of the device. | Yes |
| ventilation_power | String | The current ventilation power of the hood. | Yes |
| ventilation_power_raw | Number | The current raw ventilation power of the hood. | Yes |
| error_state | Switch | Indication flag which signals an error state for the device. | Yes |
| info_state | Switch | Indication flag which signals an information of the device. | Yes |
| fridge_super_cool | Switch | Start the super cooling mode of the fridge. | No |
| freezer_super_freeze | Switch | Start the super freezing mode of the freezer. | No |
| super_cool_can_be_controlled | Switch | Indicates if super cooling can be toggled. | Yes |
| super_freeze_can_be_controlled | Switch | Indicates if super freezing can be toggled | Yes |
| fridge_temperature_target | Number | The target temperature of the fridge. | Yes |
| fridge_temperature_current | Number | The currently measured temperature of the fridge. | Yes |
| freezer_temperature_target | Number | The target temperature of the freezer. | Yes |
| freezer_temperature_current | Number | The currently measured temperature of the freezer. | Yes |
| top_temperature_target | Number | The target temperature of the top area. | Yes |
| top_temperature_current | Number | The currently measured temperature of the top area. | Yes |
| middle_temperature_target | Number | The target temperature of the middle area. | Yes |
| middle_temperature_current | Number | The currently measured temperature of the middle area. | Yes |
| bottom_temperature_target | Number | The target temperature of the bottom area. | Yes |
| bottom_temperature_current | Number | The currently measured temperature of the bottom area. | Yes |
| light_switch | Switch | Indicates if the light of the device is enabled. | No |
| light_can_be_controlled | Switch | Indicates if the light of the device can be controlled. | Yes |
| plate_power_step | String | The power level of the heating plate. | Yes |
| plate_power_step_raw | Number | The raw power level of the heating plate. | Yes |
| door_state | Switch | Indicates if the door of the device is open. | Yes |
| door_alarm | Switch | Indicates if the door alarm of the device is active. | Yes |
| battery_level | Number | The battery level of the robotic vacuum cleaner. | Yes |
### Coffee System
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- finish_state
- power_state_on_off
- program_remaining_time
- program_elapsed_time
- error_state
- info_state
- light_switch
- light_can_be_controlled
### Dish Warmer
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- dish_warmer_program_active
- program_active_raw
- operation_state
- operation_state_raw
- power_state_on_off
- finish_state
- program_remaining_time
- program_elapsed_time
- program_progress
- temperature_target
- temperature_current
- error_state
- info_state
- door_state
### Dishwasher
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- program_start_stop
- finish_state
- power_state_on_off
- delayed_start_time
- program_remaining_time
- program_elapsed_time
- program_progress
- error_state
- info_state
- door_state
### Tumble Dryer
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- program_start_stop
- finish_state
- power_state_on_off
- delayed_start_time
- program_remaining_time
- program_elapsed_time
- program_progress
- drying_target
- drying_target_raw
- error_state
- info_state
- light_switch
- light_can_be_controlled
- door_state
### Freezer
- operation_state
- operation_state_raw
- error_state
- info_state
- freezer_super_freeze
- super_freeze_can_be_controlled
- freezer_temperature_target
- freezer_temperature_current
- door_state
- door_alarm
### Fridge
- operation_state
- operation_state_raw
- error_state
- info_state
- fridge_super_cool
- super_cool_can_be_controlled
- fridge_temperature_target
- fridge_temperature_current
- door_state
- door_alarm
### Fridge Freezer
- operation_state
- operation_state_raw
- error_state
- info_state
- fridge_super_cool
- freezer_super_freeze
- super_cool_can_be_controlled
- super_freeze_can_be_controlled
- fridge_temperature_target
- fridge_temperature_current
- freezer_temperature_target
- freezer_temperature_current
- door_state
- door_alarm
### Hob
- operation_state
- operation_state_raw
- error_state
- info_state
- plate_1_power_step to plate_6_power_step with channel type ID plate_power_step
- plate_1_power_step_raw to plate_6_power_step_raw with channel type ID plate_power_step_raw
### Hood
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- power_state_on_off
- ventilation_power
- ventilation_power_raw
- error_state
- info_state
- light_switch
- light_can_be_controlled
### Oven
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- program_start_stop
- finish_state
- power_state_on_off
- delayed_start_time
- program_remaining_time
- program_elapsed_time
- program_progress
- pre_heat_finished
- temperature_target
- temperature_current
- error_state
- info_state
- light_switch
- light_can_be_controlled
- door_state
### Robotic Vacuum Cleaner
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_paused
- remote_control_can_set_program_active
- vacuum_cleaner_program_active
- program_active_raw
- operation_state
- operation_state_raw
- finish_state
- program_start_stop_pause
- power_state_on_off
- error_state
- info_state
- battery_level
### Washer Dryer
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- spinning_speed
- spinning_speed_raw
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- program_start_stop
- finish_state
- power_state_on_off
- delayed_start_time
- program_remaining_time
- program_elapsed_time
- program_progress
- drying_target
- drying_target_raw
- error_state
- info_state
- temperature_target
- light_switch
- light_can_be_controlled
- door_state
### Washing Machine
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- spinning_speed
- spinning_speed_raw
- program_active
- program_active_raw
- program_phase
- program_phase_raw
- operation_state
- operation_state_raw
- program_start_stop
- finish_state
- power_state_on_off
- delayed_start_time
- program_remaining_time
- program_elapsed_time
- program_progress
- error_state
- info_state
- temperature_target
- light_switch
- light_can_be_controlled
- door_state
### Wine Storage
- remote_control_can_be_started
- remote_control_can_be_stopped
- remote_control_can_be_switched_on
- remote_control_can_be_switched_off
- operation_state
- operation_state_raw
- power_state_on_off
- error_state
- info_state
- temperature_target
- temperature_current
- top_temperature_target
- top_temperature_current
- middle_temperature_target
- middle_temperature_current
- bottom_temperature_target
- bottom_temperature_current
### Note on plate_power_step channels
Hob things have an additional property `plateCount` that indicates the number of plates present on the appliance.
Only the channels `plate_1_power_step` to `plate_x_power_step` will be populated by the binding where `x` is the value of the `plateCount` property.
The plate numbers do not represent the physical layout of the plates on the appliance, but always start with the `plate_1_power_step` channel.
This means that a hob with two plates will have `plate_1_power_step` and `plate_2_power_step` populated and all other `plate_x_power_step` channels empty.
The `plate_x_power_step` channels show the current power step of the according plate.
**Please note that some hobs may use dynamic numbering for plates.**
Hobs that use dynamic numbering will use the first power step channel that is currently at a power step of zero when the plate is turned on.
Additionally, when a plate is turned off all other plates with higher numbers will decrease their number by one.
For example if plate 1, 2 and 3 are active and plate 1 is turned off then plate 2 will become plate 1, plate 3 will become plate 2 and plate 3 will have a power step of zero.
This behavior is a fixed part of the affected appliances and cannot be changed.
### Note on door_state channel
The `door_state` channel might not always provide a value matching the actual state.
For example, a washing machine will not provide a valid `door_state` when the appliance is turned off.
A valid door state can be expected when the appliance is in one of the following raw operation states, compare the `operation_state_raw` channel:
- `3`: Program selected
- `4`: Program selected, waiting to start
- `5`: Running
- `6`: Paused
## Properties
The following chapters list the properties offered by appliances.
### Common Properties
| Property Name | Description |
| ------------- | ----------------------------------------------------------------------------- |
| serialNumber | Serial number of the appliance, only present for physical appliances |
| modelId | Model ID of the appliance |
| vendor | Always "Miele" |
### Account
| Property Name | Description |
| ------------- | ----------------------------------------------------------------------------- |
| connection | Type of connection used by the account, always "INTERNET" |
| accessToken | The currently used OAuth 2 access token for accessing the Miele 3rd Party API |
### Hob
| Property Name | Description |
| ------------- | ----------------------------------------------------------------------------- |
| plateCount | Number of plates offered by the appliance |
## Full Example
### demo.things:
```
Bridge mielecloud:account:home [ email="me@openhab.org", locale="en" ] {
Thing coffee_system 000703261234 "Coffee machine CVA7440" [ deviceIdentifier="000703261234" ]
Thing hob 000160102345 "Cooktop KM7677" [ deviceIdentifier="000160102345" ]
}
```
### demo.items:
```
// Coffee system
Switch coffee_system_remote_control_can_be_started { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_started" }
Switch coffee_system_remote_control_can_be_stopped { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_stopped" }
Switch coffee_system_remote_control_can_be_switched_on { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_on" }
Switch coffee_system_remote_control_can_be_switched_off { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_off" }
String coffee_system_program_active { channel="mielecloud:coffee_system:home:000703261234:program_active" }
String coffee_system_program_phase { channel="mielecloud:coffee_system:home:000703261234:program_phase" }
String coffee_system_power_state_on_off { channel="mielecloud:coffee_system:home:000703261234:power_state_on_off" }
String coffee_system_operation_state { channel="mielecloud:coffee_system:home:000703261234:operation_state" }
Switch coffee_system_finish_state { channel="mielecloud:coffee_system:home:000703261234:finish_state" }
Number coffee_system_program_remaining_time { channel="mielecloud:coffee_system:home:000703261234:program_remaining_time" }
Switch coffee_system_error_state { channel="mielecloud:coffee_system:home:000703261234:error_state" }
Switch coffee_system_info_state { channel="mielecloud:coffee_system:home:000703261234:info_state" }
Switch coffee_system_light_switch { channel="mielecloud:coffee_system:home:000703261234:light_switch" }
Switch coffee_system_light_can_be_controlled { channel="mielecloud:coffee_system:home:000703261234:light_can_be_controlled" }
// Hob
Switch hob_remote_control_can_be_started { channel="mielecloud:hob:home:000160102345:remote_control_can_be_started" }
Switch hob_remote_control_can_be_stopped { channel="mielecloud:hob:home:000160102345:remote_control_can_be_stopped" }
String hob_operation_state { channel="mielecloud:hob:home:000160102345:operation_state" }
Switch hob_error_state { channel="mielecloud:hob:home:000160102345:error_state" }
Switch hob_info_state { channel="mielecloud:hob:home:000160102345:info_state" }
Switch hob_plate_1_is_present { channel="mielecloud:hob:home:000160102345:plate_1_is_present" }
String hob_plate_1_power_step { channel="mielecloud:hob:home:000160102345:plate_1_power_step" }
Switch hob_plate_2_is_present { channel="mielecloud:hob:home:000160102345:plate_2_is_present" }
String hob_plate_2_power_step { channel="mielecloud:hob:home:000160102345:plate_2_power_step" }
Switch hob_plate_3_is_present { channel="mielecloud:hob:home:000160102345:plate_3_is_present" }
String hob_plate_3_power_step { channel="mielecloud:hob:home:000160102345:plate_3_power_step" }
Switch hob_plate_4_is_present { channel="mielecloud:hob:home:000160102345:plate_4_is_present" }
String hob_plate_4_power_step { channel="mielecloud:hob:home:000160102345:plate_4_power_step" }
Switch hob_plate_5_is_present { channel="mielecloud:hob:home:000160102345:plate_5_is_present" }
String hob_plate_5_power_step { channel="mielecloud:hob:home:000160102345:plate_5_power_step" }
Switch hob_plate_6_is_present { channel="mielecloud:hob:home:000160102345:plate_6_is_present" }
String hob_plate_6_power_step { channel="mielecloud:hob:home:000160102345:plate_6_power_step" }
```
### demo.sitemap:
```
sitemap demo label="Kitchen"
{
Frame {
// Coffee system
Text item=coffee_system_program_active
Text item=coffee_system_program_phase
Text item=coffee_system_power_state_on_off
Text item=coffee_system_operation_state
Switch item=coffee_system_finish_state
Default item=coffee_system_program_remaining_time
Switch item=coffee_system_error_state
Switch item=coffee_system_info_state
Switch item=coffee_system_light_switch
// Hob
Text item=hob_operation_state
Switch item=hob_error_state
Switch item=hob_info_state
Text item=hob_plate_1_power_step
Text item=hob_plate_2_power_step
Text item=hob_plate_3_power_step
Text item=hob_plate_4_power_step
Text item=hob_plate_5_power_step
Text item=hob_plate_6_power_step
}
}
```
## Account Configuration Example
The configuration UI is accessible at `https://<your openHAB address>/mielecloud`.
See [Discovery](#discovery) for a detailed description of how to open the configuration UI in a browser.
When first opening the configuration UI no account will be paired.
![Empty Account Overview](doc/account-overview-empty.png)
We strongly recommend to use a secure connection for pairing, details on this topic can also be found in the [Discovery](#discovery) section.
Click `Pair Account` to start the pairing process.
If not already done, go to the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx), register there and wait for the confirmation e-mail.
Obtain your client ID and client secret according to the instructions presented there.
Once you obtained your client ID and client secret continue pairing by filling in your client ID, client secret, bridge ID and an e-mail address that you wish to use for identifying the account.
You may choose any bridge ID you like as long as you only use letters, numbers, underscores and dashes.
The e-mail address does not need to match the e-mail address used for your Miele Cloud Account.
If you need to change the e-mail address later then you will need to authorize the account again.
![Pair Account](doc/pair-account.png)
A click on `Pair Account` will take you to the Miele cloud service login form where you need to log in with the same account as you used for the Miele@mobile app.
![Miele Login Form](doc/miele-login.png)
When this is the first time you pair an account, you will need to allow openHAB to access your account.
When everything worked, you are presented with a page stating that pairing was successful.
Select the locale which should be used to display localized texts in openHAB channels.
From here, you have two options:
Either let the binding automatically configure a bridge instance or copy the presented things-file template to a things-file and return to the overview page.
![Pairing Successful](doc/pairing-success.png)
Once the bridge instance is `ONLINE`, you can either pair things for all appliances via your favorite management UI or use a things-file.
The account overview provides a things-file template that is shown when you expand the account.
This can serve as a starting point for your own things-file.
![Account Overview With Bridge](doc/account-overview-with-bridge.png)
## Rule Ideas
Here are some ideas on what could be done with this binding. You have more ideas or even an example? Great! Feel free to contribute!
- Notify yourself of a finished dishwasher, tumble dryer, washer dryer or washing machine, e.g. by changing the lighting
- Control the supercooler / superfreezer of your freezer, fridge or fridge-freezer combination with a voice assistant
- Notify yourself when the oven has finished pre-heating
## Acknowledgements
The development of this binding was initiated and sponsored by Miele & Cie. KG.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,17 @@
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.mielecloud</artifactId>
<name>openHAB Add-ons :: Bundles :: Miele Cloud Binding</name>
</project>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2010-2020 Contributors to the openHAB project
See the NOTICE file(s) distributed with this work for additional
information.
This program and the accompanying materials are made available under the
terms of the Eclipse Public License 2.0 which is available at
http://www.eclipse.org/legal/epl-2.0
SPDX-License-Identifier: EPL-2.0
-->
<features name="org.openhab.binding.mielecloud-${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-mielecloud" description="Miele Cloud Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mielecloud/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,238 @@
/**
* 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.mielecloud.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MieleCloudBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Added locale config parameter, added i18n key collection
* @author Benjamin Bolte - Add pre-heat finished and plate step channels, door state and door alarm channels, info
* state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel, dish warmer thing
*/
@NonNullByDefault
public final class MieleCloudBindingConstants {
private MieleCloudBindingConstants() {
}
/**
* ID of the binding.
*/
public static final String BINDING_ID = "mielecloud";
/**
* Thing type ID of Miele cloud bridges / accounts.
*/
public static final String BRIDGE_TYPE_ID = "account";
/**
* The {@link ThingTypeUID} of Miele cloud bridges / accounts.
*/
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE_TYPE_ID);
/**
* The {@link ThingTypeUID} of Miele washing machines.
*/
public static final ThingTypeUID THING_TYPE_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, "washing_machine");
/**
* The {@link ThingTypeUID} of Miele washer-dryers.
*/
public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washer_dryer");
/**
* The {@link ThingTypeUID} of Miele coffee machines.
*/
public static final ThingTypeUID THING_TYPE_COFFEE_SYSTEM = new ThingTypeUID(BINDING_ID, "coffee_system");
/**
* The {@link ThingTypeUID} of Miele fridge-freezers.
*/
public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridge_freezer");
/**
* The {@link ThingTypeUID} of Miele fridges.
*/
public static final ThingTypeUID THING_TYPE_FRIDGE = new ThingTypeUID(BINDING_ID, "fridge");
/**
* The {@link ThingTypeUID} of Miele freezers.
*/
public static final ThingTypeUID THING_TYPE_FREEZER = new ThingTypeUID(BINDING_ID, "freezer");
/**
* The {@link ThingTypeUID} of Miele ovens.
*/
public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven");
/**
* The {@link ThingTypeUID} of Miele hobs.
*/
public static final ThingTypeUID THING_TYPE_HOB = new ThingTypeUID(BINDING_ID, "hob");
/**
* The {@link ThingTypeUID} of Miele wine storages.
*/
public static final ThingTypeUID THING_TYPE_WINE_STORAGE = new ThingTypeUID(BINDING_ID, "wine_storage");
/**
* The {@link ThingTypeUID} of Miele dishwashers.
*/
public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher");
/**
* The {@link ThingTypeUID} of Miele dryers.
*/
public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer");
/**
* The {@link ThingTypeUID} of Miele hoods.
*/
public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood");
/**
* The {@link ThingTypeUID} of Miele dish warmers.
*/
public static final ThingTypeUID THING_TYPE_DISH_WARMER = new ThingTypeUID(BINDING_ID, "dish_warmer");
/**
* The {@link ThingTypeUID} of Miele robotic vacuum cleaners.
*/
public static final ThingTypeUID THING_TYPE_ROBOTIC_VACUUM_CLEANER = new ThingTypeUID(BINDING_ID,
"robotic_vacuum_cleaner");
/**
* Name of the property storing the OAuth2 access token.
*/
public static final String PROPERTY_ACCESS_TOKEN = "accessToken";
/**
* Name of the configuration parameter for the e-mail address.
*/
public static final String CONFIG_PARAM_EMAIL = "email";
/**
* Name of the configuration parameter for the device identifier uniquely identifying a Miele device.
*/
public static final String CONFIG_PARAM_DEVICE_IDENTIFIER = "deviceIdentifier";
/**
* Name of the configuration parameter for the locale. The locale is stored as a 2-letter language code.
*/
public static final String CONFIG_PARAM_LOCALE = "locale";
/**
* Name of the property storing the number of plates for hobs.
*/
public static final String PROPERTY_PLATE_COUNT = "plateCount";
/**
* Constants for all channels.
*/
public static final class Channels {
private Channels() {
}
public static final String REMOTE_CONTROL_CAN_BE_STARTED = "remote_control_can_be_started";
public static final String REMOTE_CONTROL_CAN_BE_STOPPED = "remote_control_can_be_stopped";
public static final String REMOTE_CONTROL_CAN_BE_PAUSED = "remote_control_can_be_paused";
public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_ON = "remote_control_can_be_switched_on";
public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_OFF = "remote_control_can_be_switched_off";
public static final String REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE = "remote_control_can_set_program_active";
public static final String SPINNING_SPEED = "spinning_speed";
public static final String SPINNING_SPEED_RAW = "spinning_speed_raw";
public static final String PROGRAM_ACTIVE = "program_active";
public static final String PROGRAM_ACTIVE_RAW = "program_active_raw";
public static final String DISH_WARMER_PROGRAM_ACTIVE = "dish_warmer_program_active";
public static final String VACUUM_CLEANER_PROGRAM_ACTIVE = "vacuum_cleaner_program_active";
public static final String PROGRAM_PHASE = "program_phase";
public static final String PROGRAM_PHASE_RAW = "program_phase_raw";
public static final String OPERATION_STATE = "operation_state";
public static final String OPERATION_STATE_RAW = "operation_state_raw";
public static final String PROGRAM_START_STOP = "program_start_stop";
public static final String PROGRAM_START_STOP_PAUSE = "program_start_stop_pause";
public static final String POWER_ON_OFF = "power_state_on_off";
public static final String FINISH_STATE = "finish_state";
public static final String DELAYED_START_TIME = "delayed_start_time";
public static final String PROGRAM_REMAINING_TIME = "program_remaining_time";
public static final String PROGRAM_ELAPSED_TIME = "program_elapsed_time";
public static final String PROGRAM_PROGRESS = "program_progress";
public static final String DRYING_TARGET = "drying_target";
public static final String DRYING_TARGET_RAW = "drying_target_raw";
public static final String PRE_HEAT_FINISHED = "pre_heat_finished";
public static final String TEMPERATURE_TARGET = "temperature_target";
public static final String TEMPERATURE_CURRENT = "temperature_current";
public static final String TEMPERATURE_CORE_TARGET = "temperature_core_target";
public static final String TEMPERATURE_CORE_CURRENT = "temperature_core_current";
public static final String VENTILATION_POWER = "ventilation_power";
public static final String VENTILATION_POWER_RAW = "ventilation_power_raw";
public static final String ERROR_STATE = "error_state";
public static final String INFO_STATE = "info_state";
public static final String FRIDGE_SUPER_COOL = "fridge_super_cool";
public static final String FREEZER_SUPER_FREEZE = "freezer_super_freeze";
public static final String SUPER_COOL_CAN_BE_CONTROLLED = "super_cool_can_be_controlled";
public static final String SUPER_FREEZE_CAN_BE_CONTROLLED = "super_freeze_can_be_controlled";
public static final String FRIDGE_TEMPERATURE_TARGET = "fridge_temperature_target";
public static final String FRIDGE_TEMPERATURE_CURRENT = "fridge_temperature_current";
public static final String FREEZER_TEMPERATURE_TARGET = "freezer_temperature_target";
public static final String FREEZER_TEMPERATURE_CURRENT = "freezer_temperature_current";
public static final String TOP_TEMPERATURE_TARGET = "top_temperature_target";
public static final String TOP_TEMPERATURE_CURRENT = "top_temperature_current";
public static final String MIDDLE_TEMPERATURE_TARGET = "middle_temperature_target";
public static final String MIDDLE_TEMPERATURE_CURRENT = "middle_temperature_current";
public static final String BOTTOM_TEMPERATURE_TARGET = "bottom_temperature_target";
public static final String BOTTOM_TEMPERATURE_CURRENT = "bottom_temperature_current";
public static final String LIGHT_SWITCH = "light_switch";
public static final String LIGHT_CAN_BE_CONTROLLED = "light_can_be_controlled";
public static final String PLATE_1_POWER_STEP = "plate_1_power_step";
public static final String PLATE_1_POWER_STEP_RAW = "plate_1_power_step_raw";
public static final String PLATE_2_POWER_STEP = "plate_2_power_step";
public static final String PLATE_2_POWER_STEP_RAW = "plate_2_power_step_raw";
public static final String PLATE_3_POWER_STEP = "plate_3_power_step";
public static final String PLATE_3_POWER_STEP_RAW = "plate_3_power_step_raw";
public static final String PLATE_4_POWER_STEP = "plate_4_power_step";
public static final String PLATE_4_POWER_STEP_RAW = "plate_4_power_step_raw";
public static final String PLATE_5_POWER_STEP = "plate_5_power_step";
public static final String PLATE_5_POWER_STEP_RAW = "plate_5_power_step_raw";
public static final String PLATE_6_POWER_STEP = "plate_6_power_step";
public static final String PLATE_6_POWER_STEP_RAW = "plate_6_power_step_raw";
public static final String DOOR_STATE = "door_state";
public static final String DOOR_ALARM = "door_alarm";
public static final String BATTERY_LEVEL = "battery_level";
}
/**
* Constants for i18n keys.
*/
public static final class I18NKeys {
private I18NKeys() {
}
public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED = "@text/mielecloud.bridge.status.access.token.not.configured";
public static final String BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED = "@text/mielecloud.bridge.status.account.not.authorized";
public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED = "@text/mielecloud.bridge.status.access.token.refresh.failed";
public static final String BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL = "@text/mielecloud.bridge.status.invalid.email";
public static final String BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR = "@text/mielecloud.bridge.status.transient.http.error";
public static final String THING_STATUS_DESCRIPTION_WEBSERVICE_MISSING = "@text/mielecloud.thing.status.webservice.missing";
public static final String THING_STATUS_DESCRIPTION_REMOVED = "@text/mielecloud.thing.status.removed";
public static final String THING_STATUS_DESCRIPTION_RATELIMIT = "@text/mielecloud.thing.status.ratelimit";
public static final String THING_STATUS_DESCRIPTION_DISCONNECTED = "@text/mielecloud.thing.status.disconnected";
}
}

View File

@ -0,0 +1,33 @@
/**
* 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.mielecloud.internal.auth;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Indicates an error in the OAuth2 authorization process.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public class OAuthException extends RuntimeException {
private static final long serialVersionUID = -1863609233382694104L;
public OAuthException(final String message) {
super(message);
}
public OAuthException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,30 @@
/**
* 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.mielecloud.internal.auth;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Listener that is invoked when an OAuth 2 access token was refreshed.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public interface OAuthTokenRefreshListener {
/**
* Invoked when a new access token becomes available.
*
* @param accessToken The new access token.
*/
public void onNewAccessToken(String accessToken);
}

View File

@ -0,0 +1,74 @@
/**
* 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.mielecloud.internal.auth;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An {@link OAuthTokenRefresher} offers convenient access to OAuth 2 authentication related functionality,
* especially refreshing the access token.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Allow removing tokens from the storage
*/
@NonNullByDefault
public interface OAuthTokenRefresher {
/**
* Sets the listener that is called when the access token was refreshed.
*
* @param listener The listener to register.
* @param serviceHandle The service handle identifying the internal OAuth configuration.
* @throws OAuthException if the listener needs to be registered at an underlying service which is not available
* because the account has not yet been authorized
*/
public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle);
/**
* Unsets a listener.
*
* @param serviceHandle The service handle identifying the internal OAuth configuration.
*/
public void unsetRefreshListener(String serviceHandle);
/**
* Refreshes the access and refresh tokens for the given service handle. If an {@link OAuthTokenRefreshListener} is
* registered for the service handle then it is notified after the refresh has completed.
*
* This call will succeed if the access token is still valid or a valid refresh token exists, which can be used to
* refresh the expired access token. If refreshing fails, an {@link OAuthException} is thrown.
*
* @param serviceHandle The service handle identifying the internal OAuth configuration.
* @throws OAuthException if the token cannot be obtained or refreshed
*/
public void refreshToken(String serviceHandle);
/**
* Gets the currently stored access token from persistent storage.
*
* @param serviceHandle The service handle identifying the internal OAuth configuration.
* @return The currently stored access token or an empty {@link Optional} if there is no stored token.
*/
public Optional<String> getAccessTokenFromStorage(String serviceHandle);
/**
* Removes the tokens from persistent storage.
*
* Note: Calling this method will force the user to run through the pairing process again in order to obtain a
* working bridge.
*
* @param serviceHandle The service handle identifying the internal OAuth configuration.
*/
public void removeTokensFromStorage(String serviceHandle);
}

View File

@ -0,0 +1,138 @@
/**
* 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.mielecloud.internal.auth;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles refreshing of OAuth2 tokens managed by the openHAB runtime.
*
* @author Björn Lange - Initial contribution
*/
@Component
@NonNullByDefault
public final class OpenHabOAuthTokenRefresher implements OAuthTokenRefresher {
private final Logger logger = LoggerFactory.getLogger(OpenHabOAuthTokenRefresher.class);
private final OAuthFactory oauthFactory;
private Map<String, @Nullable AccessTokenRefreshListener> listenerByServiceHandle = new HashMap<>();
@Activate
public OpenHabOAuthTokenRefresher(@Reference OAuthFactory oauthFactory) {
this.oauthFactory = oauthFactory;
}
@Override
public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle) {
final AccessTokenRefreshListener refreshListener = tokenResponse -> {
final String accessToken = tokenResponse.getAccessToken();
if (accessToken == null) {
// Fail without exception to ensure that the OAuthClientService notifies all listeners.
logger.warn("Ignoring access token response without access token.");
} else {
listener.onNewAccessToken(accessToken);
}
};
OAuthClientService clientService = getOAuthClientService(serviceHandle);
clientService.addAccessTokenRefreshListener(refreshListener);
listenerByServiceHandle.put(serviceHandle, refreshListener);
}
@Override
public void unsetRefreshListener(String serviceHandle) {
final AccessTokenRefreshListener refreshListener = listenerByServiceHandle.get(serviceHandle);
if (refreshListener != null) {
try {
OAuthClientService clientService = getOAuthClientService(serviceHandle);
clientService.removeAccessTokenRefreshListener(refreshListener);
} catch (OAuthException e) {
logger.warn("Failed to remove refresh listener: OAuth client service is unavailable. Cause: {}",
e.getMessage());
}
}
listenerByServiceHandle.remove(serviceHandle);
}
@Override
public void refreshToken(String serviceHandle) {
if (listenerByServiceHandle.get(serviceHandle) == null) {
logger.warn("Token refreshing was requested but there is no token refresh listener registered!");
return;
}
OAuthClientService clientService = getOAuthClientService(serviceHandle);
refreshAccessToken(clientService);
}
private OAuthClientService getOAuthClientService(String serviceHandle) {
final OAuthClientService clientService = oauthFactory.getOAuthClientService(serviceHandle);
if (clientService == null) {
throw new OAuthException("OAuth client service is not available.");
}
return clientService;
}
private void refreshAccessToken(OAuthClientService clientService) {
try {
final AccessTokenResponse accessTokenResponse = clientService.refreshToken();
final String accessToken = accessTokenResponse.getAccessToken();
if (accessToken == null) {
throw new OAuthException("Access token is not available.");
}
} catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
throw new OAuthException("An error occured during token refresh: " + e.getMessage(), e);
} catch (IOException e) {
throw new OAuthException("A network error occured during token refresh: " + e.getMessage(), e);
} catch (OAuthResponseException e) {
throw new OAuthException("Miele cloud service returned an illegal response: " + e.getMessage(), e);
}
}
@Override
public Optional<String> getAccessTokenFromStorage(String serviceHandle) {
try {
AccessTokenResponse tokenResponse = getOAuthClientService(serviceHandle).getAccessTokenResponse();
if (tokenResponse == null) {
return Optional.empty();
} else {
return Optional.of(tokenResponse.getAccessToken());
}
} catch (OAuthException | org.openhab.core.auth.client.oauth2.OAuthException | IOException
| OAuthResponseException e) {
logger.debug("Cannot obtain access token from persistent storage.", e);
return Optional.empty();
}
}
@Override
public void removeTokensFromStorage(String serviceHandle) {
oauthFactory.deleteServiceAndAccessToken(serviceHandle);
}
}

View File

@ -0,0 +1,222 @@
/**
* 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.mielecloud.internal.config;
import java.util.Hashtable;
import java.util.Map;
import javax.servlet.ServletException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.FailureServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.PairAccountServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.ResourceLoader;
import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet;
import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet;
import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
import org.openhab.binding.mielecloud.internal.webservice.language.JvmLanguageProvider;
import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.config.discovery.inbox.Inbox;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.thing.ThingRegistry;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles the lifecycle of the Miele Cloud binding's configuration UI.
*
* @author Björn Lange - Initial Contribution
*/
@Component(service = MieleCloudConfigService.class, immediate = true, configurationPid = "binding.mielecloud.configService")
@NonNullByDefault
public final class MieleCloudConfigService {
private static final String ROOT_ALIAS = "/mielecloud";
private static final String PAIR_ALIAS = ROOT_ALIAS + "/pair";
private static final String FORWARD_TO_LOGIN_ALIAS = ROOT_ALIAS + "/forwardToLogin";
private static final String RESULT_ALIAS = ROOT_ALIAS + "/result";
private static final String SUCCESS_ALIAS = ROOT_ALIAS + "/success";
private static final String CREATE_BRIDGE_THING_ALIAS = ROOT_ALIAS + "/createBridgeThing";
private static final String FAILURE_ALIAS = ROOT_ALIAS + "/failure";
private static final String CSS_ALIAS = ROOT_ALIAS + "/assets/css";
private static final String JS_ALIAS = ROOT_ALIAS + "/assets/js";
private static final String IMG_ALIAS = ROOT_ALIAS + "/assets/img";
private static final String WEBSITE_RESOURCE_BASE_PATH = "org/openhab/binding/mielecloud/internal/config";
private static final String WEBSITE_CSS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/css";
private static final String WEBSITE_JS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/js";
private static final String WEBSITE_IMG_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/img";
private final Logger logger = LoggerFactory.getLogger(MieleCloudConfigService.class);
private HttpService httpService;
private OAuthFactory oauthFactory;
private Inbox inbox;
private ThingRegistry thingRegistry;
private LocaleProvider localeProvider;
/**
* For integration test purposes only.
*/
@Nullable
private AccountOverviewServlet accountOverviewServlet;
/**
* For integration test purposes only.
*/
@Nullable
private ForwardToLoginServlet forwardToLoginServlet;
/**
* For integration test purposes only.
*/
@Nullable
private ResultServlet resultServlet;
/**
* For integration test purposes only.
*/
@Nullable
private SuccessServlet successServlet;
/**
* For integration test purposes only.
*/
@Nullable
private CreateBridgeServlet createBridgeServlet;
@Activate
public MieleCloudConfigService(@Reference HttpService httpService, @Reference OAuthFactory oauthFactory,
@Reference Inbox inbox, @Reference ThingRegistry thingRegistry, @Reference LocaleProvider localeProvider) {
this.httpService = httpService;
this.oauthFactory = oauthFactory;
this.inbox = inbox;
this.thingRegistry = thingRegistry;
this.localeProvider = localeProvider;
}
@Nullable
public AccountOverviewServlet getAccountOverviewServlet() {
return accountOverviewServlet;
}
@Nullable
public ForwardToLoginServlet getForwardToLoginServlet() {
return forwardToLoginServlet;
}
@Nullable
public ResultServlet getResultServlet() {
return resultServlet;
}
@Nullable
public SuccessServlet getSuccessServlet() {
return successServlet;
}
@Nullable
public CreateBridgeServlet getCreateBridgeServlet() {
return createBridgeServlet;
}
@Activate
protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
registerWebsite(componentContext.getBundleContext());
}
private void registerWebsite(BundleContext bundleContext) {
ResourceLoader resourceLoader = new ResourceLoader(WEBSITE_RESOURCE_BASE_PATH, bundleContext);
OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory,
ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON));
try {
HttpContext httpContext = httpService.createDefaultHttpContext();
httpService.registerServlet(ROOT_ALIAS,
accountOverviewServlet = new AccountOverviewServlet(resourceLoader, thingRegistry, inbox),
new Hashtable<>(), httpContext);
httpService.registerServlet(PAIR_ALIAS, new PairAccountServlet(resourceLoader), new Hashtable<>(),
httpContext);
httpService.registerServlet(FORWARD_TO_LOGIN_ALIAS,
forwardToLoginServlet = new ForwardToLoginServlet(authorizationHandler), new Hashtable<>(),
httpContext);
httpService.registerServlet(RESULT_ALIAS, resultServlet = new ResultServlet(authorizationHandler),
new Hashtable<>(), httpContext);
httpService.registerServlet(SUCCESS_ALIAS,
successServlet = new SuccessServlet(resourceLoader, createLanguageProvider()), new Hashtable<>(),
httpContext);
httpService.registerServlet(CREATE_BRIDGE_THING_ALIAS,
createBridgeServlet = new CreateBridgeServlet(inbox, thingRegistry), new Hashtable<>(),
httpContext);
httpService.registerServlet(FAILURE_ALIAS, new FailureServlet(resourceLoader), new Hashtable<>(),
httpContext);
httpService.registerResources(CSS_ALIAS, WEBSITE_CSS_RESOURCE_PATH, httpContext);
httpService.registerResources(JS_ALIAS, WEBSITE_JS_RESOURCE_PATH, httpContext);
httpService.registerResources(IMG_ALIAS, WEBSITE_IMG_RESOURCE_PATH, httpContext);
logger.debug("Registered Miele Cloud binding website at /mielecloud");
} catch (NamespaceException | ServletException e) {
logger.warn(
"Failed to register Miele Cloud binding website. Miele Cloud binding website will not be available.",
e);
unregisterWebsite();
}
}
private LanguageProvider createLanguageProvider() {
return new CombiningLanguageProvider(new OpenHabLanguageProvider(localeProvider), new JvmLanguageProvider());
}
@Deactivate
protected void deactivate() {
unregisterWebsite();
}
private void unregisterWebsite() {
unregisterWebResource(ROOT_ALIAS);
unregisterWebResource(PAIR_ALIAS);
unregisterWebResource(FORWARD_TO_LOGIN_ALIAS);
unregisterWebResource(RESULT_ALIAS);
unregisterWebResource(SUCCESS_ALIAS);
unregisterWebResource(CREATE_BRIDGE_THING_ALIAS);
unregisterWebResource(CSS_ALIAS);
unregisterWebResource(JS_ALIAS);
unregisterWebResource(IMG_ALIAS);
forwardToLoginServlet = null;
resultServlet = null;
createBridgeServlet = null;
logger.debug("Unregistered Miele Cloud binding website at /mielecloud");
}
private void unregisterWebResource(String alias) {
try {
httpService.unregister(alias);
} catch (IllegalArgumentException e) {
logger.warn("Failed to unregister Miele Cloud binding website alias {}", alias, e);
}
}
}

View File

@ -0,0 +1,80 @@
/**
* 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.mielecloud.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
import org.openhab.core.thing.ThingUID;
/**
* Handles OAuth 2 authorization processes.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public interface OAuthAuthorizationHandler {
/**
* Begins the authorization process after the user provided client ID, client secret and a bridge ID.
*
* @param clientId Client ID.
* @param clientSecret Client secret.
* @param bridgeUid The UID of the bridge to authorize.
* @param email E-mail address identifying the account to authorize.
* @throws OngoingAuthorizationException if there already is an ongoing authorization.
*/
void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid, String email);
/**
* Creates the authorization URL for the ongoing authorization.
*
* @param redirectUri The URI to which the user is redirected after a successful login. This should point to our own
* service.
* @return The authorization URL to which the user is redirected for the log in.
* @throws NoOngoingAuthorizationException if there is no ongoing authorization.
* @throws OAuthException if the authorization URL cannot be determined. In this case the ongoing authorization is
* cancelled.
*/
String getAuthorizationUrl(String redirectUri);
/**
* Gets the UID of the bridge that is currently being authorized.
*/
ThingUID getBridgeUid();
/**
* Gets the e-mail address associated with the account that is currently being authorized.
*/
String getEmail();
/**
* Completes the authorization by extracting the authorization code from the given redirection URL, fetching the
* access token response and persisting it. After this method succeeded the access token can be read from the
* persistent storage.
*
* @param redirectUrlWithParameters The URL the remote service redirected the user to. This is the URL our servlet
* was called with.
* @throws NoOngoingAuthorizationException if there is no ongoing authorization.
* @throws OAuthException if the authorization failed. In this case the ongoing authorization is cancelled.
*/
void completeAuthorization(String redirectUrlWithParameters);
/**
* Gets the access token from persistent storage.
*
* @param email E-mail address for which the access token is requested.
* @return The access token.
* @throws OAuthException if the access token cannot be obtained.
*/
String getAccessToken(String email);
}

View File

@ -0,0 +1,213 @@
/**
* 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.mielecloud.internal.config;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.auth.OAuthException;
import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebservice;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.thing.ThingUID;
/**
* {@link OAuthAuthorizationHandler} implementation handling the OAuth 2 authorization via openHAB services.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class OAuthAuthorizationHandlerImpl implements OAuthAuthorizationHandler {
private static final String TOKEN_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/token";
private static final String AUTHORIZATION_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/login";
private static final long AUTHORIZATION_TIMEOUT_IN_MINUTES = 5;
private final OAuthFactory oauthFactory;
private final ScheduledExecutorService scheduler;
@Nullable
private OAuthClientService oauthClientService;
@Nullable
private ThingUID bridgeUid;
@Nullable
private String email;
@Nullable
private String redirectUri;
@Nullable
private ScheduledFuture<?> timer;
@Nullable
private LocalDateTime timerExpiryTimestamp;
/**
* Creates a new {@link OAuthAuthorizationHandlerImpl}.
*
* @param oauthFactory Factory for accessing the {@link OAuthClientService}.
* @param scheduler System-wide scheduler.
*/
public OAuthAuthorizationHandlerImpl(OAuthFactory oauthFactory, ScheduledExecutorService scheduler) {
this.oauthFactory = oauthFactory;
this.scheduler = scheduler;
}
@Override
public synchronized void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid,
String email) {
if (this.oauthClientService != null) {
throw new OngoingAuthorizationException("There is already an ongoing authorization!", timerExpiryTimestamp);
}
this.oauthClientService = oauthFactory.createOAuthClientService(email, TOKEN_URL, AUTHORIZATION_URL, clientId,
clientSecret, null, false);
this.bridgeUid = bridgeUid;
this.email = email;
redirectUri = null;
timer = null;
timerExpiryTimestamp = null;
}
@Override
public synchronized String getAuthorizationUrl(String redirectUri) {
final OAuthClientService oauthClientService = this.oauthClientService;
if (oauthClientService == null) {
throw new NoOngoingAuthorizationException("There is no ongoing authorization!");
}
this.redirectUri = redirectUri;
try {
timer = scheduler.schedule(this::cancelAuthorization, AUTHORIZATION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
timerExpiryTimestamp = LocalDateTime.now().plusMinutes(AUTHORIZATION_TIMEOUT_IN_MINUTES);
return oauthClientService.getAuthorizationUrl(redirectUri, null, null);
} catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
abortTimer();
cancelAuthorization();
throw new OAuthException("Failed to determine authorization URL: " + e.getMessage(), e);
}
}
@Override
public ThingUID getBridgeUid() {
final ThingUID bridgeUid = this.bridgeUid;
if (bridgeUid == null) {
throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
}
return bridgeUid;
}
@Override
public String getEmail() {
final String email = this.email;
if (email == null) {
throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
}
return email;
}
@Override
public synchronized void completeAuthorization(String redirectUrlWithParameters) {
abortTimer();
final OAuthClientService oauthClientService = this.oauthClientService;
if (oauthClientService == null) {
throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
}
try {
String authorizationCode = oauthClientService.extractAuthCodeFromAuthResponse(redirectUrlWithParameters);
// Although this method is called "get" it actually fetches and stores the token response as a side effect.
oauthClientService.getAccessTokenResponseByAuthorizationCode(authorizationCode, redirectUri);
} catch (IOException e) {
throw new OAuthException("Network error while retrieving token response: " + e.getMessage(), e);
} catch (OAuthResponseException e) {
throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
} catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
throw new OAuthException("Error while processing Miele service response: " + e.getMessage(), e);
} finally {
this.oauthClientService = null;
this.bridgeUid = null;
this.email = null;
this.redirectUri = null;
}
}
/**
* Aborts the timer.
*
* Note: All calls to this method must be {@code synchronized} to ensure thread-safety. Also note that
* {@link #cancelAuthorization()} is {@code synchronized} so the execution of this method and
* {@link #cancelAuthorization()} cannot overlap. Therefore, this method is an atomic operation from the timer's
* perspective.
*/
private void abortTimer() {
final ScheduledFuture<?> timer = this.timer;
if (timer == null) {
return;
}
if (!timer.isDone()) {
timer.cancel(false);
}
this.timer = null;
timerExpiryTimestamp = null;
}
private synchronized void cancelAuthorization() {
oauthClientService = null;
bridgeUid = null;
email = null;
redirectUri = null;
final ScheduledFuture<?> timer = this.timer;
if (timer != null) {
timer.cancel(false);
this.timer = null;
timerExpiryTimestamp = null;
}
}
@Override
public String getAccessToken(String email) {
OAuthClientService clientService = oauthFactory.getOAuthClientService(email);
if (clientService == null) {
throw new OAuthException("There is no access token registered for '" + email + "'");
}
try {
AccessTokenResponse response = clientService.getAccessTokenResponse();
if (response == null) {
throw new OAuthException(
"There is no access token in the persistent storage or it already expired and could not be refreshed");
} else {
return response.getAccessToken();
}
} catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
throw new OAuthException("Failed to read access token from persistent storage: " + e.getMessage(), e);
} catch (IOException e) {
throw new OAuthException(
"Network error during token refresh or error while reading from persistent storage: "
+ e.getMessage(),
e);
} catch (OAuthResponseException e) {
throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,121 @@
/**
* 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.mielecloud.internal.config;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
/**
* Generator for templates which can be copy-pasted into .things files by the user.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public class ThingsTemplateGenerator {
/**
* Creates a template for the bridge.
*
* @param bridgeId Id of the bridge (last part of the thing UID).
* @param locale Locale for accessing the Miele cloud service.
* @return The template.
*/
public String createBridgeConfigurationTemplate(String bridgeId, String email, String locale) {
var builder = new StringBuilder();
builder.append("Bridge ");
builder.append(MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString());
builder.append(":");
builder.append(bridgeId);
builder.append(" [ email=\"");
builder.append(email);
builder.append("\", locale=\"");
builder.append(locale);
builder.append("\" ]");
return builder.toString();
}
/**
* Creates a complete template containing the bridge and all paired devices.
*
* @param bridge The bridge which is used to pair the things.
* @param pairedThings The paired things.
* @param discoveryResults The discovery results which can be paired.
* @return The template.
*/
public String createBridgeAndThingConfigurationTemplate(Bridge bridge, List<Thing> pairedThings,
List<DiscoveryResult> discoveryResults) {
StringBuilder result = new StringBuilder();
result.append(createBridgeConfigurationTemplate(bridge.getUID().getId(),
bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString(),
getLocale(bridge)));
result.append(" {\n");
for (Thing thing : pairedThings) {
result.append(" ").append(createThingConfigurationTemplate(thing)).append("\n");
}
for (DiscoveryResult discoveryResult : discoveryResults) {
result.append(" ").append(createThingConfigurationTemplate(discoveryResult)).append("\n");
}
result.append("}");
return result.toString();
}
private String getLocale(Bridge bridge) {
var locale = bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
if (locale instanceof String) {
return (String) locale;
} else {
return "en";
}
}
private String createThingConfigurationTemplate(Thing thing) {
StringBuilder result = new StringBuilder();
result.append("Thing ").append(thing.getThingTypeUID().getId()).append(" ").append(thing.getUID().getId())
.append(" ");
final String label = thing.getLabel();
if (label != null) {
result.append("\"").append(label).append("\" ");
}
result.append("[ ");
result.append("deviceIdentifier=\"");
result.append(
thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString());
result.append("\"");
result.append(" ]");
return result.toString();
}
private String createThingConfigurationTemplate(DiscoveryResult discoveryResult) {
return "Thing " + discoveryResult.getThingTypeUID().getId() + " " + discoveryResult.getThingUID().getId()
+ " \"" + discoveryResult.getLabel() + "\" [ deviceIdentifier=\""
+ getProperty(discoveryResult, MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER) + "\" ]";
}
private String getProperty(DiscoveryResult discoveryResult, String propertyName) {
var value = discoveryResult.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER);
if (value == null) {
return "";
} else {
return value.toString();
}
}
}

View File

@ -0,0 +1,25 @@
/**
* 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.mielecloud.internal.config.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception thrown when a bridge cannot be created in the configuration flow.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class BridgeCreationFailedException extends RuntimeException {
private static final long serialVersionUID = -6150154333256723312L;
}

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.mielecloud.internal.config.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception thrown when reconfiguring an existing bridge fails.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public class BridgeReconfigurationFailedException extends RuntimeException {
private static final long serialVersionUID = -6341258448724364940L;
public BridgeReconfigurationFailedException(String message) {
super(message);
}
}

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.mielecloud.internal.config.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception thrown when no authorization is ongoing.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public class NoOngoingAuthorizationException extends RuntimeException {
private static final long serialVersionUID = 3074275827393542416L;
public NoOngoingAuthorizationException(String message) {
super(message);
}
}

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.mielecloud.internal.config.exception;
import java.time.LocalDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Exception thrown when there already is an ongoing authorization process.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class OngoingAuthorizationException extends RuntimeException {
private static final long serialVersionUID = -6742384930140134244L;
@Nullable
private final LocalDateTime ongoingAuthorizationExpiryTimestamp;
/**
* Creates a new {@link OngoingAuthorizationException}.
*
* @param message Exception message.
* @param ongoingAuthorizationExpiryTimestamp Timestamp when the ongoing authorization will expire.
*/
public OngoingAuthorizationException(String message, @Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
super(message);
this.ongoingAuthorizationExpiryTimestamp = ongoingAuthorizationExpiryTimestamp;
}
/**
* Gets the timestamp representing when the ongoing authorization will expire.
*/
@Nullable
public LocalDateTime getOngoingAuthorizationExpiryTimestamp() {
return ongoingAuthorizationExpiryTimestamp;
}
}

View File

@ -0,0 +1,62 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for servlets that have no visible frontend and just serve the purpose of redirecting the user to another
* website.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public abstract class AbstractRedirectionServlet extends HttpServlet {
private static final long serialVersionUID = 4280026301732437523L;
private final Logger logger = LoggerFactory.getLogger(AbstractRedirectionServlet.class);
@Override
protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws ServletException, IOException {
if (response == null) {
logger.warn("Ignoring received request without response.");
return;
}
if (request == null) {
logger.warn("Ignoring illegal request.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
response.sendRedirect(getRedirectionDestination(request));
}
/**
* Gets the redirection destination. This can be a relative or absolute path or a link to another website.
*
* @param request The original request sent by the browser.
* @return The redirection destination.
*/
protected abstract String getRedirectionDestination(HttpServletRequest request);
}

View File

@ -0,0 +1,93 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for servlets that show a visible frontend in the browser.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public abstract class AbstractShowPageServlet extends HttpServlet {
private static final long serialVersionUID = 3820684716753275768L;
private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
private final Logger logger = LoggerFactory.getLogger(AbstractShowPageServlet.class);
private final ResourceLoader resourceLoader;
protected ResourceLoader getResourceLoader() {
return resourceLoader;
}
/**
* Creates a new {@link AbstractShowPageServlet}.
*
* @param resourceLoader Loader for resource files.
*/
public AbstractShowPageServlet(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws ServletException, IOException {
if (response == null) {
logger.warn("Ignoring received request without response.");
return;
}
if (request == null) {
logger.warn("Ignoring illegal request.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try {
String html = handleGetRequest(request, response);
response.setContentType(CONTENT_TYPE);
response.getWriter().write(html);
response.getWriter().close();
} catch (MieleHttpException e) {
response.sendError(e.getHttpErrorCode());
} catch (IOException e) {
logger.warn("Failed to load resources.", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* Handles a GET request.
*
* @param request The request.
* @param response The response.
* @return A rendered HTML body to be displayed in the browser. The body will be framed by the binding's frontend
* layout.
* @throws MieleHttpException if an error occurs that should be handled by sending a default error response.
* @throws IOException if an error occurs while loading resources.
*/
protected abstract String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
throws MieleHttpException, IOException;
}

View File

@ -0,0 +1,182 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.inbox.Inbox;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
/**
* Servlet showing the account overview page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class AccountOverviewServlet extends AbstractShowPageServlet {
private static final long serialVersionUID = -4551210904923220429L;
private static final String ACCOUNTS_SKELETON = "index.html";
private static final String BRIDGES_TITLE_PLACEHOLDER = "<!-- BRIDGES TITLE -->";
private static final String BRIDGES_PLACEHOLDER = "<!-- BRIDGES -->";
private static final String NO_SSL_WARNING_PLACEHOLDER = "<!-- NO SSL WARNING -->";
private final ThingRegistry thingRegistry;
private final Inbox inbox;
private final ThingsTemplateGenerator templateGenerator;
/**
* Creates a new {@link AccountOverviewServlet}.
*
* @param resourceLoader Loader to use for resources.
* @param thingRegistry openHAB thing registry.
* @param inbox openHAB inbox for discovery results.
*/
public AccountOverviewServlet(ResourceLoader resourceLoader, ThingRegistry thingRegistry, Inbox inbox) {
super(resourceLoader);
this.thingRegistry = thingRegistry;
this.inbox = inbox;
this.templateGenerator = new ThingsTemplateGenerator();
}
@Override
protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
throws MieleHttpException, IOException {
String skeleton = getResourceLoader().loadResourceAsString(ACCOUNTS_SKELETON);
skeleton = renderBridges(skeleton);
skeleton = renderSslWarning(request, skeleton);
return skeleton;
}
private String renderBridges(String skeleton) {
List<Thing> bridges = thingRegistry.stream().filter(this::isMieleCloudBridge).collect(Collectors.toList());
if (bridges.isEmpty()) {
return renderNoBridges(skeleton);
} else {
return renderBridgesIntoSkeleton(skeleton, bridges);
}
}
private String renderNoBridges(String skeleton) {
return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "There is no account paired at the moment.")
.replace(BRIDGES_PLACEHOLDER, "");
}
private String renderBridgesIntoSkeleton(String skeleton, List<Thing> bridges) {
StringBuilder builder = new StringBuilder();
int index = 0;
Iterator<Thing> bridgeIterator = bridges.iterator();
while (bridgeIterator.hasNext()) {
builder.append(renderBridge(bridgeIterator.next(), index));
index++;
}
return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "The following bridges are paired")
.replace(BRIDGES_PLACEHOLDER, builder.toString());
}
private String renderBridge(Thing bridge, int index) {
StringBuilder builder = new StringBuilder();
builder.append(" <li>\n");
String thingUid = bridge.getUID().getAsString();
String thingId = bridge.getUID().getId();
builder.append(" ");
builder.append(thingUid.substring(0, thingUid.length() - thingId.length()));
builder.append(" ");
builder.append(thingId);
builder.append(" ");
builder.append(bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString());
builder.append("\n");
builder.append(" <span class=\"status ");
final ThingStatus status = bridge.getStatus();
if (status == ThingStatus.ONLINE) {
builder.append("online");
} else {
builder.append("offline");
}
builder.append("\">");
builder.append(status.toString());
builder.append("</span>\n");
builder.append(" <input class=\"trigger\" id=\"mielecloud-account-");
builder.append(thingId);
builder.append("\" type=\"checkbox\" name=\"things-file\" />\n");
builder.append(" <label for=\"mielecloud-account-");
builder.append(thingId);
builder.append("\">&lt; &gt;</label>\n");
builder.append(" <div class=\"things\">\n");
builder.append(
" <span class=\"legend\">You can use this things-file template to pair all available devices:</span>\n");
builder.append(" <div class=\"code-container\">\n");
builder.append(
" <a href=\"#\" onclick=\"copyCodeToClipboard(event, this);\" class=\"btn btn-outline-info btn-sm copy\">Copy</a>\n");
builder.append(" <textarea readonly>");
builder.append(generateConfigurationTemplate((Bridge) bridge));
builder.append("</textarea>\n");
builder.append(" </div>\n");
builder.append(" </div>\n");
builder.append(" </li>");
return builder.toString();
}
private String generateConfigurationTemplate(Bridge bridge) {
List<Thing> pairedThings = thingRegistry.stream().filter(thing -> isConnectedVia(thing, bridge))
.collect(Collectors.toList());
List<DiscoveryResult> discoveryResults = inbox.stream()
.filter(discoveryResult -> willConnectVia(discoveryResult, bridge)).collect(Collectors.toList());
return templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings, discoveryResults);
}
private boolean isConnectedVia(Thing thing, Bridge bridge) {
return bridge.getUID().equals(thing.getBridgeUID());
}
private boolean willConnectVia(DiscoveryResult discoveryResult, Bridge bridge) {
return bridge.getUID().equals(discoveryResult.getBridgeUID());
}
private boolean isMieleCloudBridge(Thing thing) {
return MieleCloudBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID());
}
private String renderSslWarning(HttpServletRequest request, String skeleton) {
if (!request.isSecure()) {
return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "<div class=\"alert alert-danger\" role=\"alert\">\n"
+ " Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n"
+ " See <a href=\"https://www.openhab.org/docs/installation/security.html\">Securing access to openHAB</a> for details.\n"
+ " </div>");
} else {
return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "");
}
}
}

View File

@ -0,0 +1,217 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException;
import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException;
import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
import org.openhab.binding.mielecloud.internal.util.EmailValidator;
import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.inbox.Inbox;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet that automatically creates a bridge and then redirects the browser to the account overview page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class CreateBridgeServlet extends AbstractRedirectionServlet {
private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector";
private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account";
private static final String LOCALE_PARAMETER_NAME = "locale";
public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
public static final String EMAIL_PARAMETER_NAME = "email";
private static final long serialVersionUID = -2912042079128722887L;
private static final String DEFAULT_LOCALE = "en";
private static final long ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS = 5000;
private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000;
private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100;
private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class);
private final Inbox inbox;
private final ThingRegistry thingRegistry;
/**
* Creates a new {@link CreateBridgeServlet}.
*
* @param inbox openHAB inbox for discovery results.
* @param thingRegistry openHAB thing registry.
*/
public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) {
this.inbox = inbox;
this.thingRegistry = thingRegistry;
}
@Override
protected String getRedirectionDestination(HttpServletRequest request) {
String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
if (bridgeUidString == null || bridgeUidString.isEmpty()) {
logger.warn("Cannot create bridge: Bridge UID is missing.");
return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true";
}
String email = request.getParameter(EMAIL_PARAMETER_NAME);
if (email == null || email.isEmpty()) {
logger.warn("Cannot create bridge: E-mail address is missing.");
return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true";
}
ThingUID bridgeUid = null;
try {
bridgeUid = new ThingUID(bridgeUidString);
} catch (IllegalArgumentException e) {
logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid);
return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true";
}
if (!EmailValidator.isValid(email)) {
logger.warn("Cannot create bridge: E-mail address '{}' is malformed.", email);
return "/mielecloud/failure?" + FailureServlet.MALFORMED_EMAIL_PARAMETER_NAME + "=true";
}
String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME));
logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale,
request.getParameter(LOCALE_PARAMETER_NAME));
try {
Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email);
waitForBridgeToComeOnline(bridge);
return "/mielecloud";
} catch (BridgeReconfigurationFailedException e) {
logger.warn("{}", e.getMessage());
return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&"
+ SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
} catch (BridgeCreationFailedException e) {
logger.warn("Thing creation failed because there was no binding available that supports the thing.");
return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&"
+ SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
}
}
private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) {
DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid)
.withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL)
.withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME)
.withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale)
.withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build();
if (inbox.add(result)) {
return pairBridge(bridgeUid);
} else {
return reconfigureBridge(bridgeUid, locale, email);
}
}
private Thing pairBridge(ThingUID thingUid) {
Thing thing = inbox.approve(thingUid, MIELE_CLOUD_BRIDGE_LABEL, null);
if (thing == null) {
throw new BridgeCreationFailedException();
}
logger.debug("Successfully created bridge {}", thingUid);
return thing;
}
private Thing reconfigureBridge(ThingUID thingUid, String locale, String email) {
logger.debug("Thing already exists. Modifying configuration.");
Thing thing = thingRegistry.get(thingUid);
if (thing == null) {
throw new BridgeReconfigurationFailedException(
"Cannot modify non existing bridge: Could neither add bridge via inbox nor find existing bridge.");
}
ThingHandler handler = thing.getHandler();
if (handler == null) {
throw new BridgeReconfigurationFailedException("Bridge exists but has no handler.");
}
if (!(handler instanceof MieleBridgeHandler)) {
throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '"
+ MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'.");
}
MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler;
bridgeHandler.disposeWebservice();
bridgeHandler.initializeWebservice();
return thing;
}
private String getValidLocale(@Nullable String localeParameterValue) {
if (localeParameterValue == null || localeParameterValue.isEmpty()
|| !LocaleValidator.isValidLanguage(localeParameterValue)) {
return DEFAULT_LOCALE;
} else {
return localeParameterValue;
}
}
private void waitForBridgeToComeOnline(Thing bridge) {
try {
waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE,
ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS);
waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(),
DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds)
throws InterruptedException {
long remainingWaitTime = timeoutInMilliseconds;
while (!condition.getAsBoolean() && remainingWaitTime > 0) {
TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS);
remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS;
}
}
private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier {
private long previousDiscoveryResultCount = 0;
@Override
public boolean getAsBoolean() {
var discoveryResultCount = countOwnDiscoveryResults();
var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount;
previousDiscoveryResultCount = discoveryResultCount;
return discoveryResultCountUnchanged;
}
private long countOwnDiscoveryResults() {
return inbox.stream().map(DiscoveryResult::getBindingId)
.filter(MieleCloudBindingConstants.BINDING_ID::equals).count();
}
}
}

View File

@ -0,0 +1,116 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Servlet showing a failure page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public class FailureServlet extends AbstractShowPageServlet {
private static final long serialVersionUID = -5195984256535664942L;
public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error";
public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse";
public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization";
public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization";
public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid";
public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid";
public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied";
public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request";
public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client";
public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope";
public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error";
public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable";
private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
/**
* Creates a new {@link FailureServlet}.
*
* @param resourceLoader Loader to use for resources.
*/
public FailureServlet(ResourceLoader resourceLoader) {
super(resourceLoader);
}
@Override
protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
throws MieleHttpException, IOException {
return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
getErrorMessage(request));
}
private String getErrorMessage(HttpServletRequest request) {
String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME);
if (oauth2Error != null) {
return getOAuth2ErrorMessage(oauth2Error);
} else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) {
return "Miele cloud service returned an illegal response.";
} else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) {
return "There is no ongoing authorization. Please start an authorization first.";
} else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) {
return "Completing the final authorization request failed. Please try the config flow again.";
} else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) {
return "Missing bridge UID.";
} else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
return "Missing e-mail address.";
} else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) {
return "Malformed bridge UID.";
} else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
return "Malformed e-mail address.";
} else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
return "Missing request URL. Please try the config flow again.";
} else {
return "Unknown error.";
}
}
private String getOAuth2ErrorMessage(String oauth2Error) {
return "OAuth2 authentication with Miele cloud service failed: " + getOAuth2ErrorDetailMessage(oauth2Error);
}
private String getOAuth2ErrorDetailMessage(String oauth2Error) {
switch (oauth2Error) {
case OAUTH2_ERROR_ACCESS_DENIED:
return "Access denied.";
case OAUTH2_ERROR_INVALID_REQUEST:
return "Malformed request.";
case OAUTH2_ERROR_UNAUTHORIZED_CLIENT:
return "Account not authorized to request authorization code.";
case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE:
return "Obtaining an authorization code is not supported.";
case OAUTH2_ERROR_INVALID_SCOPE:
return "Invalid scope.";
case OAUTH2_ERROR_SERVER_ERROR:
return "Unexpected server error.";
case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE:
return "Authorization server temporarily unavailable.";
default:
return "Unknown error code \"" + oauth2Error + "\".";
}
}
}

View File

@ -0,0 +1,148 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.auth.OAuthException;
import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
import org.openhab.binding.mielecloud.internal.util.EmailValidator;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet gathers and processes required information to perform an authorization with the Miele cloud service
* and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an
* e-mail address. If the given parameters are valid, the browser is redirected to the Miele service login. Otherwise,
* the browser is redirected to the previous page with an according error message.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ForwardToLoginServlet extends AbstractRedirectionServlet {
private static final long serialVersionUID = -9094642228439994183L;
public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId";
public static final String EMAIL_PARAMETER_NAME = "email";
private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class);
private final OAuthAuthorizationHandler authorizationHandler;
/**
* Creates a new {@link ForwardToLoginServlet}.
*
* @param authorizationHandler Handler implementing the OAuth authorization process.
*/
public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) {
this.authorizationHandler = authorizationHandler;
}
@Override
protected String getRedirectionDestination(HttpServletRequest request) {
String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME);
String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME);
String bridgeId = request.getParameter(BRIDGE_ID_PARAMETER_NAME);
String email = request.getParameter(EMAIL_PARAMETER_NAME);
if (clientId == null || clientId.isEmpty()) {
logger.warn("Request is missing client ID.");
return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME);
}
if (clientSecret == null || clientSecret.isEmpty()) {
logger.warn("Request is missing client secret.");
return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME);
}
if (bridgeId == null || bridgeId.isEmpty()) {
logger.warn("Request is missing bridge ID.");
return getErrorRedirectionUrl(PairAccountServlet.MISSING_BRIDGE_ID_PARAMETER_NAME);
}
if (email == null || email.isEmpty()) {
logger.warn("Request is missing e-mail address.");
return getErrorRedirectionUrl(PairAccountServlet.MISSING_EMAIL_PARAMETER_NAME);
}
ThingUID bridgeUid = null;
try {
bridgeUid = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, bridgeId);
} catch (IllegalArgumentException e) {
logger.warn("Passed bridge ID '{}' is invalid.", bridgeId);
return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_BRIDGE_ID_PARAMETER_NAME);
}
if (!EmailValidator.isValid(email)) {
logger.warn("Passed e-mail address '{}' is invalid.", email);
return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_EMAIL_PARAMETER_NAME);
}
try {
authorizationHandler.beginAuthorization(clientId, clientSecret, bridgeUid, email);
} catch (OngoingAuthorizationException e) {
logger.warn("Cannot begin new authorization process while another one is still running.");
return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp());
}
StringBuffer requestUrl = request.getRequestURL();
if (requestUrl == null) {
return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME);
}
try {
return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString()));
} catch (NoOngoingAuthorizationException e) {
logger.warn(
"Failed to create authorization URL: There was no ongoing authorization although we just started one.");
return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME);
} catch (OAuthException e) {
logger.warn("Failed to create authorization URL.", e);
return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME);
}
}
private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
if (ongoingAuthorizationExpiryTimestamp == null) {
return getErrorRedirectionUrl(
PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME);
}
long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp)
+ 1;
return getErrorRedirectionUrl(
PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
Long.toString(minutesUntilExpiry));
}
private String getErrorRedirectionUrl(String errorCode) {
return getErrorRedirectionUrl(errorCode, "true");
}
private String getErrorRedirectionUrl(String errorCode, String parameterValue) {
return "/mielecloud/pair?" + errorCode + "=" + parameterValue;
}
private String deriveRedirectUri(String requestUrl) {
return requestUrl + "/../result";
}
}

View File

@ -0,0 +1,35 @@
/**
* 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.mielecloud.internal.config.servlet;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception wrapping a HTTP error code for further processing.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class MieleHttpException extends Exception {
private static final long serialVersionUID = 1825214275413952809L;
private final int httpErrorCode;
public MieleHttpException(int httpErrorCode) {
this.httpErrorCode = httpErrorCode;
}
public int getHttpErrorCode() {
return httpErrorCode;
}
}

View File

@ -0,0 +1,124 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Servlet showing the pair account page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class PairAccountServlet extends AbstractShowPageServlet {
private static final long serialVersionUID = 6565378471951635420L;
public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId";
public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret";
public static final String MISSING_BRIDGE_ID_PARAMETER_NAME = "missingBridgeId";
public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
public static final String MALFORMED_BRIDGE_ID_PARAMETER_NAME = "malformedBridgeId";
public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl";
public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes";
public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown";
public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2";
public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
private static final String PAIR_ACCOUNT_SKELETON = "pairing.html";
private static final String CLIENT_ID_PLACEHOLDER = "<!-- CLIENT ID -->";
private static final String CLIENT_SECRET_PLACEHOLDER = "<!-- CLIENT SECRET -->";
private static final String ERROR_MESSAGE_PLACEHOLDER = "<!-- ERROR MESSAGE -->";
/**
* Creates a new {@link PairAccountServlet}.
*
* @param resourceLoader Loader for resources.
*/
public PairAccountServlet(ResourceLoader resourceLoader) {
super(resourceLoader);
}
@Override
protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
throws MieleHttpException, IOException {
String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON);
skeleton = renderClientIdAndClientSecret(request, skeleton);
skeleton = renderErrorMessage(request, skeleton);
return skeleton;
}
private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) {
String prefilledClientId = Optional.ofNullable(request.getParameter(CLIENT_ID_PARAMETER_NAME)).orElse("");
String prefilledClientSecret = Optional.ofNullable(request.getParameter(CLIENT_SECRET_PARAMETER_NAME))
.orElse("");
return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER,
prefilledClientSecret);
}
private String renderErrorMessage(HttpServletRequest request, String skeleton) {
if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Missing client ID.</div>");
} else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Missing client secret.</div>");
} else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_ID_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Missing bridge ID.</div>");
} else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Missing e-mail address.</div>");
} else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_ID_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!</div>");
} else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Malformed e-mail address.</div>");
} else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Failed to derive redirect URL.</div>");
} else if (ServletUtil.isParameterPresent(request,
ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) {
String minutesUntilExpiry = request
.getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME);
if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.</div>");
} else {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in "
+ minutesUntilExpiry + " minutes.</div>");
}
} else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?</div>");
} else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Missing request URL. Please try again.</div>");
} else {
return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, "");
}
}
}

View File

@ -0,0 +1,86 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.osgi.framework.BundleContext;
/**
* Provides access to resource files for servlets.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ResourceLoader {
private static final String BEGINNING_OF_INPUT = "\\A";
private final String basePath;
private final BundleContext bundleContext;
/**
* Creates a new {@link ResourceLoader}.
*
* @param basePath The base path to use for loading. A trailing {@code "/"} is removed.
* @param bundleContext {@link BundleContext} to load from.
*/
public ResourceLoader(String basePath, BundleContext bundleContext) {
this.basePath = removeTrailingSlashes(basePath);
this.bundleContext = bundleContext;
}
private String removeTrailingSlashes(String value) {
String ret = value;
while (ret.endsWith("/")) {
ret = ret.substring(0, ret.length() - 1);
}
return ret;
}
/**
* Opens a resource relative to the base path.
*
* @param filename The filename of the resource to load.
* @return A stream reading from the resource file.
* @throws FileNotFoundException If the requested resource file cannot be found.
* @throws IOException If an error occurs while opening a stream to the resource.
*/
public InputStream openResource(String filename) throws IOException {
URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename);
if (url == null) {
throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'");
}
return url.openStream();
}
/**
* Loads the contents of a resource file as UTF-8 encoded {@link String}.
*
* @param filename The filename of the resource to load.
* @return The contents of the file.
* @throws FileNotFoundException If the requested resource file cannot be found.
* @throws IOException If an error occurs while opening a stream to the resource or reading from it.
*/
public String loadResourceAsString(String filename) throws IOException {
try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) {
return scanner.useDelimiter(BEGINNING_OF_INPUT).next();
}
}
}

View File

@ -0,0 +1,96 @@
/**
* 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.mielecloud.internal.config.servlet;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.auth.OAuthException;
import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet processing the response by the Miele service after a login. This servlet is called as a result of a
* completed login to the Miele service and assumes that the OAuth 2 parameters are passed. Depending on the parameters
* and whether the token response can be fetched either the browser is redirected to the success or the failure page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ResultServlet extends AbstractRedirectionServlet {
private static final long serialVersionUID = 2157912755568949550L;
public static final String CODE_PARAMETER_NAME = "code";
public static final String STATE_PARAMETER_NAME = "state";
public static final String ERROR_PARAMETER_NAME = "error";
private final Logger logger = LoggerFactory.getLogger(ResultServlet.class);
private final OAuthAuthorizationHandler authorizationHandler;
/**
* Creates a new {@link ResultServlet}.
*
* @param authorizationHandler Handler implementing the OAuth authorization.
*/
public ResultServlet(OAuthAuthorizationHandler authorizationHandler) {
this.authorizationHandler = authorizationHandler;
}
@Override
protected String getRedirectionDestination(HttpServletRequest request) {
String error = request.getParameter(ERROR_PARAMETER_NAME);
if (error != null) {
logger.warn("Received error response: {}", error);
return "/mielecloud/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error;
}
String code = request.getParameter(CODE_PARAMETER_NAME);
if (code == null) {
logger.warn("Code is null");
return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
}
String state = request.getParameter(STATE_PARAMETER_NAME);
if (state == null) {
logger.warn("State is null");
return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
}
try {
ThingUID bridgeId = authorizationHandler.getBridgeUid();
String email = authorizationHandler.getEmail();
StringBuffer requestUrl = request.getRequestURL();
if (requestUrl == null) {
return "/mielecloud/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true";
}
try {
authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString());
} catch (OAuthException e) {
logger.warn("Failed to complete authorization.", e);
return "/mielecloud/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME
+ "=true";
}
return "/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeId.getAsString()
+ "&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
} catch (NoOngoingAuthorizationException e) {
logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out");
return "/mielecloud/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true";
}
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.mielecloud.internal.config.servlet;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility class for common servlet tasks.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ServletUtil {
private ServletUtil() {
throw new UnsupportedOperationException();
}
/**
* Gets the value of a request parameter or returns a default if the parameter is not present.
*/
public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName,
String defaultValue) {
String parameterValue = request.getParameter(parameterName);
if (parameterValue == null) {
return defaultValue;
} else {
return parameterValue;
}
}
/**
* Checks whether a request parameter is enabled.
*/
public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) {
return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false"));
}
/**
* Checks whether a parameter is present in a request.
*/
public static boolean isParameterPresent(HttpServletRequest request, String parameterName) {
String parameterValue = request.getParameter(parameterName);
return parameterValue != null && !parameterValue.trim().isEmpty();
}
}

View File

@ -0,0 +1,212 @@
/**
* 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.mielecloud.internal.config.servlet;
import java.io.IOException;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
import org.openhab.binding.mielecloud.internal.util.EmailValidator;
import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet showing the success page.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public class SuccessServlet extends AbstractShowPageServlet {
private static final long serialVersionUID = 7013060161686096950L;
public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
public static final String EMAIL_PARAMETER_NAME = "email";
public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed";
public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed";
private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
private static final String BRIDGE_UID_PLACEHOLDER = "<!-- BRIDGE UID -->";
private static final String EMAIL_PLACEHOLDER = "<!-- EMAIL -->";
private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = "<!-- THINGS TEMPLATE CODE -->";
private static final String LOCALE_OPTIONS_PLACEHOLDER = "<!-- LOCALE OPTIONS -->";
private static final String DEFAULT_LANGUAGE = "en";
private static final Set<String> SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es");
private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class);
private final LanguageProvider languageProvider;
private final ThingsTemplateGenerator templateGenerator;
/**
* Creates a new {@link SuccessServlet}.
*
* @param resourceLoader Loader for resources.
* @param languageProvider Provider for the language to use as default selection.
*/
public SuccessServlet(ResourceLoader resourceLoader, LanguageProvider languageProvider) {
super(resourceLoader);
this.languageProvider = languageProvider;
this.templateGenerator = new ThingsTemplateGenerator();
}
@Override
protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
throws MieleHttpException, IOException {
String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
if (bridgeUidString == null || bridgeUidString.isEmpty()) {
logger.warn("Success page is missing bridge UID.");
return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"Missing bridge UID.");
}
String email = request.getParameter(EMAIL_PARAMETER_NAME);
if (email == null || email.isEmpty()) {
logger.warn("Success page is missing e-mail address.");
return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"Missing e-mail address.");
}
ThingUID bridgeUid = null;
try {
bridgeUid = new ThingUID(bridgeUidString);
} catch (IllegalArgumentException e) {
logger.warn("Success page received malformed bridge UID '{}'.", bridgeUidString);
return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"Malformed bridge UID.");
}
if (!EmailValidator.isValid(email)) {
logger.warn("Success page received malformed e-mail address '{}'.", email);
return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"Malformed e-mail address.");
}
String skeleton = getResourceLoader().loadResourceAsString("success.html");
skeleton = renderErrorMessage(request, skeleton);
skeleton = renderBridgeUid(skeleton, bridgeUid);
skeleton = renderEmail(skeleton, email);
skeleton = renderLocaleSelection(skeleton);
skeleton = renderBridgeConfigurationTemplate(skeleton, bridgeUid, email);
return skeleton;
}
private String renderErrorMessage(HttpServletRequest request, String skeleton) {
if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.</div>");
} else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) {
return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
"<div class=\"alert alert-danger\" role=\"alert\">Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.</div>");
} else {
return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, "");
}
}
private String renderBridgeUid(String skeleton, ThingUID bridgeUid) {
return skeleton.replace(BRIDGE_UID_PLACEHOLDER, bridgeUid.getAsString());
}
private String renderEmail(String skeleton, String email) {
return skeleton.replace(EMAIL_PLACEHOLDER, email);
}
private String renderLocaleSelection(String skeleton) {
String preSelectedLanguage = languageProvider.getLanguage().filter(SUPPORTED_LANGUAGES::contains)
.orElse(DEFAULT_LANGUAGE);
return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER,
SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get)
.sorted()
.map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode())))
.collect(Collectors.joining("\n")));
}
private String createOptionTag(Language language, boolean selected) {
String firstPart = " <option value=\"" + language.getCode() + "\"";
String secondPart = ">" + language.format() + "</option>";
if (selected) {
return firstPart + " selected=\"selected\"" + secondPart;
} else {
return firstPart + secondPart;
}
}
private String renderBridgeConfigurationTemplate(String skeleton, ThingUID bridgeUid, String email) {
String bridgeTemplate = templateGenerator.createBridgeConfigurationTemplate(bridgeUid.getId(), email,
languageProvider.getLanguage().orElse("en"));
return skeleton.replace(THINGS_TEMPLATE_CODE_PLACEHOLDER, bridgeTemplate);
}
/**
* A language representation for user display.
*
* @author Björn Lange - Initial contribution
*/
private static final class Language implements Comparable<Language> {
private final String code;
private final String name;
private Language(String code, String name) {
this.code = code;
this.name = name;
}
/**
* Gets the 2-letter language code for accessing the Miele Cloud service.
*/
public String getCode() {
return code;
}
/**
* Formats the language for displaying.
*/
public String format() {
return name + " - " + code;
}
@Override
public int compareTo(Language other) {
return name.toUpperCase().compareTo(other.name.toUpperCase());
}
/**
* Constructs a {@link Language} from a 2-letter language code.
*
* @param code 2-letter language code.
* @return An {@link Optional} wrapping the {@link Language} or an empty {@link Optional} if there is no
* representation for the given language code.
*/
public static Optional<Language> fromCode(String code) {
Locale locale = new Locale(code);
String name = locale.getDisplayLanguage(locale);
if (name.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(new Language(code, name));
}
}
}
}

View File

@ -0,0 +1,214 @@
/**
* 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.mielecloud.internal.discovery;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
import static org.openhab.binding.mielecloud.internal.handler.MieleHandlerFactory.SUPPORTED_THING_TYPES;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
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.Thing;
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;
/**
* Discovery service for things linked to a Miele cloud account.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Do not directly listen to webservice events
*/
@NonNullByDefault
public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private static final int BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS = 5;
@Nullable
private MieleBridgeHandler bridgeHandler;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private boolean discoveringDevices = false;
/**
* Creates a new {@link ThingDiscoveryService}.
*/
public ThingDiscoveryService() {
super(SUPPORTED_THING_TYPES, BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS);
}
@Nullable
private ThingUID getBridgeUid() {
var bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return null;
} else {
return bridgeHandler.getThing().getUID();
}
}
@Override
protected void startScan() {
}
@Override
public void activate() {
startBackgroundDiscovery();
}
@Override
public void deactivate() {
stopBackgroundDiscovery();
removeOlderResults(System.currentTimeMillis(), getBridgeUid());
}
/**
* Invoked when a device state update is received from the Miele cloud.
*/
public void onDeviceStateUpdated(DeviceState deviceState) {
if (!discoveringDevices) {
return;
}
Optional<ThingTypeUID> thingTypeUid = getThingTypeUID(deviceState);
if (thingTypeUid.isPresent()) {
createDiscoveryResult(deviceState, thingTypeUid.get());
} else {
logger.debug("Unsupported Miele device type: {}", deviceState.getType().orElse("<Empty>"));
}
}
private void createDiscoveryResult(DeviceState deviceState, ThingTypeUID thingTypeUid) {
MieleBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return;
}
ThingUID thingUid = new ThingUID(thingTypeUid, bridgeHandler.getThing().getUID(),
deviceState.getDeviceIdentifier());
DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUid)
.withBridge(bridgeHandler.getThing().getUID()).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
.withLabel(getLabel(deviceState));
ThingInformationExtractor.extractProperties(thingTypeUid, deviceState).entrySet()
.forEach(entry -> discoveryResultBuilder.withProperty(entry.getKey(), entry.getValue()));
DiscoveryResult result = discoveryResultBuilder.build();
thingDiscovered(result);
}
private Optional<ThingTypeUID> getThingTypeUID(DeviceState deviceState) {
switch (deviceState.getRawType()) {
case COFFEE_SYSTEM:
return Optional.of(THING_TYPE_COFFEE_SYSTEM);
case TUMBLE_DRYER:
return Optional.of(THING_TYPE_DRYER);
case WASHING_MACHINE:
return Optional.of(THING_TYPE_WASHING_MACHINE);
case WASHER_DRYER:
return Optional.of(THING_TYPE_WASHER_DRYER);
case FREEZER:
return Optional.of(THING_TYPE_FREEZER);
case FRIDGE:
return Optional.of(THING_TYPE_FRIDGE);
case FRIDGE_FREEZER_COMBINATION:
return Optional.of(THING_TYPE_FRIDGE_FREEZER);
case HOB_INDUCTION:
case HOB_HIGHLIGHT:
return Optional.of(THING_TYPE_HOB);
case DISHWASHER:
return Optional.of(THING_TYPE_DISHWASHER);
case OVEN:
case OVEN_MICROWAVE:
case STEAM_OVEN:
case STEAM_OVEN_COMBINATION:
case STEAM_OVEN_MICROWAVE_COMBINATION:
case DIALOGOVEN:
return Optional.of(THING_TYPE_OVEN);
case WINE_CABINET:
case WINE_STORAGE_CONDITIONING_UNIT:
case WINE_CONDITIONING_UNIT:
case WINE_CABINET_FREEZER_COMBINATION:
return Optional.of(THING_TYPE_WINE_STORAGE);
case HOOD:
return Optional.of(THING_TYPE_HOOD);
case DISH_WARMER:
return Optional.of(THING_TYPE_DISH_WARMER);
case VACUUM_CLEANER:
return Optional.of(THING_TYPE_ROBOTIC_VACUUM_CLEANER);
default:
if (deviceState.getRawType() != DeviceType.UNKNOWN) {
logger.warn("Found no matching thing type for device type {}", deviceState.getRawType());
}
return Optional.empty();
}
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Starting background discovery");
removeOlderResults(System.currentTimeMillis(), getBridgeUid());
discoveringDevices = true;
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stopping background discovery");
discoveringDevices = false;
}
/**
* Invoked when a device is removed from the Miele cloud.
*/
public void onDeviceRemoved(String deviceIdentifier) {
removeOlderResults(System.currentTimeMillis(), getBridgeUid());
}
private String getLabel(DeviceState deviceState) {
Optional<String> deviceName = deviceState.getDeviceName();
if (deviceName.isPresent()) {
return deviceName.get();
}
return ThingInformationExtractor.getDeviceAndTechType(deviceState).orElse("Miele Device");
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MieleBridgeHandler) {
var bridgeHandler = (MieleBridgeHandler) handler;
bridgeHandler.setDiscoveryService(this);
this.bridgeHandler = bridgeHandler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
}

View File

@ -0,0 +1,93 @@
/**
* 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.mielecloud.internal.discovery;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
/**
* Helper class extracting information related to things from {@link DeviceState}s received from the Miele cloud.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public final class ThingInformationExtractor {
private ThingInformationExtractor() {
throw new IllegalStateException(getClass().getName() + " cannot be instantiated");
}
/**
* Extracts thing properties from a {@link DeviceState}.
*
* The returned properties always contain {@link Thing#PROPERTY_SERIAL_NUMBER} and {@link Thing#PROPERTY_MODEL_ID}.
* More might be present depending on the type of device.
*
* @param thingTypeUid {@link ThingTypeUID} of the thing to extract properties for.
* @param deviceState {@link DeviceState} received from the Miele cloud.
* @return A {@link Map} holding the properties as key-value pairs.
*/
public static Map<String, String> extractProperties(ThingTypeUID thingTypeUid, DeviceState deviceState) {
var propertyMap = new HashMap<String, String>();
propertyMap.put(Thing.PROPERTY_SERIAL_NUMBER, getSerialNumber(deviceState));
propertyMap.put(Thing.PROPERTY_MODEL_ID, getModelId(deviceState));
propertyMap.put(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceState.getDeviceIdentifier());
if (MieleCloudBindingConstants.THING_TYPE_HOB.equals(thingTypeUid)) {
deviceState.getPlateStepCount().ifPresent(plateCount -> propertyMap
.put(MieleCloudBindingConstants.PROPERTY_PLATE_COUNT, plateCount.toString()));
}
return propertyMap;
}
private static String getSerialNumber(DeviceState deviceState) {
return deviceState.getFabNumber().orElse(deviceState.getDeviceIdentifier());
}
private static String getModelId(DeviceState deviceState) {
return getDeviceAndTechType(deviceState).orElse("Unknown");
}
/**
* Formats device type and tech type from the given {@link DeviceState} for the purpose of displaying then to the
* user.
*
* If either of device or tech type is missing then it will be omitted. If both are missing then an empty
* {@link Optional} will be returned.
*
* @param deviceState {@link DeviceState} obtained from the Miele cloud.
* @return An {@link Optional} holding the formatted value or an empty {@link Optional} if neither device type nor
* tech type were present.
*/
static Optional<String> getDeviceAndTechType(DeviceState deviceState) {
Optional<String> deviceType = deviceState.getType();
Optional<String> techType = deviceState.getTechType();
if (deviceType.isPresent() && techType.isPresent()) {
return Optional.of(deviceType.get() + " " + techType.get());
}
if (!deviceType.isPresent() && techType.isPresent()) {
return techType;
}
if (deviceType.isPresent() && !techType.isPresent()) {
return deviceType;
}
return Optional.empty();
}
}

View File

@ -0,0 +1,293 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
import static org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction.*;
import java.util.Optional;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
import org.openhab.binding.mielecloud.internal.discovery.ThingInformationExtractor;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for all Miele thing handlers.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
*/
@NonNullByDefault
public abstract class AbstractMieleThingHandler extends BaseThingHandler {
protected final ActionStateFetcher actionFetcher;
protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Creates a new {@link AbstractMieleThingHandler}.
*
* @param thing The thing to handle.
*/
public AbstractMieleThingHandler(Thing thing) {
super(thing);
this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
}
private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
return Optional.empty();
}
BridgeHandler handler = bridge.getHandler();
if (handler == null || !(handler instanceof MieleBridgeHandler)) {
return Optional.empty();
}
return Optional.of((MieleBridgeHandler) handler);
}
protected MieleWebservice getWebservice() {
return getMieleBridgeHandler().map(MieleBridgeHandler::getWebservice)
.orElse(UnavailableMieleWebservice.INSTANCE);
}
@Override
public void initialize() {
getWebservice().dispatchDeviceState(getDeviceId());
// If no device state update was received so far, set the device to OFFLINE.
if (getThing().getStatus() == ThingStatus.INITIALIZING) {
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH.equals(command)) {
updateDeviceState(new DeviceChannelState(latestDeviceState));
updateTransitionState(new TransitionChannelState(latestTransitionState));
updateActionState(new ActionsChannelState(latestActionsState));
}
switch (channelUID.getId()) {
case PROGRAM_START_STOP:
if (PROGRAM_STARTED.matches(command.toString())) {
triggerProcessAction(START);
} else if (PROGRAM_STOPPED.matches(command.toString())) {
triggerProcessAction(STOP);
}
break;
case PROGRAM_START_STOP_PAUSE:
if (PROGRAM_STARTED.matches(command.toString())) {
triggerProcessAction(START);
} else if (PROGRAM_STOPPED.matches(command.toString())) {
triggerProcessAction(STOP);
} else if (PROGRAM_PAUSED.matches(command.toString())) {
triggerProcessAction(PAUSE);
}
break;
case LIGHT_SWITCH:
if (command instanceof OnOffType) {
triggerLight(OnOffType.ON.equals(command));
}
break;
case POWER_ON_OFF:
if (POWER_ON.matches(command.toString()) || POWER_OFF.matches(command.toString())) {
triggerPowerState(OnOffType.ON.equals(OnOffType.from(command.toString())));
}
break;
}
}
@Override
public void dispose() {
}
/**
* Invoked when an update of the available actions for the device managed by this handler is received from the Miele
* cloud.
*/
public final void onProcessActionUpdated(ActionsState actionState) {
latestActionsState = actionState;
updateActionState(new ActionsChannelState(latestActionsState));
}
/**
* Invoked when the device managed by this handler was removed from the Miele cloud.
*/
public final void onDeviceRemoved() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, I18NKeys.THING_STATUS_DESCRIPTION_REMOVED);
}
/**
* Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
*/
public final void onDeviceStateUpdated(DeviceState deviceState) {
actionFetcher.onDeviceStateUpdated(deviceState);
latestTransitionState = new TransitionState(latestTransitionState, deviceState);
latestDeviceState = deviceState;
updateThingProperties(deviceState);
updateDeviceState(new DeviceChannelState(latestDeviceState));
updateTransitionState(new TransitionChannelState(latestTransitionState));
updateThingStatus(latestDeviceState);
}
protected void triggerProcessAction(final ProcessAction processAction) {
performPutAction(() -> getWebservice().putProcessAction(getDeviceId(), processAction),
t -> logger.warn("Failed to perform '{}' operation for device '{}'.", processAction, getDeviceId(), t));
}
protected void triggerLight(final boolean on) {
performPutAction(() -> getWebservice().putLight(getDeviceId(), on),
t -> logger.warn("Failed to set light state to '{}' for device '{}'.", on, getDeviceId(), t));
}
protected void triggerPowerState(final boolean on) {
performPutAction(() -> getWebservice().putPowerState(getDeviceId(), on),
t -> logger.warn("Failed to set the power state to '{}' for device '{}'.", on, getDeviceId(), t));
}
protected void triggerProgram(final long programId) {
performPutAction(() -> getWebservice().putProgram(getDeviceId(), programId), t -> logger
.warn("Failed to activate program with ID '{}' for device '{}'.", programId, getDeviceId(), t));
}
private void performPutAction(Runnable action, Consumer<Exception> onError) {
scheduler.execute(() -> {
try {
action.run();
} catch (TooManyRequestsException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
I18NKeys.THING_STATUS_DESCRIPTION_RATELIMIT);
onError.accept(e);
} catch (Exception e) {
onError.accept(e);
}
});
}
protected final String getDeviceId() {
return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString();
}
/**
* Creates a {@link ChannelUID} from the given name.
*
* @param name channel name
* @return {@link ChannelUID}
*/
protected ChannelUID channel(String name) {
return new ChannelUID(getThing().getUID(), name);
}
/**
* Updates the thing status depending on whether the managed device is connected and reachable.
*/
private void updateThingStatus(DeviceState deviceState) {
if (deviceState.isInState(StateType.NOT_CONNECTED)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
I18NKeys.THING_STATUS_DESCRIPTION_DISCONNECTED);
} else {
updateStatus(ThingStatus.ONLINE);
}
}
/**
* Determines the status of the currently selected program.
*/
protected ProgramStatus getProgramStatus(StateType rawStatus) {
if (rawStatus.equals(StateType.RUNNING)) {
return PROGRAM_STARTED;
}
return PROGRAM_STOPPED;
}
/**
* Determines the power status of the managed device.
*/
protected PowerStatus getPowerStatus(StateType rawStatus) {
if (rawStatus.equals(StateType.OFF) || rawStatus.equals(StateType.NOT_CONNECTED)) {
return POWER_OFF;
}
return POWER_ON;
}
/**
* Updates the thing properties. This is necessary if properties have not been set during discovery.
*/
private void updateThingProperties(DeviceState deviceState) {
var properties = editProperties();
properties.putAll(ThingInformationExtractor.extractProperties(getThing().getThingTypeUID(), deviceState));
updateProperties(properties);
}
/**
* Updates the device state channels.
*
* @param device The {@link DeviceChannelState} information to update the device channel states with.
*/
protected abstract void updateDeviceState(DeviceChannelState device);
/**
* Updates the transition state channels.
*
* @param transition The {@link TransitionChannelState} information to update the transition channel states with.
*/
protected abstract void updateTransitionState(TransitionChannelState transition);
/**
* Updates the device action state channels.
*
* @param action The {@link ActionsChannelState} information to update the action channel states with.
*/
protected abstract void updateActionState(ActionsChannelState actions);
}

View File

@ -0,0 +1,75 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele coffee devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Switch from polling to SSE, add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class CoffeeSystemThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link CoffeeSystemThingHandler}.
*
* @param thing The thing to handle.
*/
public CoffeeSystemThingHandler(Thing thing) {
super(thing);
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
}
}

View File

@ -0,0 +1,91 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
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;
/**
* ThingHandler implementation for the Miele cooling devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add door state and door alarm, add info state channel and map signal flags from API
*/
@NonNullByDefault
public class CoolingDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link CoolingDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public CoolingDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (!OnOffType.ON.equals(command) && !OnOffType.OFF.equals(command)) {
return;
}
switch (channelUID.getId()) {
case FRIDGE_SUPER_COOL:
triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERCOOLING
: ProcessAction.STOP_SUPERCOOLING);
break;
case FREEZER_SUPER_FREEZE:
triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERFREEZING
: ProcessAction.STOP_SUPERFREEZING);
break;
}
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(FRIDGE_SUPER_COOL), device.getFridgeSuperCool());
updateState(channel(FREEZER_SUPER_FREEZE), device.getFreezerSuperFreeze());
updateState(channel(FRIDGE_TEMPERATURE_TARGET), device.getFridgeTemperatureTarget());
updateState(channel(FREEZER_TEMPERATURE_TARGET), device.getFreezerTemperatureTarget());
updateState(channel(FRIDGE_TEMPERATURE_CURRENT), device.getFridgeTemperatureCurrent());
updateState(channel(FREEZER_TEMPERATURE_CURRENT), device.getFreezerTemperatureCurrent());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(DOOR_STATE), device.getDoorState());
updateState(channel(DOOR_ALARM), device.getDoorAlarm());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(SUPER_COOL_CAN_BE_CONTROLLED), actions.getSuperCoolCanBeControlled());
updateState(channel(SUPER_FREEZE_CAN_BE_CONTROLLED), actions.getSuperFreezeCanBeControlled());
}
}

View File

@ -0,0 +1,88 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ThingHandler implementation for Miele dish warmers.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class DishWarmerDeviceThingHandler extends AbstractMieleThingHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Creates a new {@link DishWarmerDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public DishWarmerDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (DISH_WARMER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
try {
triggerProgram(Long.parseLong(command.toString()));
} catch (NumberFormatException e) {
logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
}
}
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(DISH_WARMER_PROGRAM_ACTIVE), device.getProgramActiveId());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(DOOR_STATE), device.getDoorState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
}
}

View File

@ -0,0 +1,75 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele dishwasher devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class DishwasherDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link DishwasherDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public DishwasherDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(DOOR_STATE), device.getDoorState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
}
}

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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele dryer and washingDryer devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class DryerDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link DryerDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public DryerDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(DRYING_TARGET), device.getDryingTarget());
updateState(channel(DRYING_TARGET_RAW), device.getDryingTargetRaw());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
updateState(channel(DOOR_STATE), device.getDoorState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
}
}

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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele hob devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add plate step, add info state channel and map signal flags from API
*/
@NonNullByDefault
public class HobDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link HobDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public HobDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(PLATE_1_POWER_STEP), device.getPlateStep(0));
updateState(channel(PLATE_1_POWER_STEP_RAW), device.getPlateStepRaw(0));
updateState(channel(PLATE_2_POWER_STEP), device.getPlateStep(1));
updateState(channel(PLATE_2_POWER_STEP_RAW), device.getPlateStepRaw(1));
updateState(channel(PLATE_3_POWER_STEP), device.getPlateStep(2));
updateState(channel(PLATE_3_POWER_STEP_RAW), device.getPlateStepRaw(2));
updateState(channel(PLATE_4_POWER_STEP), device.getPlateStep(3));
updateState(channel(PLATE_4_POWER_STEP_RAW), device.getPlateStepRaw(3));
updateState(channel(PLATE_5_POWER_STEP), device.getPlateStep(4));
updateState(channel(PLATE_5_POWER_STEP_RAW), device.getPlateStepRaw(4));
updateState(channel(PLATE_6_POWER_STEP), device.getPlateStep(5));
updateState(channel(PLATE_6_POWER_STEP_RAW), device.getPlateStepRaw(5));
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
// No state transition required
}
@Override
protected void updateActionState(ActionsChannelState actions) {
// The Hob device has no trigger actions
}
}

View File

@ -0,0 +1,73 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele Hood devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
*/
@NonNullByDefault
public class HoodDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link HoodDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public HoodDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(VENTILATION_POWER), device.getVentilationPower());
updateState(channel(VENTILATION_POWER_RAW), device.getVentilationPowerRaw());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
}
}

View File

@ -0,0 +1,362 @@
/**
* 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.mielecloud.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
import org.openhab.binding.mielecloud.internal.auth.OAuthException;
import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefreshListener;
import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
import org.openhab.binding.mielecloud.internal.discovery.ThingDiscoveryService;
import org.openhab.binding.mielecloud.internal.util.EmailValidator;
import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
import org.openhab.binding.mielecloud.internal.webservice.ConnectionStatusListener;
import org.openhab.binding.mielecloud.internal.webservice.DeviceStateListener;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* BridgeHandler implementation for the Miele cloud account.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Introduced CombiningLanguageProvider field and interactions, added LanguageProvider super
* interface, switched from polling to SSE, added support for multiple bridges
*/
@NonNullByDefault
public class MieleBridgeHandler extends BaseBridgeHandler
implements OAuthTokenRefreshListener, LanguageProvider, ConnectionStatusListener, DeviceStateListener {
private static final int NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED = 6;
private final Supplier<MieleWebservice> webserviceFactory;
private final OAuthTokenRefresher tokenRefresher;
private final CombiningLanguageProvider languageProvider;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
private @Nullable MieleWebservice webService;
private @Nullable ThingDiscoveryService discoveryService;
/**
* Creates a new {@link MieleBridgeHandler}.
*
* @param bridge The bridge to handle.
* @param webserviceFactory Factory for creating {@link MieleWebservice} instances.
* @param tokenRefresher Token refresher.
* @param languageProvider Language provider.
*/
public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
super(bridge);
this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
this.tokenRefresher = tokenRefresher;
this.languageProvider = languageProvider;
}
public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
/**
* Gets the current webservice instance for communication with the Miele service.
*
* This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
*/
public MieleWebservice getWebservice() {
MieleWebservice webservice = webService;
if (webservice != null) {
return webservice;
} else {
return UnavailableMieleWebservice.INSTANCE;
}
}
private String getOAuthServiceHandle() {
return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
}
@Override
public void initialize() {
// It is required to set a status in this method as stated in the Javadoc of ThingHandler.initialize
updateStatus(ThingStatus.UNKNOWN);
initializeWebservice();
}
public void initializeWebservice() {
if (!EmailValidator.isValid(getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
// When the e-mail configuration is changed a new initialization will be triggered. Therefore, we can leave
// the bridge in this state.
return;
}
try {
webService = webserviceFactory.get();
} catch (MieleWebserviceInitializationException e) {
logger.warn("Failed to initialize webservice.", e);
updateStatus(ThingStatus.OFFLINE);
return;
}
try {
tokenRefresher.setRefreshListener(this, getOAuthServiceHandle());
} catch (OAuthException e) {
logger.debug("Could not initialize Miele Cloud bridge.", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
// When the authorization takes place a new initialization will be triggered. Therefore, we can leave the
// bridge in this state.
return;
}
languageProvider.setPrioritizedLanguageProvider(this);
tryInitializeWebservice();
MieleWebservice webservice = getWebservice();
webservice.addConnectionStatusListener(this);
webservice.addDeviceStateListener(this);
if (webservice.hasAccessToken()) {
webservice.connectSse();
}
}
@Override
public void handleRemoval() {
performLogout();
tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
super.handleRemoval();
}
@Override
public void dispose() {
logger.debug("Disposing {}", this.getClass().getName());
disposeWebservice();
}
public void disposeWebservice() {
getWebservice().removeConnectionStatusListener(this);
getWebservice().removeDeviceStateListener(this);
getWebservice().disconnectSse();
languageProvider.unsetPrioritizedLanguageProvider();
tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
stopWebservice();
}
private void stopWebservice() {
final MieleWebservice webService = this.webService;
this.webService = null;
if (webService == null) {
return;
}
scheduler.submit(() -> {
CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
if (logoutFuture != null) {
try {
logoutFuture.get();
} catch (InterruptedException e) {
logger.warn("Interrupted while waiting for logout!");
} catch (ExecutionException e) {
logger.warn("Failed to wait for logout.", e);
}
}
try {
webService.close();
} catch (Exception e) {
logger.warn("Failed to close webservice.", e);
}
});
}
@Override
public void onNewAccessToken(String accessToken) {
logger.debug("Setting new access token for webservice access.");
updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
// Without this the retry would fail causing the thing to go OFFLINE
getWebservice().setAccessToken(accessToken);
// If there was no access token during initialization then the SSE connection was not established.
getWebservice().connectSse();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
private void performLogout() {
logoutFuture = new CompletableFuture<>();
scheduler.execute(() -> {
try {
getWebservice().logout();
} catch (Exception e) {
logger.warn("Failed to logout from Miele cloud.", e);
}
Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
});
}
private void tryInitializeWebservice() {
Optional<String> accessToken = tokenRefresher.getAccessTokenFromStorage(getOAuthServiceHandle());
if (!accessToken.isPresent()) {
logger.debug("No OAuth2 access token available. Retrying later.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
return;
}
getWebservice().setAccessToken(accessToken.get());
updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
}
@Override
public void onConnectionAlive() {
updateStatus(ThingStatus.ONLINE);
}
@Override
public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
tryToRefreshAccessToken();
return;
}
if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
&& getThing().getStatus() != ThingStatus.UNKNOWN) {
return;
}
if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
&& failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
return;
}
switch (connectionError) {
case AUTHORIZATION_FAILED:
// Handled above.
break;
case REQUEST_EXECUTION_FAILED:
case SERVICE_UNAVAILABLE:
case RESPONSE_MALFORMED:
case TIMEOUT:
case TOO_MANY_RERQUESTS:
case SSE_STREAM_ENDED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
break;
case SERVER_ERROR:
case REQUEST_INTERRUPTED:
case OTHER_HTTP_ERROR:
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
break;
}
}
private void tryToRefreshAccessToken() {
try {
tokenRefresher.refreshToken(getOAuthServiceHandle());
getWebservice().connectSse();
} catch (OAuthException e) {
logger.debug("Failed to refresh OAuth token!", e);
getWebservice().disconnectSse();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
}
}
@Override
public Optional<String> getLanguage() {
Object languageObject = thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
if (languageObject instanceof String) {
String language = (String) languageObject;
if (language.isEmpty() || !LocaleValidator.isValidLanguage(language)) {
return Optional.empty();
} else {
return Optional.of(language);
}
} else {
return Optional.empty();
}
}
@Override
public void onDeviceStateUpdated(DeviceState deviceState) {
ThingDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.onDeviceStateUpdated(deviceState);
}
invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
}
@Override
public void onProcessActionUpdated(ActionsState actionState) {
invokeOnThingHandlers(actionState.getDeviceIdentifier(),
handler -> handler.onProcessActionUpdated(actionState));
}
@Override
public void onDeviceRemoved(String deviceIdentifier) {
ThingDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.onDeviceRemoved(deviceIdentifier);
}
invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
}
private void invokeOnThingHandlers(String deviceIdentifier, Consumer<AbstractMieleThingHandler> action) {
getThing().getThings().stream().map(Thing::getHandler)
.filter(handler -> handler instanceof AbstractMieleThingHandler)
.map(handler -> (AbstractMieleThingHandler) handler)
.filter(handler -> deviceIdentifier.equals(handler.getDeviceId())).forEach(action);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ThingDiscoveryService.class);
}
}

View File

@ -0,0 +1,123 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebserviceFactory;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceConfiguration;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
import org.openhab.core.i18n.LocaleProvider;
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;
/**
* Factory producing the {@link ThingHandler}s for all things supported by this binding.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Added language provider, added support for multiple bridges
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.mielecloud")
public class MieleHandlerFactory extends BaseThingHandlerFactory {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE, THING_TYPE_WASHING_MACHINE,
THING_TYPE_WASHER_DRYER, THING_TYPE_COFFEE_SYSTEM, THING_TYPE_FRIDGE_FREEZER, THING_TYPE_FRIDGE,
THING_TYPE_FREEZER, THING_TYPE_OVEN, THING_TYPE_WINE_STORAGE, THING_TYPE_HOB, THING_TYPE_DRYER,
THING_TYPE_DISHWASHER, THING_TYPE_HOOD, THING_TYPE_DISH_WARMER, THING_TYPE_ROBOTIC_VACUUM_CLEANER);
private final HttpClientFactory httpClientFactory;
private final OAuthTokenRefresher tokenRefresher;
private final LocaleProvider localeProvider;
private final MieleWebserviceFactory webserviceFactory = new DefaultMieleWebserviceFactory();
@Activate
public MieleHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference OAuthTokenRefresher tokenRefresher, @Reference LocaleProvider localeProvider) {
this.httpClientFactory = httpClientFactory;
this.tokenRefresher = tokenRefresher;
this.localeProvider = localeProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
@Nullable
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
return createBridgeHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_WASHING_MACHINE) || thingTypeUID.equals(THING_TYPE_WASHER_DRYER)) {
return new WashingDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_COFFEE_SYSTEM)) {
return new CoffeeSystemThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_FRIDGE_FREEZER) || thingTypeUID.equals(THING_TYPE_FRIDGE)
|| thingTypeUID.equals(THING_TYPE_FREEZER)) {
return new CoolingDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_WINE_STORAGE)) {
return new WineStorageDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_OVEN)) {
return new OvenDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_HOB)) {
return new HobDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_DISHWASHER)) {
return new DishwasherDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_DRYER)) {
return new DryerDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_HOOD)) {
return new HoodDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_DISH_WARMER)) {
return new DishWarmerDeviceThingHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_ROBOTIC_VACUUM_CLEANER)) {
return new RoboticVacuumCleanerDeviceThingHandler(thing);
}
return null;
}
private ThingHandler createBridgeHandler(Thing thing) {
CombiningLanguageProvider languageProvider = getLanguageProvider();
Function<ScheduledExecutorService, MieleWebservice> webserviceFactoryFunction = scheduler -> webserviceFactory
.create(MieleWebserviceConfiguration.builder().withHttpClientFactory(httpClientFactory)
.withLanguageProvider(languageProvider).withTokenRefresher(tokenRefresher)
.withServiceHandle(thing.getUID().getAsString()).withScheduler(scheduler).build());
return new MieleBridgeHandler((Bridge) thing, webserviceFactoryFunction, tokenRefresher, languageProvider);
}
private CombiningLanguageProvider getLanguageProvider() {
return new CombiningLanguageProvider(null, new OpenHabLanguageProvider(localeProvider));
}
}

View File

@ -0,0 +1,80 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele oven devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add pre-heat finished channel, add info state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class OvenDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link OvenDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public OvenDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(PRE_HEAT_FINISHED), device.hasPreHeatFinished());
updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
updateState(channel(DOOR_STATE), device.getDoorState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
}
}

View File

@ -0,0 +1,86 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ThingHandler implementation for Miele robotic vacuum cleaners.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class RoboticVacuumCleanerDeviceThingHandler extends AbstractMieleThingHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Creates a new {@link RoboticVacuumCleanerDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public RoboticVacuumCleanerDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (VACUUM_CLEANER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
try {
triggerProgram(Long.parseLong(command.toString()));
} catch (NumberFormatException e) {
logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
}
}
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(VACUUM_CLEANER_PROGRAM_ACTIVE), device.getProgramActiveId());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_START_STOP_PAUSE), device.getProgramStartStopPause());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(BATTERY_LEVEL), device.getBatteryLevel());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
updateState(channel(REMOTE_CONTROL_CAN_BE_PAUSED), actions.getRemoteControlCanBePaused());
updateState(channel(REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE), actions.getRemoteControlCanSetProgramActive());
}
}

View File

@ -0,0 +1,80 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele washing devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class WashingDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link WashingDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public WashingDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(SPINNING_SPEED), device.getSpinningSpeed());
updateState(channel(SPINNING_SPEED_RAW), device.getSpinningSpeedRaw());
updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
updateState(channel(DOOR_STATE), device.getDoorState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
if (transition.hasFinishedChanged()) {
updateState(channel(FINISH_STATE), transition.getFinishState());
}
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
}
}

View File

@ -0,0 +1,75 @@
/**
* 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.mielecloud.internal.handler;
import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
/**
* ThingHandler implementation for the Miele wine storage devices.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Add channel state wrappers
* @author Benjamin Bolte - Add info state channel and map signal flags from API
*/
@NonNullByDefault
public class WineStorageDeviceThingHandler extends AbstractMieleThingHandler {
/**
* Creates a new {@link WineStorageDeviceThingHandler}.
*
* @param thing The thing to handle.
*/
public WineStorageDeviceThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
}
@Override
protected void updateDeviceState(DeviceChannelState device) {
updateState(channel(OPERATION_STATE), device.getOperationState());
updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
updateState(channel(TEMPERATURE_TARGET), device.getWineTemperatureTarget());
updateState(channel(TEMPERATURE_CURRENT), device.getWineTemperatureCurrent());
updateState(channel(TOP_TEMPERATURE_TARGET), device.getWineTopTemperatureTarget());
updateState(channel(TOP_TEMPERATURE_CURRENT), device.getWineTopTemperatureCurrent());
updateState(channel(MIDDLE_TEMPERATURE_TARGET), device.getWineMiddleTemperatureTarget());
updateState(channel(MIDDLE_TEMPERATURE_CURRENT), device.getWineMiddleTemperatureCurrent());
updateState(channel(BOTTOM_TEMPERATURE_TARGET), device.getWineBottomTemperatureTarget());
updateState(channel(BOTTOM_TEMPERATURE_CURRENT), device.getWineBottomTemperatureCurrent());
updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
updateState(channel(ERROR_STATE), device.getErrorState());
updateState(channel(INFO_STATE), device.getInfoState());
}
@Override
protected void updateTransitionState(TransitionChannelState transition) {
}
@Override
protected void updateActionState(ActionsChannelState actions) {
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
}
}

View File

@ -0,0 +1,68 @@
/**
* 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.mielecloud.internal.handler.channel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
/**
* Wrapper for {@link ActionsState} handling the type conversion to {@link State} for directly filling channels.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ActionsChannelState {
private final ActionsState actions;
public ActionsChannelState(ActionsState actions) {
this.actions = actions;
}
public State getRemoteControlCanBeSwitchedOn() {
return OnOffType.from(actions.canBeSwitchedOn());
}
public State getRemoteControlCanBeSwitchedOff() {
return OnOffType.from(actions.canBeSwitchedOff());
}
public State getLightCanBeControlled() {
return OnOffType.from(actions.canControlLight());
}
public State getSuperCoolCanBeControlled() {
return OnOffType.from(actions.canContolSupercooling());
}
public State getSuperFreezeCanBeControlled() {
return OnOffType.from(actions.canControlSuperfreezing());
}
public State getRemoteControlCanBeStarted() {
return OnOffType.from(actions.canBeStarted());
}
public State getRemoteControlCanBeStopped() {
return OnOffType.from(actions.canBeStopped());
}
public State getRemoteControlCanBePaused() {
return OnOffType.from(actions.canBePaused());
}
public State getRemoteControlCanSetProgramActive() {
return OnOffType.from(actions.canSetActiveProgramId());
}
}

View File

@ -0,0 +1,73 @@
/**
* 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.mielecloud.internal.handler.channel;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Utility class handling type conversions from Java types to channel types.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class ChannelTypeUtil {
private ChannelTypeUtil() {
throw new IllegalStateException("ChannelTypeUtil cannot be instantiated.");
}
/**
* Converts an {@link Optional} of {@link String} to {@link State}.
*/
public static State stringToState(Optional<String> value) {
return value.filter(v -> !v.isEmpty()).filter(v -> !v.equals("null")).map(v -> (State) new StringType(v))
.orElse(UnDefType.UNDEF);
}
/**
* Converts an {@link Optional} of {@link Boolean} to {@link State}.
*/
public static State booleanToState(Optional<Boolean> value) {
return value.map(v -> (State) OnOffType.from(v)).orElse(UnDefType.UNDEF);
}
/**
* Converts an {@link Optional} of {@link Integer} to {@link State}.
*/
public static State intToState(Optional<Integer> value) {
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
}
/**
* Converts an {@link Optional} of {@link Long} to {@link State}.
*/
public static State longToState(Optional<Long> value) {
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
}
/**
* Converts an {@link Optional} of {@link Integer} to {@link State} representing a temperature.
*/
public static State intToTemperatureState(Optional<Integer> value) {
// The Miele 3rd Party API always provides temperatures in °C (even if the device uses another unit).
return value.map(v -> (State) new QuantityType<>(v, SIUnits.CELSIUS)).orElse(UnDefType.UNDEF);
}
}

View File

@ -0,0 +1,269 @@
/**
* 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.mielecloud.internal.handler.channel;
import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
import java.util.Arrays;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.CoolingDeviceTemperatureState;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
import org.openhab.binding.mielecloud.internal.webservice.api.WineStorageDeviceTemperatureState;
import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
/**
* Wrapper for {@link DeviceState} handling the type conversion to {@link State} for directly filling channels.
*
* @author Björn Lange - Initial contribution
* @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm and info state channel and map
* signal flags from API
* @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner thing
*/
@NonNullByDefault
public final class DeviceChannelState {
private final DeviceState device;
private final CoolingDeviceTemperatureState coolingTemperature;
private final WineStorageDeviceTemperatureState wineTemperature;
public DeviceChannelState(DeviceState device) {
this.device = device;
this.coolingTemperature = new CoolingDeviceTemperatureState(device);
this.wineTemperature = new WineStorageDeviceTemperatureState(device);
}
public State getLightSwitch() {
return ChannelTypeUtil.booleanToState(device.getLightState());
}
public State getDoorState() {
return ChannelTypeUtil.booleanToState(device.getDoorState());
}
public State getDoorAlarm() {
return ChannelTypeUtil.booleanToState(device.getDoorAlarm());
}
public State getErrorState() {
return OnOffType.from(device.hasError());
}
public State getInfoState() {
return OnOffType.from(device.hasInfo());
}
public State getPowerOnOff() {
return new StringType(getPowerStatus().getState());
}
public State getProgramElapsedTime() {
return ChannelTypeUtil.intToState(device.getElapsedTime());
}
public State getOperationState() {
return ChannelTypeUtil.stringToState(device.getStatus());
}
public State getOperationStateRaw() {
return ChannelTypeUtil.intToState(device.getStatusRaw());
}
public State getProgramPhase() {
return ChannelTypeUtil.stringToState(device.getProgramPhase());
}
public State getProgramPhaseRaw() {
return ChannelTypeUtil.intToState(device.getProgramPhaseRaw());
}
public State getProgramActive() {
return ChannelTypeUtil.stringToState(device.getSelectedProgram());
}
public State getProgramActiveRaw() {
return ChannelTypeUtil.longToState(device.getSelectedProgramId());
}
public State getProgramActiveId() {
return ChannelTypeUtil.stringToState(device.getSelectedProgramId().map(Object::toString));
}
public State getFridgeSuperCool() {
return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERCOOLING, StateType.SUPERCOOLING_SUPERFREEZING));
}
public State getFreezerSuperFreeze() {
return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERFREEZING, StateType.SUPERCOOLING_SUPERFREEZING));
}
public State getFridgeTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTargetTemperature());
}
public State getFreezerTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTargetTemperature());
}
public State getFridgeTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTemperature());
}
public State getFreezerTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTemperature());
}
public State getProgramStartStop() {
return new StringType(getProgramStartStopStatus().getState());
}
public State getProgramStartStopPause() {
return new StringType(getProgramStartStopPauseStatus().getState());
}
public State getDelayedStartTime() {
return ChannelTypeUtil.intToState(device.getStartTime());
}
public State getDryingTarget() {
return ChannelTypeUtil.stringToState(device.getDryingTarget());
}
public State getDryingTargetRaw() {
return ChannelTypeUtil.intToState(device.getDryingTargetRaw());
}
public State hasPreHeatFinished() {
return ChannelTypeUtil.booleanToState(device.hasPreHeatFinished());
}
public State getTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(device.getTargetTemperature(0));
}
public State getVentilationPower() {
return ChannelTypeUtil.stringToState(device.getVentilationStep());
}
public State getVentilationPowerRaw() {
return ChannelTypeUtil.intToState(device.getVentilationStepRaw());
}
public State getPlateStep(int index) {
return ChannelTypeUtil.stringToState(device.getPlateStep(index));
}
public State getPlateStepRaw(int index) {
return ChannelTypeUtil.intToState(device.getPlateStepRaw(index));
}
public State getTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(device.getTemperature(0));
}
public State getSpinningSpeed() {
return ChannelTypeUtil.stringToState(device.getSpinningSpeed());
}
public State getSpinningSpeedRaw() {
return ChannelTypeUtil.intToState(device.getSpinningSpeedRaw());
}
public State getBatteryLevel() {
return ChannelTypeUtil.intToState(device.getBatteryLevel());
}
public State getWineTemperatureTarget() {
return ChannelTypeUtil.intToState(wineTemperature.getTargetTemperature());
}
public State getWineTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTemperature());
}
public State getWineTopTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTargetTemperature());
}
public State getWineTopTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTemperature());
}
public State getWineMiddleTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTargetTemperature());
}
public State getWineMiddleTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTemperature());
}
public State getWineBottomTemperatureTarget() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTargetTemperature());
}
public State getWineBottomTemperatureCurrent() {
return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTemperature());
}
/**
* Determines the status of the currently selected program.
*/
private PowerStatus getPowerStatus() {
if (device.isInState(StateType.OFF) || device.isInState(StateType.NOT_CONNECTED)) {
return POWER_OFF;
} else {
return POWER_ON;
}
}
/**
* Determines the status of the currently selected program respecting the possibilities started and stopped.
*/
protected ProgramStatus getProgramStartStopStatus() {
if (device.isInState(StateType.RUNNING)) {
return PROGRAM_STARTED;
} else {
return PROGRAM_STOPPED;
}
}
/**
* Determines the status of the currently selected program respecting the possibilities started, stopped and paused.
*/
protected ProgramStatus getProgramStartStopPauseStatus() {
if (device.isInState(StateType.RUNNING)) {
return PROGRAM_STARTED;
} else if (device.isInState(StateType.PAUSE)) {
return PROGRAM_PAUSED;
} else {
return PROGRAM_STOPPED;
}
}
/**
* Gets whether the device is in one of the given states.
*
* @param stateType The states to check.
* @return An empty {@link Optional} if the raw status is unknown, otherwise an {@link Optional} with a value
* indicating whether the device is in one of the given states.
*/
private Optional<Boolean> isInState(StateType... stateType) {
return device.getStateType().map(it -> Arrays.asList(stateType).contains(it));
}
}

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.mielecloud.internal.handler.channel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
import org.openhab.core.types.State;
/**
* Wrapper for {@link TransitionState} handling the type conversion to {@link State} for directly filling channels.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class TransitionChannelState {
private final TransitionState transition;
public TransitionChannelState(TransitionState transition) {
this.transition = transition;
}
public boolean hasFinishedChanged() {
return transition.hasFinishedChanged();
}
public State getFinishState() {
return ChannelTypeUtil.booleanToState(transition.isFinished());
}
public State getProgramRemainingTime() {
return ChannelTypeUtil.intToState(transition.getRemainingTime());
}
public State getProgramProgress() {
return ChannelTypeUtil.intToState(transition.getProgress());
}
}

View File

@ -0,0 +1,35 @@
/**
* 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.mielecloud.internal.util;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility for validating e-mail addresses.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class EmailValidator {
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w-_\\.+]*[\\w-_\\.]\\@([\\w]+\\.)+[\\w]+[\\w]$");
private EmailValidator() {
throw new UnsupportedOperationException();
}
public static boolean isValid(String emailAddress) {
return EMAIL_PATTERN.matcher(emailAddress).matches();
}
}

View File

@ -0,0 +1,45 @@
/**
* 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.mielecloud.internal.util;
import java.util.Locale;
import java.util.MissingResourceException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility for validating locales.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class LocaleValidator {
private LocaleValidator() {
throw new UnsupportedOperationException();
}
/**
* Checks whether the given string is a valid two letter language code.
*
* @param language The string to check.
* @return Whether it is a valid language.
*/
public static boolean isValidLanguage(String language) {
try {
String iso3Language = new Locale(language).getISO3Language();
return iso3Language != null && !iso3Language.isEmpty();
} catch (MissingResourceException e) {
return false;
}
}
}

View File

@ -0,0 +1,81 @@
/**
* 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.mielecloud.internal.webservice;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ActionStateFetcher} fetches the updated actions state for a device from the {@link MieleWebservice} if
* the state of that device changed.
*
* Note that an instance of this class is required for each device.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Make calls to webservice asynchronous
*/
@NonNullByDefault
public class ActionStateFetcher {
private Optional<DeviceState> lastDeviceState = Optional.empty();
private final Supplier<MieleWebservice> webserviceSupplier;
private final ScheduledExecutorService scheduler;
private final Logger logger = LoggerFactory.getLogger(ActionStateFetcher.class);
/**
* Creates a new {@link ActionStateFetcher}.
*
* @param webserviceSupplier Getter function for access to the {@link MieleWebservice}.
* @param scheduler System-wide scheduler.
*/
public ActionStateFetcher(Supplier<MieleWebservice> webserviceSupplier, ScheduledExecutorService scheduler) {
this.webserviceSupplier = webserviceSupplier;
this.scheduler = scheduler;
}
/**
* Invoked when the state of a device was updated.
*/
public void onDeviceStateUpdated(DeviceState deviceState) {
if (hasDeviceStatusChanged(deviceState)) {
scheduler.submit(() -> fetchActions(deviceState));
}
lastDeviceState = Optional.of(deviceState);
}
private boolean hasDeviceStatusChanged(DeviceState newDeviceState) {
return lastDeviceState.map(DeviceState::getStateType)
.map(rawStatus -> !newDeviceState.getStateType().equals(rawStatus)).orElse(true);
}
private void fetchActions(DeviceState deviceState) {
try {
webserviceSupplier.get().fetchActions(deviceState.getDeviceIdentifier());
} catch (MieleWebserviceException e) {
logger.warn("Failed to fetch action state for device {}: {} - {}", deviceState.getDeviceIdentifier(),
e.getConnectionError(), e.getMessage());
} catch (AuthorizationFailedException | TooManyRequestsException e) {
logger.warn("Failed to fetch action state for device {}: {}", deviceState.getDeviceIdentifier(),
e.getMessage());
}
}
}

View File

@ -0,0 +1,35 @@
/**
* 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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ConnectionError} enumeration represents the error state of a connection to the Miele cloud.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public enum ConnectionError {
SERVER_ERROR,
SERVICE_UNAVAILABLE,
OTHER_HTTP_ERROR,
REQUEST_INTERRUPTED,
TIMEOUT,
REQUEST_EXECUTION_FAILED,
RESPONSE_MALFORMED,
AUTHORIZATION_FAILED,
TOO_MANY_RERQUESTS,
SSE_STREAM_ENDED,
UNKNOWN,
}

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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Listener for the connection status.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public interface ConnectionStatusListener {
/**
* Called regularly while the connection is up and running.
*/
void onConnectionAlive();
/**
* Called when a connection error is encountered.
*
* @param connectionError The error.
* @param failedReconnectAttempts The number of failed attempts to reconnect.
*/
void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts);
}

View File

@ -0,0 +1,355 @@
/**
* 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.mielecloud.internal.webservice;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
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.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactoryImpl;
import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
import org.openhab.binding.mielecloud.internal.webservice.sse.SseConnection;
import org.openhab.binding.mielecloud.internal.webservice.sse.SseListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* Default implementation of the {@link MieleWebservice}.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public final class DefaultMieleWebservice implements MieleWebservice, SseListener {
private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
public static final String THIRD_PARTY_ENDPOINTS_BASENAME = SERVER_ADDRESS + "/thirdparty";
private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + "%s" + "/actions";
private static final String ENDPOINT_LOGOUT = THIRD_PARTY_ENDPOINTS_BASENAME + "/logout";
private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
private static final String SSE_EVENT_TYPE_DEVICES = "devices";
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(DefaultMieleWebservice.class);
private Optional<String> accessToken = Optional.empty();
private final RequestFactory requestFactory;
private final DeviceStateDispatcher deviceStateDispatcher;
private final List<ConnectionStatusListener> connectionStatusListeners = new ArrayList<>();
private final RetryStrategy retryStrategy;
private final SseConnection sseConnection;
/**
* Creates a new {@link DefaultMieleWebservice} with default retry configuration which is to retry failed operations
* once on a transient error. In case an authorization error occurs, a new access token is requested and a retry of
* the failed request is executed.
*
* @param configuration The configuration holding all parameters for constructing the instance.
* @throws MieleWebserviceInitializationException if initializing the HTTP client fails.
*/
public DefaultMieleWebservice(MieleWebserviceConfiguration configuration) {
this(new RequestFactoryImpl(configuration.getHttpClientFactory(), configuration.getLanguageProvider()),
new RetryStrategyCombiner(new NTimesRetryStrategy(1),
new AuthorizationFailedRetryStrategy(configuration.getTokenRefresher(),
configuration.getServiceHandle())),
new DeviceStateDispatcher(), configuration.getScheduler());
}
/**
* This constructor only exists for testing.
*/
DefaultMieleWebservice(RequestFactory requestFactory, RetryStrategy retryStrategy,
DeviceStateDispatcher deviceStateDispatcher, ScheduledExecutorService scheduler) {
this.requestFactory = requestFactory;
this.retryStrategy = retryStrategy;
this.deviceStateDispatcher = deviceStateDispatcher;
this.sseConnection = new SseConnection(ENDPOINT_ALL_SSE_EVENTS, this::createSseRequest, scheduler);
this.sseConnection.addSseListener(this);
}
@Override
public void setAccessToken(String accessToken) {
this.accessToken = Optional.of(accessToken);
}
@Override
public boolean hasAccessToken() {
return accessToken.isPresent();
}
@Override
public synchronized void connectSse() {
sseConnection.connect();
}
@Override
public synchronized void disconnectSse() {
sseConnection.disconnect();
}
@Nullable
private Request createSseRequest(String endpoint) {
Optional<String> accessToken = this.accessToken;
if (!accessToken.isPresent()) {
logger.warn("No access token present.");
return null;
}
return requestFactory.createSseRequest(endpoint, accessToken.get());
}
@Override
public void onServerSentEvent(ServerSentEvent event) {
fireConnectionAlive();
if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
return;
}
try {
deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
} catch (MieleSyntaxException e) {
logger.warn("SSE payload is not valid Json: {}", event.getData());
}
}
private void fireConnectionAlive() {
connectionStatusListeners.forEach(ConnectionStatusListener::onConnectionAlive);
}
@Override
public void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts) {
connectionStatusListeners.forEach(l -> l.onConnectionError(connectionError, failedReconnectAttempts));
}
@Override
public void fetchActions(String deviceId) {
Actions actions = retryStrategy.performRetryableOperation(() -> getActions(deviceId),
e -> logger.warn("Cannot poll action state: {}. Retrying...", e.getMessage()));
if (actions != null) {
deviceStateDispatcher.dispatchActionStateUpdates(deviceId, actions);
} else {
logger.warn("Cannot poll action state. Response is missing actions.");
}
}
@Override
public void putProcessAction(String deviceId, ProcessAction processAction) {
if (processAction.equals(ProcessAction.UNKNOWN)) {
throw new IllegalArgumentException("Process action must not be UNKNOWN.");
}
String formattedProcessAction = GSON.toJson(processAction, ProcessAction.class);
formattedProcessAction = formattedProcessAction.substring(1, formattedProcessAction.length() - 1);
String json = "{\"processAction\":" + formattedProcessAction + "}";
logger.debug("Activate process action {} of Miele device {}", processAction.toString(), deviceId);
putActions(deviceId, json);
}
@Override
public void putLight(String deviceId, boolean enabled) {
Light light = enabled ? Light.ENABLE : Light.DISABLE;
String json = "{\"light\":" + light.format() + "}";
logger.debug("Set light of Miele device {} to {}", deviceId, enabled);
putActions(deviceId, json);
}
@Override
public void putPowerState(String deviceId, boolean enabled) {
String action = enabled ? "powerOn" : "powerOff";
String json = "{\"" + action + "\":true}";
logger.debug("Set power state of Miele device {} to {}", deviceId, action);
putActions(deviceId, json);
}
@Override
public void putProgram(String deviceId, long programId) {
String json = "{\"programId\":" + programId + "}";
logger.debug("Activate program with ID {} of Miele device {}", programId, deviceId);
putActions(deviceId, json);
}
@Override
public void logout() {
Optional<String> accessToken = this.accessToken;
if (!accessToken.isPresent()) {
logger.debug("No access token present.");
return;
}
try {
logger.debug("Invalidating Miele webservice access token.");
Request request = requestFactory.createPostRequest(ENDPOINT_LOGOUT, accessToken.get());
this.accessToken = Optional.empty();
sendRequest(request);
} catch (MieleWebserviceTransientException e) {
throw new MieleWebserviceException("Transient error occurred during logout.", e, e.getConnectionError());
}
}
/**
* Sends the given request and wraps the possible exceptions in Miele exception types.
*
* @param request The {@link Request} to send.
* @return The obtained {@link ContentResponse}.
* @throws MieleWebserviceException if an irrecoverable error occurred.
* @throws MieleWebserviceTransientException if a recoverable error occurred.
*/
private ContentResponse sendRequest(Request request) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Send {} request to Miele webservice on uri {}",
Optional.ofNullable(request).map(Request::getMethod).orElse("null"),
Optional.ofNullable(request).map(Request::getURI).map(URI::toString).orElse("null"));
}
ContentResponse response = request.send();
logger.debug("Received response with status code {}", response.getStatus());
return response;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MieleWebserviceException("Interrupted.", e, ConnectionError.REQUEST_INTERRUPTED);
} catch (TimeoutException e) {
throw new MieleWebserviceTransientException("Request timed out.", e, ConnectionError.TIMEOUT);
} catch (ExecutionException e) {
throw new MieleWebserviceException("Request execution failed.", e,
ConnectionError.REQUEST_EXECUTION_FAILED);
}
}
/**
* Gets all available device actions.
*
* @param deviceId The unique device ID.
*
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
* is recoverable by retrying the operation.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
private Actions getActions(String deviceId) {
Optional<String> accessToken = this.accessToken;
if (!accessToken.isPresent()) {
throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
}
try {
logger.debug("Fetch action state description for Miele device {}", deviceId);
Request request = requestFactory.createGetRequest(String.format(ENDPOINT_ACTIONS, deviceId),
accessToken.get());
ContentResponse response = sendRequest(request);
HttpUtil.checkHttpSuccess(response);
Actions actions = GSON.fromJson(response.getContentAsString(), Actions.class);
if (actions == null) {
throw new MieleWebserviceTransientException("Failed to parse response message.",
ConnectionError.RESPONSE_MALFORMED);
}
return actions;
} catch (JsonSyntaxException e) {
throw new MieleWebserviceTransientException("Failed to parse response message.", e,
ConnectionError.RESPONSE_MALFORMED);
}
}
/**
* Performs a PUT request to the actions endpoint for the specified device.
*
* @param deviceId The ID of the device to PUT for.
* @param json The Json body to send with the request.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
* is recoverable by retrying the operation.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
private void putActions(String deviceId, String json) {
retryStrategy.performRetryableOperation(() -> {
Optional<String> accessToken = this.accessToken;
if (!accessToken.isPresent()) {
throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
}
Request request = requestFactory.createPutRequest(String.format(ENDPOINT_ACTIONS, deviceId),
accessToken.get(), json);
ContentResponse response = sendRequest(request);
HttpUtil.checkHttpSuccess(response);
}, e -> {
logger.warn("Failed to perform PUT request: {}. Retrying...", e.getMessage());
});
}
@Override
public void dispatchDeviceState(String deviceIdentifier) {
deviceStateDispatcher.dispatchDeviceState(deviceIdentifier);
}
@Override
public void addDeviceStateListener(DeviceStateListener listener) {
deviceStateDispatcher.addListener(listener);
}
@Override
public void removeDeviceStateListener(DeviceStateListener listener) {
deviceStateDispatcher.removeListener(listener);
}
@Override
public void addConnectionStatusListener(ConnectionStatusListener listener) {
connectionStatusListeners.add(listener);
}
@Override
public void removeConnectionStatusListener(ConnectionStatusListener listener) {
connectionStatusListeners.remove(listener);
}
@Override
public void close() throws Exception {
requestFactory.close();
}
}

View File

@ -0,0 +1,28 @@
/**
* 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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Factory creating {@link DefaultMieleWebservice} instances.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class DefaultMieleWebserviceFactory implements MieleWebserviceFactory {
@Override
public MieleWebservice create(MieleWebserviceConfiguration configuration) {
return new DefaultMieleWebservice(configuration);
}
}

View File

@ -0,0 +1,49 @@
/**
* 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.mielecloud.internal.webservice;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
/**
* A cache for {@link Device} objects associated with unique identifiers.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
class DeviceCache {
private final Map<String, Device> entries = new HashMap<>();
public void replaceAllDevices(DeviceCollection deviceCollection) {
clear();
deviceCollection.getDeviceIdentifiers().stream().forEach(i -> entries.put(i, deviceCollection.getDevice(i)));
}
public void clear() {
entries.clear();
}
public Set<String> getDeviceIds() {
return entries.keySet();
}
public Optional<Device> getDevice(String deviceIdentifier) {
return Optional.ofNullable(entries.get(deviceIdentifier));
}
}

View File

@ -0,0 +1,109 @@
/**
* 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.mielecloud.internal.webservice;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles event dispatching to {@link DeviceStateListener}s.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class DeviceStateDispatcher {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<DeviceStateListener> listeners = new CopyOnWriteArrayList<>();
private Set<String> previousDeviceIdentifiers = new HashSet<>();
private final DeviceCache cache = new DeviceCache();
/**
* Adds a listener. The listener will be immediately invoked with the current status of all known devices.
*
* @param listener The listener to add.
*/
public void addListener(DeviceStateListener listener) {
if (listeners.contains(listener)) {
logger.warn("Listener '{}' was registered multiple times.", listener);
}
listeners.add(listener);
cache.getDeviceIds().forEach(deviceIdentifier -> cache.getDevice(deviceIdentifier)
.ifPresent(device -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
}
/**
* Removes a listener.
*/
public void removeListener(DeviceStateListener listener) {
listeners.remove(listener);
}
/**
* Clears the internal device state cache.
*/
public void clearCache() {
cache.clear();
}
/**
* Dispatches device status updates to all registered {@link DeviceStateListener}. This includes device removal.
*
* @param devices {@link DeviceCollection} which contains the state information to dispatch.
*/
public void dispatchDeviceStateUpdates(DeviceCollection devices) {
cache.replaceAllDevices(devices);
dispatchDevicesRemoved(devices);
cache.getDeviceIds().forEach(this::dispatchDeviceState);
}
/**
* Dispatches the cached state of the device identified by the given device identifier.
*/
public void dispatchDeviceState(String deviceIdentifier) {
cache.getDevice(deviceIdentifier).ifPresent(device -> listeners
.forEach(listener -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
}
/**
* Dispatches device action updates to all registered {@link DeviceStateListener}.
*
* @param deviceId ID of the device to dispatch the {@link Actions} for.
* @param actions {@link Actions} to dispatch.
*/
public void dispatchActionStateUpdates(String deviceId, Actions actions) {
listeners.forEach(listener -> listener.onProcessActionUpdated(new ActionsState(deviceId, actions)));
}
private void dispatchDevicesRemoved(DeviceCollection devices) {
Set<String> presentDeviceIdentifiers = devices.getDeviceIdentifiers();
Set<String> removedDeviceIdentifiers = previousDeviceIdentifiers;
removedDeviceIdentifiers.removeAll(presentDeviceIdentifiers);
previousDeviceIdentifiers = devices.getDeviceIdentifiers();
removedDeviceIdentifiers
.forEach(deviceIdentifier -> listeners.forEach(listener -> listener.onDeviceRemoved(deviceIdentifier)));
}
}

View File

@ -0,0 +1,46 @@
/**
* 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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
/**
* Listener for the device states.
*
* @author Björn Lange and Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public interface DeviceStateListener {
/**
* Invoked when new status information is available for a device.
*
* @param deviceState The device state information.
*/
void onDeviceStateUpdated(DeviceState deviceState);
/**
* Invoked when a new process action is available for a device.
*
* @param ActionsState The action state information.
*/
void onProcessActionUpdated(ActionsState actionState);
/**
* Invoked when a device got removed from the Miele cloud and no information is available about it.
*
* @param deviceIdentifier The identifier of the removed device.
*/
void onDeviceRemoved(String deviceIdentifier);
}

View File

@ -0,0 +1,94 @@
/**
* 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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Response;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ErrorMessage;
import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
/**
* Holds utility functions for working with HTTP.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class HttpUtil {
private static final String RETRY_AFTER_HEADER_FIELD_NAME = "Retry-After";
private HttpUtil() {
throw new IllegalStateException("This class must not be instantiated");
}
/**
* Checks whether the HTTP status given in {@code response} is a success state. In case an error state is obtained,
* exceptions are thrown.
*
* @param response The response to check.
* @throws MieleWebserviceTransientException if the status indicates a transient HTTP error.
* @throws MieleWebserviceException if the status indicates another HTTP error.
* @throws AuthorizationFailedException if the status indicates an authorization failure.
* @throws TooManyRequestsException if the status indicates that too many requests have been made against the remote
* endpoint.
*/
public static void checkHttpSuccess(Response response) {
if (isHttpSuccessStatus(response.getStatus())) {
return;
}
String exceptionMessage = getHttpErrorMessageFromCloudResponse(response);
switch (response.getStatus()) {
case 401:
throw new AuthorizationFailedException(exceptionMessage);
case 429:
String retryAfter = null;
if (response.getHeaders().containsKey(RETRY_AFTER_HEADER_FIELD_NAME)) {
retryAfter = response.getHeaders().get(RETRY_AFTER_HEADER_FIELD_NAME);
}
throw new TooManyRequestsException(exceptionMessage, retryAfter);
case 500:
throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVER_ERROR);
case 503:
throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVICE_UNAVAILABLE);
default:
throw new MieleWebserviceException(exceptionMessage, ConnectionError.OTHER_HTTP_ERROR);
}
}
/**
* Gets whether {@code httpStatus} is a HTTP error code from the 200 range (success).
*/
private static boolean isHttpSuccessStatus(int httpStatus) {
return httpStatus / 100 == 2;
}
private static String getHttpErrorMessageFromCloudResponse(Response response) {
String exceptionMessage = "HTTP error " + response.getStatus() + ": " + response.getReason();
if (response instanceof ContentResponse) {
try {
ErrorMessage errorMessage = ErrorMessage.fromJson(((ContentResponse) response).getContentAsString());
exceptionMessage += "\nCloud returned message: " + errorMessage.getMessage();
} catch (MieleSyntaxException e) {
exceptionMessage += "\nCloud returned invalid message.";
}
}
return exceptionMessage;
}
}

View File

@ -0,0 +1,142 @@
/**
* 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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
/**
* The {@link MieleWebservice} serves as an interface to the Miele REST API and wraps all calls to it.
*
* @author Björn Lange and Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public interface MieleWebservice extends AutoCloseable {
/**
* Sets the OAuth2 access token to use.
*/
void setAccessToken(String accessToken);
/**
* Returns whether an access token is available.
*/
boolean hasAccessToken();
/**
* Connects to the Miele webservice SSE endpoint and starts receiving events.
*/
void connectSse();
/**
* Disconnects a running connection from the Miele SSE endpoint.
*/
void disconnectSse();
/**
* Fetches the available actions for the device with the given {@code deviceId}.
*
* @param deviceId The unique ID of the device to fetch the available actions for.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
void fetchActions(String deviceId);
/**
* Performs a PUT operation with the given {@code processAction}.
*
* @param deviceId ID of the device to trigger the action for.
* @param processAction The action to perform.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
void putProcessAction(String deviceId, ProcessAction processAction);
/**
* Performs a PUT operation enabling or disabling the device's light.
*
* @param deviceId ID of the device to trigger the action for.
* @param enabled {@code true} to enable or {@code false} to disable the light.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
void putLight(String deviceId, boolean enabled);
/**
* Performs a PUT operation switching the device on or off.
*
* @param deviceId ID of the device to trigger the action for.
* @param enabled {@code true} to switch on or {@code false} to switch off the device.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
void putPowerState(String deviceId, boolean enabled);
/**
* Performs a PUT operation setting the active program.
*
* @param deviceId ID of the device to trigger the action for.
* @param program The program to activate.
* @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
* @throws AuthorizationFailedException if the authorization against the webservice failed.
* @throws TooManyRequestsException if too many requests have been made against the webservice recently.
*/
void putProgram(String deviceId, long programId);
/**
* Performs a logout and invalidates the current OAuth2 token. This operation is assumed to work on the first try
* and is never retried. HTTP errors are ignored.
*
* @throws MieleWebserviceException if the request operation fails.
*/
void logout();
/**
* Dispatches the cached state of the device identified by the given device identifier.
*/
void dispatchDeviceState(String deviceIdentifier);
/**
* Adds a {@link DeviceStateListener}.
*
* @param listener The listener to add.
*/
void addDeviceStateListener(DeviceStateListener listener);
/**
* Removes a {@link DeviceStateListener}.
*
* @param listener The listener to remove.
*/
void removeDeviceStateListener(DeviceStateListener listener);
/**
* Adds a {@link ConnectionStatusListener}.
*
* @param listener The listener to add.
*/
void addConnectionStatusListener(ConnectionStatusListener listener);
/**
* Removes a {@link ConnectionStatusListener}.
*
* @param listener The listener to remove.
*/
void removeConnectionStatusListener(ConnectionStatusListener listener);
}

View File

@ -0,0 +1,134 @@
/**
* 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.mielecloud.internal.webservice;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
/**
* Represents a webservice configuration.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public final class MieleWebserviceConfiguration {
private final HttpClientFactory httpClientFactory;
private final LanguageProvider languageProvider;
private final OAuthTokenRefresher tokenRefresher;
private final String serviceHandle;
private final ScheduledExecutorService scheduler;
private MieleWebserviceConfiguration(MieleWebserviceConfigurationBuilder builder) {
this.httpClientFactory = getOrThrow(builder.httpClientFactory, "httpClientFactory");
this.languageProvider = getOrThrow(builder.languageProvider, "languageProvider");
this.tokenRefresher = getOrThrow(builder.tokenRefresher, "tokenRefresher");
this.serviceHandle = getOrThrow(builder.serviceHandle, "serviceHandle");
this.scheduler = getOrThrow(builder.scheduler, "scheduler");
}
private static <T> T getOrThrow(@Nullable T object, String objectName) {
if (object == null) {
throw new IllegalArgumentException(objectName + " must not be null");
}
return object;
}
/**
* Gets the factory to use for HttpClient construction.
*/
public HttpClientFactory getHttpClientFactory() {
return httpClientFactory;
}
/**
* Gets the provider for the language to use when making requests to the API.
*/
public LanguageProvider getLanguageProvider() {
return languageProvider;
}
/**
* Gets the refresher for OAuth tokens.
*/
public OAuthTokenRefresher getTokenRefresher() {
return tokenRefresher;
}
/**
* Gets the handle referring to the OAuth tokens in the framework's persistent storage.
*/
public String getServiceHandle() {
return serviceHandle;
}
/**
* Gets the system wide scheduler.
*/
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public static MieleWebserviceConfigurationBuilder builder() {
return new MieleWebserviceConfigurationBuilder();
}
public static final class MieleWebserviceConfigurationBuilder {
@Nullable
private HttpClientFactory httpClientFactory;
@Nullable
private LanguageProvider languageProvider;
@Nullable
private OAuthTokenRefresher tokenRefresher;
@Nullable
private String serviceHandle;
@Nullable
private ScheduledExecutorService scheduler;
private MieleWebserviceConfigurationBuilder() {
}
public MieleWebserviceConfigurationBuilder withHttpClientFactory(HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
return this;
}
public MieleWebserviceConfigurationBuilder withLanguageProvider(LanguageProvider languageProvider) {
this.languageProvider = languageProvider;
return this;
}
public MieleWebserviceConfigurationBuilder withTokenRefresher(OAuthTokenRefresher tokenRefresher) {
this.tokenRefresher = tokenRefresher;
return this;
}
public MieleWebserviceConfigurationBuilder withServiceHandle(String serviceHandle) {
this.serviceHandle = serviceHandle;
return this;
}
public MieleWebserviceConfigurationBuilder withScheduler(ScheduledExecutorService scheduler) {
this.scheduler = scheduler;
return this;
}
public MieleWebserviceConfiguration build() {
return new MieleWebserviceConfiguration(this);
}
}
}

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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Factory for creating {@link MieleWebservice} instances.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public interface MieleWebserviceFactory {
/**
* Creates a new {@link MieleWebservice}.
*
* @param configuration The configuration holding all required parameters to construct the instance.
* @return A new {@link MieleWebservice}.
*/
public MieleWebservice create(MieleWebserviceConfiguration configuration);
}

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.mielecloud.internal.webservice;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link MieleWebservice} that serves as a replacement when no webservice is available.
*
* @author Björn Lange - Initial Contribution
*/
@NonNullByDefault
public final class UnavailableMieleWebservice implements MieleWebservice {
public static final UnavailableMieleWebservice INSTANCE = new UnavailableMieleWebservice();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private UnavailableMieleWebservice() {
}
@Override
public void setAccessToken(String accessToken) {
logger.warn("Cannot set access token: The Miele cloud service is not available.");
}
@Override
public boolean hasAccessToken() {
logger.warn("There is no access token: The Miele cloud service is not available.");
return false;
}
@Override
public void connectSse() {
logger.warn("Cannot connect to SSE stream: The Miele cloud service is not available.");
}
@Override
public void disconnectSse() {
logger.warn("Cannot disconnect from SSE stream: The Miele cloud service is not available.");
}
@Override
public void fetchActions(String deviceId) {
logger.warn("Cannot fetch actions for device '{}': The Miele cloud service is not available.", deviceId);
}
@Override
public void putProcessAction(String deviceId, ProcessAction processAction) {
logger.warn("Cannot perform '{}' operation for device '{}': The Miele cloud service is not available.",
processAction, deviceId);
}
@Override
public void putLight(String deviceId, boolean enabled) {
logger.warn("Cannot set light state to '{}' for device '{}': The Miele cloud service is not available.",
enabled ? "ON" : "OFF", deviceId);
}
@Override
public void putPowerState(String deviceId, boolean enabled) {
logger.warn("Cannot set power state to '{}' for device '{}': The Miele cloud service is not available.",
enabled ? "ON" : "OFF", deviceId);
}
@Override
public void putProgram(String deviceId, long programId) {
logger.warn("Cannot activate program with ID '{}' for device '{}': The Miele cloud service is not available.",
programId, deviceId);
}
@Override
public void logout() {
logger.warn("Cannot logout: The Miele cloud service is not available.");
}
@Override
public void dispatchDeviceState(String deviceIdentifier) {
logger.warn("Cannot re-emit device state for device '{}': The Miele cloud service is not available.",
deviceIdentifier);
}
@Override
public void addDeviceStateListener(DeviceStateListener listener) {
logger.warn("Cannot add listener for all devices: The Miele cloud service is not available.");
}
@Override
public void removeDeviceStateListener(DeviceStateListener listener) {
logger.warn("Cannot remove listener: The Miele cloud service is not available.");
}
@Override
public void addConnectionStatusListener(ConnectionStatusListener listener) {
logger.warn("Cannot add connection error listener: The Miele cloud service is not available.");
}
@Override
public void removeConnectionStatusListener(ConnectionStatusListener listener) {
logger.warn("Cannot remove listener: The Miele cloud service is not available.");
}
@Override
public void close() throws Exception {
}
}

View File

@ -0,0 +1,176 @@
/**
* 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.mielecloud.internal.webservice.api;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
/**
* Provides convenient access to the list of actions that can be performed with a device.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public class ActionsState {
private final String deviceIdentifier;
private final Optional<Actions> actions;
public ActionsState(String deviceIdentifier, @Nullable Actions actions) {
this.deviceIdentifier = deviceIdentifier;
this.actions = Optional.ofNullable(actions);
}
/**
* Gets the unique identifier of the device to which this state refers.
*/
public String getDeviceIdentifier() {
return deviceIdentifier;
}
/**
* Gets whether the device can be started.
*/
public boolean canBeStarted() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START)).orElse(false);
}
/**
* Gets whether the device can be stopped.
*/
public boolean canBeStopped() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP)).orElse(false);
}
/**
* Gets whether the device can be paused.
*/
public boolean canBePaused() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.PAUSE)).orElse(false);
}
/**
* Gets whether supercooling can be controlled.
*/
public boolean canContolSupercooling() {
return canStartSupercooling() || canStopSupercooling();
}
/**
* Gets whether supercooling can be started.
*/
public boolean canStartSupercooling() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERCOOLING))
.orElse(false);
}
/**
* Gets whether supercooling can be stopped.
*/
public boolean canStopSupercooling() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERCOOLING))
.orElse(false);
}
/**
* Gets whether superfreezing can be controlled.
*/
public boolean canControlSuperfreezing() {
return canStartSuperfreezing() || canStopSuperfreezing();
}
/**
* Gets whether superfreezing can be started.
*/
public boolean canStartSuperfreezing() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERFREEZING))
.orElse(false);
}
/**
* Gets whether superfreezing can be stopped.
*/
public boolean canStopSuperfreezing() {
return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERFREEZING))
.orElse(false);
}
/**
* Gets whether light can be enabled.
*/
public boolean canEnableLight() {
return actions.map(Actions::getLight).map(a -> a.contains(Light.ENABLE)).orElse(false);
}
/**
* Gets whether light can be disabled.
*/
public boolean canDisableLight() {
return actions.map(Actions::getLight).map(a -> a.contains(Light.DISABLE)).orElse(false);
}
/**
* Gets whether the device can be switched on.
*/
public boolean canBeSwitchedOn() {
return actions.flatMap(Actions::getPowerOn).map(Boolean.TRUE::equals).orElse(false);
}
/**
* Gets whether the device can be switched off.
*/
public boolean canBeSwitchedOff() {
return actions.flatMap(Actions::getPowerOff).map(Boolean.TRUE::equals).orElse(false);
}
/**
* Gets whether the light can be controlled.
*/
public boolean canControlLight() {
return canEnableLight() || canDisableLight();
}
/**
* Gets whether the active program can be set.
*/
public boolean canSetActiveProgramId() {
return !actions.map(Actions::getProgramId).map(List::isEmpty).orElse(true);
}
@Override
public int hashCode() {
return Objects.hash(actions, deviceIdentifier);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ActionsState other = (ActionsState) obj;
return Objects.equals(actions, other.actions) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
}
}

View File

@ -0,0 +1,103 @@
/**
* 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.mielecloud.internal.webservice.api;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Provides easy access to temperature values mapped for cooling devices.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class CoolingDeviceTemperatureState {
private final DeviceState deviceState;
public CoolingDeviceTemperatureState(DeviceState deviceState) {
this.deviceState = deviceState;
}
/**
* Gets the current temperature of the fridge part of the device.
*
* @return The current temperature of the fridge part of the device.
*/
public Optional<Integer> getFridgeTemperature() {
switch (deviceState.getRawType()) {
case FRIDGE:
return deviceState.getTemperature(0);
case FRIDGE_FREEZER_COMBINATION:
return deviceState.getTemperature(0);
default:
return Optional.empty();
}
}
/**
* Gets the target temperature of the fridge part of the device.
*
* @return The target temperature of the fridge part of the device.
*/
public Optional<Integer> getFridgeTargetTemperature() {
switch (deviceState.getRawType()) {
case FRIDGE:
return deviceState.getTargetTemperature(0);
case FRIDGE_FREEZER_COMBINATION:
return deviceState.getTargetTemperature(0);
default:
return Optional.empty();
}
}
/**
* Gets the current temperature of the freezer part of the device.
*
* @return The current temperature of the freezer part of the device.
*/
public Optional<Integer> getFreezerTemperature() {
switch (deviceState.getRawType()) {
case FREEZER:
return deviceState.getTemperature(0);
case FRIDGE_FREEZER_COMBINATION:
return deviceState.getTemperature(1);
default:
return Optional.empty();
}
}
/**
* Gets the target temperature of the freezer part of the device.
*
* @return The target temperature of the freezer part of the device.
*/
public Optional<Integer> getFreezerTargetTemperature() {
switch (deviceState.getRawType()) {
case FREEZER:
return deviceState.getTargetTemperature(0);
case FRIDGE_FREEZER_COMBINATION:
return deviceState.getTargetTemperature(1);
default:
return Optional.empty();
}
}
}

View File

@ -0,0 +1,558 @@
/**
* 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.mielecloud.internal.webservice.api;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
/**
* This immutable class provides methods to extract the device state information in a comfortable way.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Introduced null handling
* @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm, info state channel and map signal
* flags from API
* @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner things
*/
@NonNullByDefault
public class DeviceState {
private final String deviceIdentifier;
private final Optional<Device> device;
public DeviceState(String deviceIdentifier, @Nullable Device device) {
this.deviceIdentifier = deviceIdentifier;
this.device = Optional.ofNullable(device);
}
/**
* Gets the unique identifier for this device.
*
* @return The unique identifier for this device.
*/
public String getDeviceIdentifier() {
return deviceIdentifier;
}
/**
* Gets the main operation status of the device.
*
* @return The main operation status of the device.
*/
public Optional<String> getStatus() {
return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueLocalized);
}
/**
* Gets the raw main operation status of the device.
*
* @return The raw main operation status of the device.
*/
public Optional<Integer> getStatusRaw() {
return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw);
}
/**
* Gets the raw operation status of the device parsed to a {@link StateType}.
*
* @return The raw operation status of the device parsed to a {@link StateType}.
*/
public Optional<StateType> getStateType() {
return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw)
.flatMap(StateType::fromCode);
}
/**
* Gets the currently selected program type of the device.
*
* @return The currently selected program type of the device.
*/
public Optional<String> getSelectedProgram() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueLocalized);
}
/**
* Gets the selected program ID.
*
* @return The selected program ID.
*/
public Optional<Long> getSelectedProgramId() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueRaw);
}
/**
* Gets the currently active phase of the active program.
*
* @return The currently active phase of the active program.
*/
public Optional<String> getProgramPhase() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getProgramPhase)
.flatMap(ProgramPhase::getValueLocalized);
}
/**
* Gets the currently active raw phase of the active program.
*
* @return The currently active raw phase of the active program.
*/
public Optional<Integer> getProgramPhaseRaw() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getProgramPhase).flatMap(ProgramPhase::getValueRaw);
}
/**
* Gets the currently selected drying step.
*
* @return The currently selected drying step.
*/
public Optional<String> getDryingTarget() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueLocalized);
}
/**
* Gets the currently selected raw drying step.
*
* @return The currently selected raw drying step.
*/
public Optional<Integer> getDryingTargetRaw() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueRaw);
}
/**
* Calculates if pre-heating the oven has finished.
*
* @return Whether pre-heating the oven has finished.
*/
public Optional<Boolean> hasPreHeatFinished() {
if (deviceIsInOffState()) {
return Optional.empty();
}
Optional<Integer> targetTemperature = getTargetTemperature(0);
Optional<Integer> currentTemperature = getTemperature(0);
if (!targetTemperature.isPresent() || !currentTemperature.isPresent()) {
return Optional.empty();
}
return Optional.of(isInState(StateType.RUNNING) && currentTemperature.get() >= targetTemperature.get());
}
/**
* Gets the target temperature with the given index.
*
* @return The target temperature with the given index.
*/
public Optional<Integer> getTargetTemperature(int index) {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).map(State::getTargetTemperature).flatMap(l -> getOrNull(l, index))
.flatMap(Temperature::getValueLocalized);
}
/**
* Gets the current temperature of the device for the given index.
*
* @param index The index of the device zone for which the temperature shall be obtained.
* @return The target temperature if available.
*/
public Optional<Integer> getTemperature(int index) {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).map(State::getTemperature).flatMap(l -> getOrNull(l, index))
.flatMap(Temperature::getValueLocalized);
}
/**
* Gets the remaining time of the active program.
*
* @return The remaining time in seconds.
*/
public Optional<Integer> getRemainingTime() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getRemainingTime).flatMap(this::toSeconds);
}
/**
* Gets the elapsed time of the active program.
*
* @return The elapsed time in seconds.
*/
public Optional<Integer> getElapsedTime() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getElapsedTime).flatMap(this::toSeconds);
}
/**
* Gets the relative start time of the active program.
*
* @return The delayed start time in seconds.
*/
public Optional<Integer> getStartTime() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getStartTime).flatMap(this::toSeconds);
}
/**
* Gets the "fullRemoteControl" state information of the device. If this flag is true ALL remote control actions
* of the device can be triggered.
*
* @return Whether the device can be remote controlled.
*/
public Optional<Boolean> isRemoteControlEnabled() {
return device.flatMap(Device::getState).flatMap(State::getRemoteEnable)
.flatMap(RemoteEnable::getFullRemoteControl);
}
/**
* Calculates the program process.
*
* @return The progress of the active program in percent.
*/
public Optional<Integer> getProgress() {
if (deviceIsInOffState()) {
return Optional.empty();
}
Optional<Double> elapsedTime = device.flatMap(Device::getState).flatMap(State::getElapsedTime)
.flatMap(this::toSeconds).map(Integer::doubleValue);
Optional<Double> remainingTime = device.flatMap(Device::getState).flatMap(State::getRemainingTime)
.flatMap(this::toSeconds).map(Integer::doubleValue);
if (elapsedTime.isPresent() && remainingTime.isPresent()
&& (elapsedTime.get() != 0 || remainingTime.get() != 0)) {
return Optional.of((int) ((elapsedTime.get() / (elapsedTime.get() + remainingTime.get())) * 100.0));
} else {
return Optional.empty();
}
}
private Optional<Integer> toSeconds(List<Integer> time) {
if (time.size() != 2) {
return Optional.empty();
}
return Optional.of((time.get(0) * 60 + time.get(1)) * 60);
}
/**
* Gets the spinning speed.
*
* @return The spinning speed.
*/
public Optional<String> getSpinningSpeed() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw)
.map(String::valueOf);
}
/**
* Gets the raw spinning speed.
*
* @return The raw spinning speed.
*/
public Optional<Integer> getSpinningSpeedRaw() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw);
}
/**
* Gets the ventilation step.
*
* @return The ventilation step.
*/
public Optional<String> getVentilationStep() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
.flatMap(VentilationStep::getValueLocalized).map(Object::toString);
}
/**
* Gets the raw ventilation step.
*
* @return The raw ventilation step.
*/
public Optional<Integer> getVentilationStepRaw() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
.flatMap(VentilationStep::getValueRaw);
}
/**
* Gets the plate power step of the device for the given index.
*
* @param index The index of the device plate for which the power step shall be obtained.
* @return The plate power step if available.
*/
public Optional<String> getPlateStep(int index) {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
.flatMap(PlateStep::getValueLocalized);
}
/**
* Gets the raw plate power step of the device for the given index.
*
* @param index The index of the device plate for which the power step shall be obtained.
* @return The raw plate power step if available.
*/
public Optional<Integer> getPlateStepRaw(int index) {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
.flatMap(PlateStep::getValueRaw);
}
/**
* Gets the number of available plate steps.
*
* @return The number of available plate steps.
*/
public Optional<Integer> getPlateStepCount() {
return device.flatMap(Device::getState).map(State::getPlateStep).map(List::size);
}
/**
* Indicates if the device has an error that requires a user action.
*
* @return Whether the device has an error that requires a user action.
*/
public boolean hasError() {
return isInState(StateType.FAILURE)
|| device.flatMap(Device::getState).flatMap(State::getSignalFailure).orElse(false);
}
/**
* Indicates if the device has a user information.
*
* @return Whether the device has a user information.
*/
public boolean hasInfo() {
if (deviceIsInOffState()) {
return false;
}
return device.flatMap(Device::getState).flatMap(State::getSignalInfo).orElse(false);
}
/**
* Gets the state of the light attached to the device.
*
* @return An {@link Optional} with value {@code true} if the light is turned on, {@code false} if the light is
* turned off or an empty {@link Optional} if light is not supported or no state is available.
*/
public Optional<Boolean> getLightState() {
if (deviceIsInOffState()) {
return Optional.empty();
}
Optional<Light> light = device.flatMap(Device::getState).map(State::getLight);
if (light.isPresent()) {
if (light.get().equals(Light.ENABLE)) {
return Optional.of(true);
} else if (light.get().equals(Light.DISABLE)) {
return Optional.of(false);
}
}
return Optional.empty();
}
/**
* Gets the state of the door attached to the device.
*
* @return Whether the device door is open.
*/
public Optional<Boolean> getDoorState() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getSignalDoor);
}
/**
* Gets the state of the device's door alarm.
*
* @return Whether the device door alarm was triggered.
*/
public Optional<Boolean> getDoorAlarm() {
if (deviceIsInOffState()) {
return Optional.empty();
}
Optional<Boolean> doorState = getDoorState();
Optional<Boolean> failure = device.flatMap(Device::getState).flatMap(State::getSignalFailure);
if (!doorState.isPresent() || !failure.isPresent()) {
return Optional.empty();
}
return Optional.of(doorState.get() && failure.get());
}
/**
* Gets the battery level.
*
* @return The battery level.
*/
public Optional<Integer> getBatteryLevel() {
if (deviceIsInOffState()) {
return Optional.empty();
}
return device.flatMap(Device::getState).flatMap(State::getBatteryLevel);
}
/**
* Gets the device type.
*
* @return The device type as human readable value.
*/
public Optional<String> getType() {
return device.flatMap(Device::getIdent).flatMap(Ident::getType).flatMap(Type::getValueLocalized)
.filter(type -> !type.isEmpty());
}
/**
* Gets the raw device type.
*
* @return The raw device type.
*/
public DeviceType getRawType() {
return device.flatMap(Device::getIdent).flatMap(Ident::getType).map(Type::getValueRaw)
.orElse(DeviceType.UNKNOWN);
}
/**
* Gets the user-defined name of the device.
*
* @return The user-defined name of the device.
*/
public Optional<String> getDeviceName() {
return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceName).filter(name -> !name.isEmpty());
}
/**
* Gets the fabrication (=serial) number of the device.
*
* @return The serial number of the device.
*/
public Optional<String> getFabNumber() {
return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
.flatMap(DeviceIdentLabel::getFabNumber).filter(fabNumber -> !fabNumber.isEmpty());
}
/**
* Gets the tech type of the device.
*
* @return The tech type of the device.
*/
public Optional<String> getTechType() {
return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
.flatMap(DeviceIdentLabel::getTechType).filter(techType -> !techType.isEmpty());
}
private <T> Optional<T> getOrNull(List<T> list, int index) {
if (index < 0 || index >= list.size()) {
return Optional.empty();
}
return Optional.ofNullable(list.get(index));
}
private boolean deviceIsInOffState() {
return getStateType().map(StateType.OFF::equals).orElse(true);
}
public boolean isInState(StateType stateType) {
return getStateType().map(stateType::equals).orElse(false);
}
@Override
public int hashCode() {
return Objects.hash(device, deviceIdentifier);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DeviceState other = (DeviceState) obj;
return Objects.equals(device, other.device) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
}
}

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.mielecloud.internal.webservice.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the power status of the device, i.e. whether it is powered on, off or in standby.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public enum PowerStatus {
POWER_ON("on"),
POWER_OFF("off"),
STANDBY("standby");
/**
* Corresponding state of the ChannelTypeDefinition
*/
private String state;
PowerStatus(String value) {
this.state = value;
}
/**
* Checks whether the given value is the raw state represented by this enum instance.
*/
public boolean matches(String passedValue) {
return state.equalsIgnoreCase(passedValue);
}
/**
* Gets the raw state.
*/
public String getState() {
return state;
}
}

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.mielecloud.internal.webservice.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the status of a program.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public enum ProgramStatus {
PROGRAM_STARTED("start"),
PROGRAM_STOPPED("stop"),
PROGRAM_PAUSED("pause");
/**
* Corresponding state of the ChannelTypeDefinition
*/
private String state;
ProgramStatus(String value) {
this.state = value;
}
/**
* Checks whether the given value is the raw state represented by this enum instance.
*/
public boolean matches(String passedValue) {
return state.equalsIgnoreCase(passedValue);
}
/**
* Gets the raw state.
*/
public String getState() {
return state;
}
}

View File

@ -0,0 +1,147 @@
/**
* 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.mielecloud.internal.webservice.api;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
/**
* This immutable class provides methods to extract the state information related to state transitions in a comfortable
* way.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class TransitionState {
private final boolean remainingTimeWasSetInCurrentProgram;
private final Optional<DeviceState> previousState;
private final DeviceState nextState;
/**
* Creates a new {@link TransitionState}.
*
* Note: {@code previousState} <b>must not</b> be saved in a field in this class as this will create a linked list
* and cause memory issues. The constructor only serves the purpose of unpacking state that must be carried on.
*
* @param previousTransitionState The previous transition state if it exists.
* @param nextState The device state which the device is transitioning to.
*/
public TransitionState(@Nullable TransitionState previousTransitionState, DeviceState nextState) {
this.remainingTimeWasSetInCurrentProgram = wasRemainingTimeSetInCurrentProgram(previousTransitionState,
nextState);
this.previousState = Optional.ofNullable(previousTransitionState).map(it -> it.nextState);
this.nextState = nextState;
}
/**
* Gets whether the finish state changed due to the transition form the previous to the current state.
*
* @return Whether the finish state changed due to the transition form the previous to the current state.
*/
public boolean hasFinishedChanged() {
return previousState.map(this::hasFinishedChangedFromPreviousState).orElse(true);
}
private boolean hasFinishedChangedFromPreviousState(DeviceState previous) {
if (previous.getStateType().equals(nextState.getStateType())) {
return false;
}
if (isInRunningState(previous) && nextState.isInState(StateType.FAILURE)) {
return false;
}
if (isInRunningState(previous) != isInRunningState(nextState)) {
return true;
}
if (nextState.isInState(StateType.OFF)) {
return true;
}
return false;
}
/**
* Gets whether a program finished.
*
* @return Whether a program finished.
*/
public Optional<Boolean> isFinished() {
return previousState.flatMap(this::hasFinishedFromPreviousState);
}
private Optional<Boolean> hasFinishedFromPreviousState(DeviceState prevState) {
if (!prevState.getStateType().isPresent()) {
return Optional.empty();
}
if (nextState.isInState(StateType.OFF)) {
return Optional.of(false);
}
if (nextState.isInState(StateType.FAILURE)) {
return Optional.of(false);
}
return Optional.of(!isInRunningState(nextState));
}
/**
* Gets the remaining time of the active program.
*
* Note: Tracking changes in the remaining time is a workaround for the Miele API not properly distinguishing
* between "there is no remaining time set" and "the remaining time is zero". If the remaining time is zero when a
* program is started then we assume that no timer was set / program with remaining time is active. This may be
* changed later by the user which is detected by the remaining time changing from 0 to some larger value.
*
* @return The remaining time in seconds.
*/
public Optional<Integer> getRemainingTime() {
if (!remainingTimeWasSetInCurrentProgram && isInRunningState(nextState)) {
return nextState.getRemainingTime().filter(it -> it != 0);
} else {
return nextState.getRemainingTime();
}
}
/**
* Gets the program progress.
*
* @return The progress of the active program in percent.
*/
public Optional<Integer> getProgress() {
if (getRemainingTime().isPresent()) {
return nextState.getProgress();
} else {
return Optional.empty();
}
}
private static boolean wasRemainingTimeSetInCurrentProgram(@Nullable TransitionState previousTransitionState,
DeviceState nextState) {
if (previousTransitionState != null && isInRunningState(previousTransitionState.nextState)) {
return previousTransitionState.remainingTimeWasSetInCurrentProgram
|| previousTransitionState.getRemainingTime().isPresent();
} else {
return false;
}
}
private static boolean isInRunningState(DeviceState device) {
return device.isInState(StateType.RUNNING) || device.isInState(StateType.PAUSE);
}
}

View File

@ -0,0 +1,206 @@
/**
* 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.mielecloud.internal.webservice.api;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
/**
* Provides easy access to temperature values mapped for wine storage devices.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class WineStorageDeviceTemperatureState {
private static final Set<DeviceType> ALL_WINE_STORAGES = Set.of(DeviceType.WINE_CABINET,
DeviceType.WINE_CABINET_FREEZER_COMBINATION, DeviceType.WINE_CONDITIONING_UNIT,
DeviceType.WINE_STORAGE_CONDITIONING_UNIT);
private final DeviceState deviceState;
private final List<Integer> effectiveTemperatures;
private final List<Integer> effectiveTargetTemperatures;
/**
* Creates a new {@link WineStorageDeviceTemperatureState}.
*
* @param deviceState Device state to query extended state information from.
*/
public WineStorageDeviceTemperatureState(DeviceState deviceState) {
this.deviceState = deviceState;
effectiveTemperatures = getEffectiveTemperatures();
effectiveTargetTemperatures = getEffectiveTargetTemperatures();
}
private List<Integer> getEffectiveTemperatures() {
return Arrays
.asList(deviceState.getTemperature(0), deviceState.getTemperature(1), deviceState.getTemperature(2))
.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
}
private List<Integer> getEffectiveTargetTemperatures() {
return Arrays
.asList(deviceState.getTargetTemperature(0), deviceState.getTargetTemperature(1),
deviceState.getTargetTemperature(2))
.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
}
/**
* Gets the current main temperature of the wine storage.
*
* @return The current main temperature of the wine storage.
*/
public Optional<Integer> getTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getTemperatureFromList(effectiveTemperatures);
}
/**
* Gets the target main temperature of the wine storage.
*
* @return The target main temperature of the wine storage.
*/
public Optional<Integer> getTargetTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getTemperatureFromList(effectiveTargetTemperatures);
}
private Optional<Integer> getTemperatureFromList(List<Integer> temperatures) {
if (temperatures.isEmpty()) {
return Optional.empty();
}
if (temperatures.size() > 1) {
return Optional.empty();
}
return Optional.of(temperatures.get(0));
}
/**
* Gets the current top temperature of the wine storage.
*
* @return The current top temperature of the wine storage.
*/
public Optional<Integer> getTopTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getTopTemperatureFromList(effectiveTemperatures);
}
/**
* Gets the target top temperature of the wine storage.
*
* @return The target top temperature of the wine storage.
*/
public Optional<Integer> getTopTargetTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getTopTemperatureFromList(effectiveTargetTemperatures);
}
private Optional<Integer> getTopTemperatureFromList(List<Integer> temperatures) {
if (temperatures.size() <= 1) {
return Optional.empty();
}
return Optional.of(temperatures.get(0));
}
/**
* Gets the current middle temperature of the wine storage.
*
* @return The current middle temperature of the wine storage.
*/
public Optional<Integer> getMiddleTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getMiddleTemperatureFromList(effectiveTemperatures);
}
/**
* Gets the target middle temperature of the wine storage.
*
* @return The target middle temperature of the wine storage.
*/
public Optional<Integer> getMiddleTargetTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getMiddleTemperatureFromList(effectiveTargetTemperatures);
}
private Optional<Integer> getMiddleTemperatureFromList(List<Integer> temperatures) {
if (temperatures.size() != 3) {
return Optional.empty();
}
return Optional.of(temperatures.get(1));
}
/**
* Gets the current bottom temperature of the wine storage.
*
* @return The current bottom temperature of the wine storage.
*/
public Optional<Integer> getBottomTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getBottomTemperatureFromList(effectiveTemperatures);
}
/**
* Gets the target bottom temperature of the wine storage.
*
* @return The target bottom temperature of the wine storage.
*/
public Optional<Integer> getBottomTargetTemperature() {
if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
return Optional.empty();
}
return getBottomTemperatureFromList(effectiveTargetTemperatures);
}
private Optional<Integer> getBottomTemperatureFromList(List<Integer> temperatures) {
if (temperatures.size() == 3) {
return Optional.of(temperatures.get(2));
}
if (temperatures.size() == 2) {
return Optional.of(temperatures.get(1));
}
return Optional.empty();
}
}

View File

@ -0,0 +1,137 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the device actions queried from the Miele REST API.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public class Actions {
@SerializedName("processAction")
@Nullable
private final List<ProcessAction> processAction = null;
@SerializedName("light")
@Nullable
private final List<Integer> light = null;
@SerializedName("startTime")
@Nullable
private final List<List<Integer>> startTime = null;
@SerializedName("programId")
@Nullable
private final List<Integer> programId = null;
@SerializedName("deviceName")
@Nullable
private String deviceName;
@SerializedName("powerOff")
@Nullable
private Boolean powerOff;
@SerializedName("powerOn")
@Nullable
private Boolean powerOn;
public List<ProcessAction> getProcessAction() {
if (processAction == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(processAction);
}
public List<Light> getLight() {
final List<Integer> lightRefCopy = light;
if (lightRefCopy == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(lightRefCopy.stream().map(Light::fromId).collect(Collectors.toList()));
}
/**
* Gets the start time encoded as {@link List} of {@link List} of {@link Integer} values.
* The first list entry defines the lower time constraint for setting the delayed start time. The second list
* entry defines the upper time constraint. The time constraints are defined as a list of integers with the full
* hour as first and minutes as second element.
*
* @return The possible start time interval encoded as described above.
*/
public Optional<List<List<Integer>>> getStartTime() {
if (startTime == null) {
return Optional.empty();
}
return Optional.of(Collections.unmodifiableList(startTime));
}
public List<Integer> getProgramId() {
if (programId == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(programId);
}
public Optional<String> getDeviceName() {
return Optional.ofNullable(deviceName);
}
public Optional<Boolean> getPowerOn() {
return Optional.ofNullable(powerOn);
}
public Optional<Boolean> getPowerOff() {
return Optional.ofNullable(powerOff);
}
@Override
public String toString() {
return "ActionState [processAction=" + processAction + ", light=" + light + ", startTime=" + startTime
+ ", programId=" + programId + ", deviceName=" + deviceName + ", powerOff=" + powerOff + ", powerOn="
+ powerOn + "]";
}
@Override
public int hashCode() {
return Objects.hash(deviceName, light, powerOn, powerOff, processAction, startTime, programId);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Actions other = (Actions) obj;
return Objects.equals(deviceName, other.deviceName) && Objects.equals(light, other.light)
&& Objects.equals(powerOn, other.powerOn) && Objects.equals(powerOff, other.powerOff)
&& Objects.equals(processAction, other.processAction) && Objects.equals(startTime, other.startTime)
&& Objects.equals(programId, other.programId);
}
}

View File

@ -0,0 +1,65 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Immutable POJO representing a device queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class Device {
@Nullable
private Ident ident;
@Nullable
private State state;
public Optional<Ident> getIdent() {
return Optional.ofNullable(ident);
}
public Optional<State> getState() {
return Optional.ofNullable(state);
}
@Override
public int hashCode() {
return Objects.hash(ident, state);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Device other = (Device) obj;
return Objects.equals(ident, other.ident) && Objects.equals(state, other.state);
}
@Override
public String toString() {
return "Device [ident=" + ident + ", state=" + state + "]";
}
}

View File

@ -0,0 +1,97 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* Immutable POJO representing a collection of devices queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class DeviceCollection {
private static final java.lang.reflect.Type STRING_DEVICE_MAP_TYPE = new TypeToken<Map<String, Device>>() {
}.getType();
private final Map<String, Device> devices;
DeviceCollection(Map<String, Device> devices) {
this.devices = devices;
}
/**
* Creates a new {@link DeviceCollection} from the given Json text.
*
* @param json The Json text.
* @return The created {@link DeviceCollection}.
* @throws MieleSyntaxException if parsing the data from {@code json} fails.
*/
public static DeviceCollection fromJson(String json) {
try {
Map<String, Device> devices = new Gson().fromJson(json, STRING_DEVICE_MAP_TYPE);
if (devices == null) {
throw new MieleSyntaxException("Failed to parse Json.");
}
return new DeviceCollection(devices);
} catch (JsonSyntaxException e) {
throw new MieleSyntaxException("Failed to parse Json.", e);
}
}
public Set<String> getDeviceIdentifiers() {
return devices.keySet();
}
public Device getDevice(String identifier) {
Device device = devices.get(identifier);
if (device == null) {
throw new IllegalArgumentException("There is no device for identifier " + identifier);
}
return device;
}
@Override
public int hashCode() {
return Objects.hash(devices);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DeviceCollection other = (DeviceCollection) obj;
return Objects.equals(devices, other.devices);
}
@Override
public String toString() {
return "DeviceCollection [devices=" + devices + "]";
}
}

View File

@ -0,0 +1,92 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Immutable POJO representing the full device identification queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class DeviceIdentLabel {
@Nullable
private String fabNumber;
@Nullable
private String fabIndex;
@Nullable
private String techType;
@Nullable
private String matNumber;
@Nullable
private final List<String> swids = null;
public Optional<String> getFabNumber() {
return Optional.ofNullable(fabNumber);
}
public Optional<String> getFabIndex() {
return Optional.ofNullable(fabIndex);
}
public Optional<String> getTechType() {
return Optional.ofNullable(techType);
}
public Optional<String> getMatNumber() {
return Optional.ofNullable(matNumber);
}
public List<String> getSwids() {
if (swids == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(swids);
}
@Override
public int hashCode() {
return Objects.hash(fabIndex, fabNumber, matNumber, swids, techType);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DeviceIdentLabel other = (DeviceIdentLabel) obj;
return Objects.equals(fabIndex, other.fabIndex) && Objects.equals(fabNumber, other.fabNumber)
&& Objects.equals(matNumber, other.matNumber) && Objects.equals(swids, other.swids)
&& Objects.equals(techType, other.techType);
}
@Override
public String toString() {
return "DeviceIdentLabel [fabNumber=" + fabNumber + ", fabIndex=" + fabIndex + ", techType=" + techType
+ ", matNumber=" + matNumber + ", swids=" + swids + "]";
}
}

View File

@ -0,0 +1,129 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents the Miele device type.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public enum DeviceType {
/**
* {@link DeviceType} for unknown devices.
*/
UNKNOWN,
@SerializedName("1")
WASHING_MACHINE,
@SerializedName("2")
TUMBLE_DRYER,
@SerializedName("7")
DISHWASHER,
@SerializedName("8")
DISHWASHER_SEMI_PROF,
@SerializedName("12")
OVEN,
@SerializedName("13")
OVEN_MICROWAVE,
@SerializedName("14")
HOB_HIGHLIGHT,
@SerializedName("15")
STEAM_OVEN,
@SerializedName("16")
MICROWAVE,
@SerializedName("17")
COFFEE_SYSTEM,
@SerializedName("18")
HOOD,
@SerializedName("19")
FRIDGE,
@SerializedName("20")
FREEZER,
@SerializedName("21")
FRIDGE_FREEZER_COMBINATION,
/**
* Might also be AUTOMATIC ROBOTIC VACUUM CLEANER.
*/
@SerializedName("23")
VACUUM_CLEANER,
@SerializedName("24")
WASHER_DRYER,
@SerializedName("25")
DISH_WARMER,
@SerializedName("27")
HOB_INDUCTION,
@SerializedName("28")
HOB_GAS,
@SerializedName("31")
STEAM_OVEN_COMBINATION,
@SerializedName("32")
WINE_CABINET,
@SerializedName("33")
WINE_CONDITIONING_UNIT,
@SerializedName("34")
WINE_STORAGE_CONDITIONING_UNIT,
@SerializedName("39")
DOUBLE_OVEN,
@SerializedName("40")
DOUBLE_STEAM_OVEN,
@SerializedName("41")
DOUBLE_STEAM_OVEN_COMBINATION,
@SerializedName("42")
DOUBLE_MICROWAVE,
@SerializedName("43")
DOUBLE_MICROWAVE_OVEN,
@SerializedName("45")
STEAM_OVEN_MICROWAVE_COMBINATION,
@SerializedName("48")
VACUUM_DRAWER,
@SerializedName("67")
DIALOGOVEN,
@SerializedName("68")
WINE_CABINET_FREEZER_COMBINATION,
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the current drying step, queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class DryingStep {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DryingStep other = (DryingStep) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "DryingStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ keyLocalized + "]";
}
}

View File

@ -0,0 +1,76 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* Immutable POJO representing an error message. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class ErrorMessage {
@Nullable
private String message;
/**
* Creates a new {@link ErrorMessage} from the given Json text.
*
* @param json The Json text.
* @return The created {@link ErrorMessage}.
* @throws MieleSyntaxException if parsing the data from {@code json} fails.
*/
public static ErrorMessage fromJson(String json) {
try {
ErrorMessage errorMessage = new Gson().fromJson(json, ErrorMessage.class);
if (errorMessage == null) {
throw new MieleSyntaxException("Failed to parse Json.");
}
return errorMessage;
} catch (JsonSyntaxException e) {
throw new MieleSyntaxException("Failed to parse Json.", e);
}
}
public Optional<String> getMessage() {
return Optional.ofNullable(message);
}
@Override
public int hashCode() {
return Objects.hash(message);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ErrorMessage other = (ErrorMessage) obj;
return Objects.equals(message, other.message);
}
}

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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Immutable POJO representing the device identification queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class Ident {
@Nullable
private Type type;
@Nullable
private String deviceName;
@Nullable
private DeviceIdentLabel deviceIdentLabel;
@Nullable
private XkmIdentLabel xkmIdentLabel;
public Optional<Type> getType() {
return Optional.ofNullable(type);
}
public Optional<String> getDeviceName() {
return Optional.ofNullable(deviceName);
}
public Optional<DeviceIdentLabel> getDeviceIdentLabel() {
return Optional.ofNullable(deviceIdentLabel);
}
public Optional<XkmIdentLabel> getXkmIdentLabel() {
return Optional.ofNullable(xkmIdentLabel);
}
@Override
public int hashCode() {
return Objects.hash(deviceIdentLabel, deviceName, type, xkmIdentLabel);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Ident other = (Ident) obj;
return Objects.equals(deviceIdentLabel, other.deviceIdentLabel) && Objects.equals(deviceName, other.deviceName)
&& Objects.equals(type, other.type) && Objects.equals(xkmIdentLabel, other.xkmIdentLabel);
}
@Override
public String toString() {
return "Ident [type=" + type + ", deviceName=" + deviceName + ", deviceIdentLabel=" + deviceIdentLabel
+ ", xkmIdentLabel=" + xkmIdentLabel + "]";
}
}

View File

@ -0,0 +1,74 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents the state of a light on a Miele device.
*
* @author Roland Edelhoff - Initial contribution
* @author Björn Lange - Added NOT_SUPPORTED entry
*/
@NonNullByDefault
public enum Light {
/**
* {Light} for unknown states.
*/
UNKNOWN(),
ENABLE(1),
DISABLE(2),
NOT_SUPPORTED(0, 255);
private List<Integer> ids;
Light(int... ids) {
this.ids = Collections.unmodifiableList(Arrays.stream(ids).boxed().collect(Collectors.toList()));
}
/**
* Gets the {@link Light} state matching the given ID.
*
* @param id The ID.
* @return The matching {@link Light} or {@code UNKNOWN} if no ID matches.
*/
public static Light fromId(@Nullable Integer id) {
for (Light light : Light.values()) {
if (light.ids.contains(id)) {
return light;
}
}
return Light.UNKNOWN;
}
/**
* Formats this instance for interaction with the Miele webservice.
*/
public String format() {
if (ids.isEmpty()) {
return "";
} else {
return Integer.toString(ids.get(0));
}
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link RuntimeException} thrown when the syntax of a message received from the Miele REST API does not match and
* cannot be interpreted as the expected syntax (e.g. by ignoring entries).
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class MieleSyntaxException extends RuntimeException {
private static final long serialVersionUID = 8253804935427566729L;
public MieleSyntaxException(String message) {
super(message);
}
public MieleSyntaxException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing a plate power state. Queried from the Miele REST API.
*
* @author Benjamin Bolte - Initial contribution
*/
@NonNullByDefault
public class PlateStep {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
PlateStep other = (PlateStep) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "PlateStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", key_localized="
+ keyLocalized + "]";
}
}

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.mielecloud.internal.webservice.api.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents a process action.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public enum ProcessAction {
/**
* {@StateType} for unknown states.
*/
UNKNOWN,
@SerializedName("1")
START,
@SerializedName("2")
STOP,
@SerializedName("3")
PAUSE,
@SerializedName("4")
START_SUPERFREEZING,
@SerializedName("5")
STOP_SUPERFREEZING,
@SerializedName("6")
START_SUPERCOOLING,
@SerializedName("7")
STOP_SUPERCOOLING,
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the program type that is currently running. Queried from the Miele REST API.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public class ProgramId {
@SerializedName("value_raw")
@Nullable
private Long valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Long> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ProgramId other = (ProgramId) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ keyLocalized + "]";
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the current program's phase. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class ProgramPhase {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ProgramPhase other = (ProgramPhase) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "ProgramPhase [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ keyLocalized + "]";
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the type of program currently running. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class ProgramType {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ProgramType other = (ProgramType) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ keyLocalized + "]";
}
}

View File

@ -0,0 +1,65 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Immutable POJO representing the remote control capabilities of a device. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class RemoteEnable {
@Nullable
private Boolean fullRemoteControl;
@Nullable
private Boolean smartGrid;
public Optional<Boolean> getFullRemoteControl() {
return Optional.ofNullable(fullRemoteControl);
}
public Optional<Boolean> getSmartGrid() {
return Optional.ofNullable(smartGrid);
}
@Override
public int hashCode() {
return Objects.hash(fullRemoteControl, smartGrid);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RemoteEnable other = (RemoteEnable) obj;
return Objects.equals(fullRemoteControl, other.fullRemoteControl) && Objects.equals(smartGrid, other.smartGrid);
}
@Override
public String toString() {
return "RemoteEnable [fullRemoteControl=" + fullRemoteControl + ", smartGrid=" + smartGrid + "]";
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the current spinning speed, queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class SpinningSpeed {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("unit")
@Nullable
private String unit;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getUnit() {
return Optional.ofNullable(unit);
}
@Override
public int hashCode() {
return Objects.hash(unit, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SpinningSpeed other = (SpinningSpeed) obj;
return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "SpinningSpeed [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
}
}

View File

@ -0,0 +1,238 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Immutable POJO representing the state of a device. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
* @author Benjamin Bolte - Add plate step
* @author Björn Lange - Add elapsed time channel
*/
@NonNullByDefault
public class State {
@Nullable
private Status status;
/**
* Currently used by Miele webservice.
*/
@Nullable
private ProgramId ProgramID;
/**
* Planned to be used in the future.
*/
@Nullable
private ProgramId programId;
@Nullable
private ProgramType programType;
@Nullable
private ProgramPhase programPhase;
@Nullable
private final List<Integer> remainingTime = null;
@Nullable
private final List<Integer> startTime = null;
@Nullable
private final List<Temperature> targetTemperature = null;
@Nullable
private final List<Temperature> temperature = null;
@Nullable
private Boolean signalInfo;
@Nullable
private Boolean signalFailure;
@Nullable
private Boolean signalDoor;
@Nullable
private RemoteEnable remoteEnable;
@Nullable
private Integer light;
@Nullable
private final List<Integer> elapsedTime = null;
@Nullable
private SpinningSpeed spinningSpeed;
@Nullable
private DryingStep dryingStep;
@Nullable
private VentilationStep ventilationStep;
@Nullable
private final List<PlateStep> plateStep = null;
@Nullable
private Integer batteryLevel;
public Optional<Status> getStatus() {
return Optional.ofNullable(status);
}
public Optional<ProgramId> getProgramId() {
// There is a typo for the program ID in the Miele Cloud API, which will be corrected in the future.
// For the sake of robustness, we currently support both upper and lower case.
return Optional.ofNullable(programId != null ? programId : ProgramID);
}
public Optional<ProgramType> getProgramType() {
return Optional.ofNullable(programType);
}
public Optional<ProgramPhase> getProgramPhase() {
return Optional.ofNullable(programPhase);
}
/**
* Gets the remaining time encoded as {@link List} of {@link Integer} values.
*
* @return The remaining time encoded as {@link List} of {@link Integer} values.
*/
public Optional<List<Integer>> getRemainingTime() {
if (remainingTime == null) {
return Optional.empty();
}
return Optional.ofNullable(Collections.unmodifiableList(remainingTime));
}
/**
* Gets the start time encoded as {@link List} of {@link Integer} values.
*
* @return The start time encoded as {@link List} of {@link Integer} values.
*/
public Optional<List<Integer>> getStartTime() {
if (startTime == null) {
return Optional.empty();
}
return Optional.ofNullable(Collections.unmodifiableList(startTime));
}
public List<Temperature> getTargetTemperature() {
if (targetTemperature == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(targetTemperature);
}
public List<Temperature> getTemperature() {
if (temperature == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(temperature);
}
public Optional<Boolean> getSignalInfo() {
return Optional.ofNullable(signalInfo);
}
public Optional<Boolean> getSignalFailure() {
return Optional.ofNullable(signalFailure);
}
public Optional<Boolean> getSignalDoor() {
return Optional.ofNullable(signalDoor);
}
public Optional<RemoteEnable> getRemoteEnable() {
return Optional.ofNullable(remoteEnable);
}
public Light getLight() {
return Light.fromId(light);
}
/**
* Gets the elapsed time encoded as {@link List} of {@link Integer} values.
*
* @return The elapsed time encoded as {@link List} of {@link Integer} values.
*/
public Optional<List<Integer>> getElapsedTime() {
if (elapsedTime == null) {
return Optional.empty();
}
return Optional.ofNullable(Collections.unmodifiableList(elapsedTime));
}
public Optional<SpinningSpeed> getSpinningSpeed() {
return Optional.ofNullable(spinningSpeed);
}
public Optional<DryingStep> getDryingStep() {
return Optional.ofNullable(dryingStep);
}
public Optional<VentilationStep> getVentilationStep() {
return Optional.ofNullable(ventilationStep);
}
public List<PlateStep> getPlateStep() {
if (plateStep == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(plateStep);
}
public Optional<Integer> getBatteryLevel() {
return Optional.ofNullable(batteryLevel);
}
@Override
public int hashCode() {
return Objects.hash(dryingStep, elapsedTime, light, programPhase, ProgramID, programId, programType,
remainingTime, remoteEnable, signalDoor, signalFailure, signalInfo, startTime, status,
targetTemperature, temperature, ventilationStep, plateStep, batteryLevel);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
State other = (State) obj;
return Objects.equals(dryingStep, other.dryingStep) && Objects.equals(elapsedTime, other.elapsedTime)
&& Objects.equals(light, other.light) && Objects.equals(programPhase, other.programPhase)
&& Objects.equals(ProgramID, other.ProgramID) && Objects.equals(programId, other.programId)
&& Objects.equals(programType, other.programType) && Objects.equals(remainingTime, other.remainingTime)
&& Objects.equals(remoteEnable, other.remoteEnable) && Objects.equals(signalDoor, other.signalDoor)
&& Objects.equals(signalFailure, other.signalFailure) && Objects.equals(signalInfo, other.signalInfo)
&& Objects.equals(startTime, other.startTime) && Objects.equals(status, other.status)
&& Objects.equals(targetTemperature, other.targetTemperature)
&& Objects.equals(temperature, other.temperature)
&& Objects.equals(ventilationStep, other.ventilationStep) && Objects.equals(plateStep, other.plateStep)
&& Objects.equals(batteryLevel, other.batteryLevel);
}
@Override
public String toString() {
return "State [status=" + status + ", programId=" + getProgramId() + ", programType=" + programType
+ ", programPhase=" + programPhase + ", remainingTime=" + remainingTime + ", startTime=" + startTime
+ ", targetTemperature=" + targetTemperature + ", temperature=" + temperature + ", signalInfo="
+ signalInfo + ", signalFailure=" + signalFailure + ", signalDoor=" + signalDoor + ", remoteEnable="
+ remoteEnable + ", light=" + light + ", elapsedTime=" + elapsedTime + ", dryingStep=" + dryingStep
+ ", ventilationStep=" + ventilationStep + ", plateStep=" + plateStep + ", batteryLevel=" + batteryLevel
+ "]";
}
}

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.mielecloud.internal.webservice.api.json;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the Miele device state.
*
* @author Roland Edelhoff - Initial contribution
*/
@NonNullByDefault
public enum StateType {
OFF(1),
ON(2),
PROGRAMMED(3),
PROGRAMMED_WAITING_TO_START(4),
RUNNING(5),
PAUSE(6),
END_PROGRAMMED(7),
FAILURE(8),
PROGRAMME_INTERRUPTED(9),
IDLE(10),
RINSE_HOLD(11),
SERVICE(12),
SUPERFREEZING(13),
SUPERCOOLING(14),
SUPERHEATING(15),
SUPERCOOLING_SUPERFREEZING(146),
NOT_CONNECTED(255);
private static final Map<Integer, StateType> STATE_TYPE_BY_CODE;
static {
Map<Integer, StateType> stateTypeByCode = new HashMap<>();
for (StateType stateType : values()) {
stateTypeByCode.put(stateType.code, stateType);
}
STATE_TYPE_BY_CODE = Collections.unmodifiableMap(stateTypeByCode);
}
private final int code;
private StateType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static Optional<StateType> fromCode(int code) {
return Optional.ofNullable(STATE_TYPE_BY_CODE.get(code));
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the actual status of a device. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class Status {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Status other = (Status) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "Status [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized=" + keyLocalized
+ "]";
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing a temperature value. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class Temperature {
@SerializedName("value_raw")
@Nullable
private Integer valueRaw;
@SerializedName("value_localized")
@Nullable
private Double valueLocalized;
@SerializedName("unit")
@Nullable
private String unit;
public Optional<Integer> getValueRaw() {
return Optional.ofNullable(valueRaw);
}
public Optional<Integer> getValueLocalized() {
return Optional.ofNullable(valueLocalized).map(Double::intValue);
}
public Optional<String> getUnit() {
return Optional.ofNullable(unit);
}
@Override
public int hashCode() {
return Objects.hash(unit, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Temperature other = (Temperature) obj;
return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
&& Objects.equals(valueRaw, other.valueRaw);
}
@Override
public String toString() {
return "Temperature [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.mielecloud.internal.webservice.api.json;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Immutable POJO representing the type of a device. Queried from the Miele REST API.
*
* @author Björn Lange - Initial contribution
*/
@NonNullByDefault
public class Type {
@SerializedName("key_localized")
@Nullable
private String keyLocalized;
@SerializedName("value_raw")
@Nullable
private DeviceType valueRaw;
@SerializedName("value_localized")
@Nullable
private String valueLocalized;
public Optional<String> getKeyLocalized() {
return Optional.ofNullable(keyLocalized);
}
public DeviceType getValueRaw() {
return Optional.ofNullable(valueRaw).orElse(DeviceType.UNKNOWN);
}
public Optional<String> getValueLocalized() {
return Optional.ofNullable(valueLocalized);
}
@Override
public int hashCode() {
return Objects.hash(keyLocalized, valueLocalized, valueRaw);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Type other = (Type) obj;
return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
&& valueRaw == other.valueRaw;
}
@Override
public String toString() {
return "Type [keyLocalized=" + keyLocalized + ", valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized
+ "]";
}
}

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