mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[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:
parent
5ec535f37b
commit
705f5c577c
@ -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
|
||||
|
@ -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>
|
||||
|
13
bundles/org.openhab.binding.mielecloud/NOTICE
Normal file
13
bundles/org.openhab.binding.mielecloud/NOTICE
Normal 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
|
623
bundles/org.openhab.binding.mielecloud/README.md
Normal file
623
bundles/org.openhab.binding.mielecloud/README.md
Normal 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 |
BIN
bundles/org.openhab.binding.mielecloud/doc/miele-login.png
Normal file
BIN
bundles/org.openhab.binding.mielecloud/doc/miele-login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
bundles/org.openhab.binding.mielecloud/doc/pair-account.png
Normal file
BIN
bundles/org.openhab.binding.mielecloud/doc/pair-account.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
bundles/org.openhab.binding.mielecloud/doc/pairing-success.png
Normal file
BIN
bundles/org.openhab.binding.mielecloud/doc/pairing-success.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
17
bundles/org.openhab.binding.mielecloud/pom.xml
Normal file
17
bundles/org.openhab.binding.mielecloud/pom.xml
Normal 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>
|
@ -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>
|
@ -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";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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("\">< ></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, "");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 + "\".";
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 {
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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
|
||||
+ "]";
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
+ "]";
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user