From: bruestel Date: Thu, 13 May 2021 12:56:03 +0000 (+0200) Subject: [homeconnect] Initial contribution (#9187) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=a1a990989e2674474e593552e6ee309fd06b0bfc;p=openhab-addons.git [homeconnect] Initial contribution (#9187) Signed-off-by: Jonas Brüstel Co-authored-by: Laurent Garnier --- diff --git a/CODEOWNERS b/CODEOWNERS index 0b3707515f..bd3f9741e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -109,6 +109,7 @@ /bundles/org.openhab.binding.helios/ @kgoderis /bundles/org.openhab.binding.heliosventilation/ @ramack /bundles/org.openhab.binding.heos/ @Wire82 +/bundles/org.openhab.binding.homeconnect/ @bruestel /bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s /bundles/org.openhab.binding.homewizard/ @Daniel-42 /bundles/org.openhab.binding.hpprinter/ @cossey diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 4735007ce2..b163c68bdb 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -531,6 +531,11 @@ org.openhab.binding.heos ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.homeconnect + ${project.version} + org.openhab.addons.bundles org.openhab.binding.homematic diff --git a/bundles/org.openhab.binding.homeconnect/NOTICE b/bundles/org.openhab.binding.homeconnect/NOTICE new file mode 100644 index 0000000000..f780805ab9 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/NOTICE @@ -0,0 +1,65 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab2-addons + +== Third-party Content + +Thymeleaf +* License: Apache License 2.0 +* Project: https://www.thymeleaf.org/ +* Source: https://github.com/thymeleaf/thymeleaf + +Thymeleaf - Java 8 Time API compatibility +* License: Apache License 2.0 +* Project: https://www.thymeleaf.org/ +* Source: https://github.com/thymeleaf/thymeleaf-extras-java8time + +OGNL Object Graph Navigation Library +* License: Apache License 2.0 +* Project: http://www.opensymphony.com/ognl/ +* Source: https://github.com/jkuhnert/ognl/ + +Javassist +* License: Apache License 2.0 +* Project: http://www.javassist.org/ +* Source: https://github.com/jboss-javassist/javassist + +ATTOPARSER +* License: Apache License 2.0 +* Project: http://www.attoparser.org/ +* Source: https://github.com/attoparser/attoparser + +UNBESCAPE +* License: Apache License 2.0 +* Project: http://www.unbescape.org/ +* Source: https://github.com/unbescape/unbescape + +Bucket4j +* License: Apache License 2.0 +* Project: https://github.com/vladimir-bukhtoyarov/bucket4j +* Source: https://github.com/vladimir-bukhtoyarov/bucket4j + +Feather icons +* License: MIT License +* Project: https://feathericons.com/ +* Source: https://github.com/feathericons/feather + +jQuery +* License: MIT License +* Project: https://jquery.com/ +* Source: https://github.com/jquery/jquery + +Bootstrap +* License: MIT License +* Project: https://getbootstrap.com/ +* Source: https://github.com/twbs/bootstrap diff --git a/bundles/org.openhab.binding.homeconnect/README.md b/bundles/org.openhab.binding.homeconnect/README.md new file mode 100644 index 0000000000..2f349d4e2e --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/README.md @@ -0,0 +1,317 @@ +# Home Connect Binding + +The binding integrates the [Home Connect](https://www.home-connect.com/) system into openHAB. +By using the Home Connect API it connects to household devices from brands like Bosch and Siemens. + +Because all status updates and commands have to go through the API, a permanent internet connection is required. + +## Supported Things + +### Bridge + +The __Home Connect API__ (Bridge Type ID: api_bridge) is responsible for the communication with the Home Connect API. All devices use a bridge to execute commands and listen for updates. Without a working bridge the devices cannot communicate. + +### Devices + +Supported devices: dishwasher, washer, washer / dryer combination, dryer, oven, refrigerator freezer, coffee machine, hood, cooktop* + +*\* experimental support* + +| Home appliance | Thing Type ID | +| --------------- | ------------ | +| Dishwasher | dishwasher | +| Washer | washer | +| Washer / Dryer combination | washerdryer | +| Dryer | dryer | +| Oven | oven | +| Hood | hood | +| Cooktop | hob | +| Refrigerator Freezer | fridgefreezer | +| Coffee Machine | coffeemaker | + +> **INFO**: Currently the Home Connect API does not support all appliance programs. Please check if your desired program is available (e.g. https://developer.home-connect.com/docs/washing-machine/supported_programs_and_options). + + +## Discovery + +After the bridge has been added and authorized, devices are discovered automatically. + + +## Channels + +| Channel Type ID | Item Type | Read only | Description | Available on thing | +| --------------- | --------- | --------- | ----------- | ------------------ | +| power_state | Switch | false | This setting describes the current power state of the home appliance. | dishwasher, oven, coffeemaker, hood, hob | +| door_state | Contact | true | This status describes the door state of a home appliance. A status change is either triggered by the user operating the home appliance locally (i.e. opening/closing door) or automatically by the home appliance (i.e. locking the door). | dishwasher, washer, washerdryer, dryer, oven, fridgefreezer | +| operation_state | String | true | This status describes the operation state of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hood, hob, coffeemaker | +| remote_start_allowance_state | Switch | true | This status indicates whether the remote program start is enabled. This can happen due to a programmatic change (only disabling), or manually by the user changing the flag locally on the home appliance, or automatically after a certain duration - usually in 24 hours. | dishwasher, washer, washerdryer, dryer, oven, hood, coffeemaker | +| remote_control_active_state | Switch | true | This status indicates whether the allowance for remote controlling is enabled. | dishwasher, washer, washerdryer, dryer, oven, hood, hob | +| active_program_state | String | true | This status describes the active program of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hood, hob, coffeemaker | +| selected_program_state | String | false | This state describes the selected program of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hob, coffeemaker | +| remaining_program_time_state | Number:Time | true | This status indicates the remaining program time of the home appliance. | dishwasher, washer, washerdryer, dryer, oven | +| elapsed_program_time | Number:Time | true | This status indicates the elapsed program time of the home appliance. | oven | +| program_progress_state | Number:Dimensionless | true | This status describes the program progress of the home appliance in percent. | dishwasher, washer, washerdryer, dryer, oven, coffeemaker | +| duration | Number:Time | true | This status describes the duration of the program of the home appliance. | oven | +| oven_current_cavity_temperature | Number:Temperature | true | This status describes the current cavity temperature of the home appliance. | oven | +| setpoint_temperature | Number:Temperature | false | This status describes the setpoint/target temperature of the home appliance. | oven | +| laundry_care_washer_temperature | String | false | This status describes the temperature of the washing program of the home appliance. | washer, washerdryer | +| laundry_care_washer_spin_speed | String | false | This status defines the spin speed of a washer program of the home appliance. | washer, washerdryer | +| laundry_care_washer_idos1 | String | false | This status defines the i-Dos 1 dosing level of a washer program of the home appliance (if appliance supports i-Dos). | washer | +| laundry_care_washer_idos2 | String | false | This status defines the i-Dos 2 dosing level of a washer program of the home appliance (if appliance supports i-Dos). | washer | +| dryer_drying_target | String | false | This status defines the desired dryness of a program of the home appliance. | dryer, washerdryer | +| setpoint_temperature_refrigerator | Number:Temperature | false | Target temperature of the refrigerator compartment (range depends on appliance - common range 2 to 8°C). | fridgefreezer | +| setpoint_temperature_freezer | Number:Temperature | false | Target temperature of the freezer compartment (range depends on appliance - common range -16 to -24°C). | fridgefreezer | +| super_mode_refrigerator | Switch | false | The setting has no impact on setpoint temperatures but will make the fridge compartment cool to the lowest possible temperature until it is disabled manually by the customer or by the HA because of a timeout. | fridgefreezer | +| super_mode_freezer | Switch | false | This setting has no impact on setpoint temperatures but will make the freezer compartment cool to the lowest possible temperature until it is disabled manually by the customer or by the home appliance because of a timeout. | fridgefreezer | +| coffeemaker_drip_tray_full_state | Switch | true | Is coffee maker drip tray full? | coffeemaker | +| coffeemaker_water_tank_empty_state | Switch | true | Is coffee maker water tank empty? | coffeemaker | +| coffeemaker_bean_container_empty_state | Switch | true | Is coffee maker bean container empty? | coffeemaker | +| hood_venting_level | String | true | This option defines the required fan setting of the hood. | hood | +| hood_intensive_level | String | true | This option defines the intensive setting of the hood. | hood | +| hood_program_state | String | false | Adds hood controller actions to the appliance. The following commands are supported: `stop`, `venting1`, `venting2`, `venting3`, `venting4`, `venting5`, `ventingIntensive1`, `ventingIntensive1`, `automatic` and `delayed`. Furthermore it is possible to send raw (Home Connect JSON payload) to the home appliance. | hood | +| basic_actions_state | String | false | Adds basic controller actions to the appliance. The following basic commands are supported: `start` (start current selected program), `stop` (stop current program) and `selected` (show current program information). Furthermore it is possible to send raw (Home Connect JSON payload) to the home appliance. | dishwasher, oven, washer, washerdryer, dryer, coffeemaker | +| functional_light_state | Switch | false | This setting describes the current functional light state of the home appliance. | hood | +| functional_light_brightness_state | Dimmer | false | This setting describes the brightness state of the functional light. | hood | +| ambient_light_state | Switch | false | This setting describes the current ambient light state of the home appliance. | dishwasher, hood | +| ambient_light_brightness_state | Dimmer | false | This setting describes the brightness state of the ambient light. *INFO: Please note that the brightness can't be set if the ambient light color is set to `CustomColor`.* | dishwasher, hood | +| ambient_light_color_state | String | false | This setting describes the current ambient light color state of the home appliance. | dishwasher, hood | +| ambient_light_custom_color_state | Color | false | This setting describes the custom color state of the ambient light. HSB color commands are supported as well as hex color string e.g. `#11ff00`. *INFO: Please note that the brightness can't be set.* | dishwasher, hood | + + +## Thing Configuration + +### Configuring the __Home Connect API__ Bridge + + +#### 1. Preconditions + +1. Please create an account at [Home Connect](https://www.home-connect.com/) and add your physical appliance to your account. +2. Test the connection to your physical appliance via mobile app ([Apple App Store (iOS)](https://itunes.apple.com/de/app/home-connect-app/id901397789?mt=8) or [Google Play Store (Android)](https://play.google.com/store/apps/details?id=com.bshg.homeconnect.android.release)). + +#### 2. Create Home Connect developer account + +1. Create an account at [https://developer.home-connect.com](https://developer.home-connect.com) and login. +2. Please make sure you've added your associated Home Connect account email at . You should fill in your email address, which you use for the official Android or iOS app, at `Default Home Connect User Account for Testing`. +![Screenshot Home Connect profile page](doc/home_connect_profile.png "Screenshot Home Connect profile page") + +3. Register / Create an application at [https://developer.home-connect.com/applications](https://developer.home-connect.com/applications) + * _Application ID_: e.g. `openhab-binding` + * _OAuth Flow_: Authorization Code Grant Flow + * _Home Connect User Account for Testing_: the associated user account email from [Home Connect](https://www.home-connect.com/) + > **WARNING**: Please don't use your developer account username + + **_Please don't use your developer account username_** + * _Redirect URIs_: add your openHAB URL followed by `/homeconnect` + for example: `http://192.168.178.34:8080/homeconnect` or `https://myhome.domain.com/homeconnect` + * _One Time Token Mode_: keep unchecked + * _Proof Key for Code Exchange_: keep unchecked +4. After your application has been created, you should see the _Client ID_ and _Client Secret_ of the application. Please save these for later. + +![Screenshot Home Connect application page](doc/home_connect_application.png "Screenshot Home Connect application page") + + + +#### 3. Setup bridge (openHAB UI) + +The Home Connect bridge can be configured in the openHAB UI as follows: + +1. Go to the Inbox and press the add button +2. Choose `Home Connect Binding` +3. Select `Home Connect API` +4. Setup and save thing + * __client id:__ your application client id + * __client secret:__ your application client secret + * __simulator:__ false +5. Now navigate to the URL (`Redirct URI`) you've added to your Home Connect application in the previous step (2.3). For example `http://192.168.178.80:8080/homeconnect`. +6. Please follow the steps shown to authenticate your binding. You can redo this step every time. For example if you have authentication problems, just start wizard again. +![Screenshot Home Connect wizard page 1](doc/homeconnect_setup_1.png "Screenshot Home Connect wizard page 1") +![Screenshot Home Connect wizard page 2](doc/homeconnect_setup_2.png "Screenshot Home Connect wizard page 2") +![Screenshot Home Connect wizard page 3](doc/homeconnect_setup_3.png "Screenshot Home Connect wizard page 3") +![Screenshot Home Connect wizard page 4](doc/homeconnect_setup_4.png "Screenshot Home Connect wizard page 4") + +7. That's it! Now you can use autodiscovery to add devices. Your devices should show up if you start a device scan in the openHAB UI. + + + +## Examples: File based configuration + +If you prefer to configure everything via file instead of openHAB UI, here are some examples. + +### things/homeconnect.things + +``` +Bridge homeconnect:api_bridge:api_bridge_at_home "Home Connect API" [ clientId="1234", clientSecret="1234", simulator=false] { + // Thing configurations + Thing dishwasher dishwasher1 "Dishwasher" [ haId="SIEMENS-HCS02DWH1-6F2FC400C1EA4A" ] + Thing washer washer1 "Washer" [ haId="SIEMENS-HCS03WCH1-1F35EC2BE34A0F" ] + Thing fridgefreezer fridge1 "Fridge Freezer [ haId="SIEMENS-HCS05FRF1-7B3FA5EB3D885B" ] + Thing oven oven1 "Oven" [ haId="BOSCH-HCS01OVN1-2132B6FA25BA21" ] + Thing dryer dryer1 "Dryer" [ haId="BOSCH-HCS04DYR1-3921C766AD5BAF" ] + Thing coffeemaker coffee1 "Coffee machine" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ] + Thing washerdryer washerdryer1 "Washerdryer" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ] + Thing fridgefreezer fridgefreezer1 "Fridge/Freezer" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ] + Thing hood hood1 "Hood" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ] + Thing hob hob1 "Hob" [ haId="BOSCH-HCS06COM1-2140A8821AE7AB" ] +} +``` + +### items/homeconnect.items + +The channel parameter uses the following syntax: `homeconnect::::`. For example: `homeconnect:dishwasher:api_bridge_at_home:dishwasher1:power_state` + +``` +// dishwasher +Switch Dishwasher_PowerState "Power State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:power_state"} +Contact Dishwasher_DoorState "Door State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:door_state"} +String Dishwasher_OperationState "Operation State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:operation_state"} +Switch Dishwasher_RemoteStartAllowanceState "Remote Start Allowance State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remote_start_allowance_state"} +Switch Dishwasher_RemoteControlActiveState "Remote Control Activation State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remote_control_active_state"} +String Dishwasher_SelectedProgramState "Selected Program" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:selected_program_state"} +String Dishwasher_ActiveProgramState "Active Program" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:active_program_state"} +Number:Time Dishwasher_RemainingProgramTimeState "Remaining program time" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:remaining_program_time_state"} +Number:Dimensionless Dishwasher_ProgramProgressState "Progress State" {channel="homeconnect:dishwasher:api_bridge_at_home:dishwasher1:program_progress_state"} +``` + +## Home Connect Console + +The binding comes with a separate user interface, which is reachable through the web browser http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect (e.g. http://192.168.178.100:8080/homeconnect). + +Features: + +* overview of your bridges and appliances +* send commands to your appliances +* see latest API requests +* see received events from the Home Connect backend +* API request counts + +> **INFO**: If you have a problems with your installation, please always provide request and event exports. ![Screenshot Home Connect wizard page 4](doc/export_button.png "Export button") + +## How To + +### Notification on credential error + +To get notified when your Home Connect credentials have been revoked or expired you can use the following rule to get notified. + +This can happen if + +* your openHAB instance was offline for a longer period or +* new terms weren't accepted or +* a technical problem occurred. + +```java +rule "Offline check - Home Connect bridge" +when + Thing "" changed +then + val statusInfo = getThingStatusInfo("") + val status = statusInfo.getStatus() + val statusDetail = statusInfo.getStatusDetail() + + if ((status !== null) && (statusDetail !== null)) { + logInfo("api_bridge", "Home Connect bridge status: " + status.toString() + " detail: " + statusDetail.toString()) + if (status.toString() == 'OFFLINE' && statusDetail.toString() == 'CONFIGURATION_PENDING') { + logError("api_bridge", "Home Connect bridge offline.") + // send push, email, ... + } + } +end +``` + +### Start program with custom settings + +Currently, not all program options of a device are available as items in openHAB. For example, you cannot change the `Fill quantity` of a coffee maker program. If you wish to start a program with a custom setting, you can send a special command to the item of type `basic_actions_state`. + +> **INFO**: Only for advanced users. You need to know how to use the `curl` command. Alternatively you you can use the binding UI to trigger the commands. + +#### 1. Retrieve "special command" payload + +You have a couple options to get the program settings payload. + +a) You could have a look at the Home Connect developer documentation (https://developer.home-connect.com/docs/) and create the payload on your own. + +b) You could have a look at the request logs and extract the payload from there. + +1. On the physical device, select your desired program with the appropriate options. +2. Open the appliance section of the binding UI (http(s)://[YOUROPENHAB]:[YOURPORT]/appliances) and click the 'Selected Program' button. +![Screenshot Home Connect wizard page 4](doc/selected_program_1.png "Get selected program") +3. ![Screenshot Home Connect wizard page 4](doc/selected_program_2.png "Get selected program") Copy the JSON payload. In a further step, this payload will be used to start the program. + + +#### 2. Start program + +After you've extracted the desired program command, you can start your program via openHAB rule or through a `curl` command. + +##### in rule + +*Example rule:* + +```java +rule "trigger program" +when + Time cron "0 32 13 ? * * *" +then + homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state.sendCommand('{"data":{"key":"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato","options":[{"key":"ConsumerProducts.CoffeeMaker.Option.CoffeeTemperature","value":"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.BeanAmount","value":"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.FillQuantity","value":60,"unit":"ml"}]}}') +end +``` + +Please replace `homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state` with your item name (of channel type `basic_actions_state`). + +##### via curl + +*Example command:* + +```bash +curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d '{"data":{"key":"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato","options":[{"key":"ConsumerProducts.CoffeeMaker.Option.CoffeeTemperature","value":"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.BeanAmount","value":"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild","unit":"enum"},{"key":"ConsumerProducts.CoffeeMaker.Option.FillQuantity","value":60,"unit":"ml"}]}}' "http://localhost:8080/rest/items/homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state" +``` + +Please replace `homeconnect_CoffeeMaker_BOSCH_HCS06COM1_B95E5103934D_basic_actions_state` with your item name (of channel type `basic_actions_state`). + +## FAQ + +### I can't start my oven via openHAB. + +Some operations are not possible at the moment. You need to sign an "Additional Partner Agreement". Please have a look at: +https://developer.home-connect.com/docs/authorization/scope + +### I can't switch remote start to on. + +The channel of type `remote_start_allowance_state` is read only. You can only enable it directly on the physical appliance. + +### In case of error... + +Please check log UI (http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect) and ask for help in the community forum or on github. Please provide request and event exports. + ![Screenshot Home Connect wizard page 4](doc/export_button.png "Export button") + +### Rate limit reached + +The Home Connect API enforces rate [limits](https://developer.home-connect.com/docs/general/ratelimiting). If you have a lot of `429` response codes in your request log section (http(s)://[YOUROPENHAB]:[YOURPORT]/log/requests), please check the error response. + + +### Error message 'Program not supported', 'Unsupported operation' or 'SDK.Error.UnsupportedOption' + +Not all appliance programs and program options are supported by the Home Connect API. Unfortunately you can't use them. You will see error messages like the following in the binding UI (request log): + +```json +{ + "error": { + "key": "SDK.Error.UnsupportedProgram", + "description": "Unsupported operation: LaundryCare.Washer.Program.Cotton.CottonEco" + } +} +``` + +```json +{ + "error": { + "key": "SDK.Error.UnsupportedProgram", + "description": "Program not supported" + } +} +``` + +### How to find the Home Appliance ID (HaID) of my device? + +You have two options to find the right HaID of your device. + +1. You can use the openHAB UI and start a scan. ![Screenshot openHAB UI Scan for new devices](doc/ui-scan-for-haid.png "Scan") +2. You can use Home Connect binding UI. Please have a look at the first API request. ![Screenshot Home Connect Binding UI](doc/binding-ui-haid.png "First request") diff --git a/bundles/org.openhab.binding.homeconnect/doc/binding-ui-haid.png b/bundles/org.openhab.binding.homeconnect/doc/binding-ui-haid.png new file mode 100644 index 0000000000..c84f43a4c8 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/binding-ui-haid.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/export_button.png b/bundles/org.openhab.binding.homeconnect/doc/export_button.png new file mode 100644 index 0000000000..b6a6caa7cf Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/export_button.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/home_connect_application.png b/bundles/org.openhab.binding.homeconnect/doc/home_connect_application.png new file mode 100644 index 0000000000..9b02c6f428 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/home_connect_application.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/home_connect_profile.png b/bundles/org.openhab.binding.homeconnect/doc/home_connect_profile.png new file mode 100644 index 0000000000..714d240808 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/home_connect_profile.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/homeconnect_log_selected.png b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_log_selected.png new file mode 100644 index 0000000000..8379cd6a70 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_log_selected.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_1.png b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_1.png new file mode 100644 index 0000000000..6ae3af67f1 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_1.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_2.png b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_2.png new file mode 100644 index 0000000000..e002ac9903 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_2.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_3.png b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_3.png new file mode 100644 index 0000000000..c2f08519e8 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_3.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_4.png b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_4.png new file mode 100644 index 0000000000..447f9a613d Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/homeconnect_setup_4.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/selected_program_1.png b/bundles/org.openhab.binding.homeconnect/doc/selected_program_1.png new file mode 100644 index 0000000000..3d19e42644 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/selected_program_1.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/selected_program_2.png b/bundles/org.openhab.binding.homeconnect/doc/selected_program_2.png new file mode 100644 index 0000000000..a9da17ba74 Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/selected_program_2.png differ diff --git a/bundles/org.openhab.binding.homeconnect/doc/ui-scan-for-haid.png b/bundles/org.openhab.binding.homeconnect/doc/ui-scan-for-haid.png new file mode 100644 index 0000000000..1f666281fd Binary files /dev/null and b/bundles/org.openhab.binding.homeconnect/doc/ui-scan-for-haid.png differ diff --git a/bundles/org.openhab.binding.homeconnect/pom.xml b/bundles/org.openhab.binding.homeconnect/pom.xml new file mode 100644 index 0000000000..7733bf370c --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.homeconnect + + openHAB Add-ons :: Bundles :: Home Connect Binding + + + android.*;resolution:="optional",com.android.*;resolution:="optional",dalvik.*;resolution:="optional",org.apache.harmony.*;resolution:="optional",org.conscrypt.*;resolution:="optional",sun.*;resolution:="optional",javax.annotation.meta.*;resolution:="optional",com.fasterxml.jackson.*;resolution:="optional",com.sun.jdi.*;resolution:="optional" + + + + + + + org.thymeleaf + thymeleaf + 3.0.11.RELEASE + compile + + + org.thymeleaf.extras + thymeleaf-extras-java8time + 3.0.4.RELEASE + + + ognl + ognl + 3.1.12 + compile + + + org.javassist + javassist + 3.20.0-GA + compile + + + org.attoparser + attoparser + 2.0.5.RELEASE + compile + + + org.unbescape + unbescape + 1.1.6.RELEASE + compile + + + + + com.github.vladimir-bukhtoyarov + bucket4j-core + 4.10.0 + + + diff --git a/bundles/org.openhab.binding.homeconnect/src/main/feature/feature.xml b/bundles/org.openhab.binding.homeconnect/src/main/feature/feature.xml new file mode 100644 index 0000000000..c58c8a7f7d --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.homeconnect/${project.version} + + diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java new file mode 100644 index 0000000000..67ce71ab69 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.homeconnect.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link HomeConnectBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jonas Brüstel - Initial contribution + */ +@NonNullByDefault +public class HomeConnectBindingConstants { + + public static final String BINDING_ID = "homeconnect"; + + public static final String HA_ID = "haId"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_API_BRIDGE = new ThingTypeUID(BINDING_ID, "api_bridge"); + public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher"); + public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven"); + public static final ThingTypeUID THING_TYPE_WASHER = new ThingTypeUID(BINDING_ID, "washer"); + public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washerdryer"); + public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridgefreezer"); + public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer"); + public static final ThingTypeUID THING_TYPE_COFFEE_MAKER = new ThingTypeUID(BINDING_ID, "coffeemaker"); + public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood"); + public static final ThingTypeUID THING_TYPE_COOKTOP = new ThingTypeUID(BINDING_ID, "hob"); + + // Setting + public static final String SETTING_POWER_STATE = "BSH.Common.Setting.PowerState"; + public static final String SETTING_LIGHTING = "Cooking.Common.Setting.Lighting"; + public static final String SETTING_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled"; + public static final String SETTING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness"; + public static final String SETTING_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness"; + public static final String SETTING_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor"; + public static final String SETTING_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor"; + public static final String SETTING_FREEZER_SETPOINT_TEMPERATURE = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer"; + public static final String SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator"; + public static final String SETTING_REFRIGERATOR_SUPER_MODE = "Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator"; + public static final String SETTING_FREEZER_SUPER_MODE = "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer"; + + // Status + public static final String STATUS_DOOR_STATE = "BSH.Common.Status.DoorState"; + public static final String STATUS_OPERATION_STATE = "BSH.Common.Status.OperationState"; + public static final String STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE = "Cooking.Oven.Status.CurrentCavityTemperature"; + public static final String STATUS_REMOTE_CONTROL_START_ALLOWED = "BSH.Common.Status.RemoteControlStartAllowed"; + public static final String STATUS_REMOTE_CONTROL_ACTIVE = "BSH.Common.Status.RemoteControlActive"; + public static final String STATUS_LOCAL_CONTROL_ACTIVE = "BSH.Common.Status.LocalControlActive"; + + // SSE Event types + public static final String EVENT_ELAPSED_PROGRAM_TIME = "BSH.Common.Option.ElapsedProgramTime"; + public static final String EVENT_OVEN_CAVITY_TEMPERATURE = STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE; + public static final String EVENT_POWER_STATE = SETTING_POWER_STATE; + public static final String EVENT_CONNECTED = "CONNECTED"; + public static final String EVENT_DISCONNECTED = "DISCONNECTED"; + public static final String EVENT_DOOR_STATE = STATUS_DOOR_STATE; + public static final String EVENT_OPERATION_STATE = STATUS_OPERATION_STATE; + public static final String EVENT_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"; + public static final String EVENT_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram"; + public static final String EVENT_REMOTE_CONTROL_START_ALLOWED = STATUS_REMOTE_CONTROL_START_ALLOWED; + public static final String EVENT_REMOTE_CONTROL_ACTIVE = STATUS_REMOTE_CONTROL_ACTIVE; + public static final String EVENT_LOCAL_CONTROL_ACTIVE = STATUS_LOCAL_CONTROL_ACTIVE; + public static final String EVENT_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime"; + public static final String EVENT_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress"; + public static final String EVENT_SETPOINT_TEMPERATURE = "Cooking.Oven.Option.SetpointTemperature"; + public static final String EVENT_DURATION = "BSH.Common.Option.Duration"; + public static final String EVENT_WASHER_TEMPERATURE = "LaundryCare.Washer.Option.Temperature"; + public static final String EVENT_WASHER_SPIN_SPEED = "LaundryCare.Washer.Option.SpinSpeed"; + public static final String EVENT_WASHER_IDOS_1_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos1DosingLevel"; + public static final String EVENT_WASHER_IDOS_2_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos2DosingLevel"; + public static final String EVENT_FREEZER_SETPOINT_TEMPERATURE = SETTING_FREEZER_SETPOINT_TEMPERATURE; + public static final String EVENT_FRIDGE_SETPOINT_TEMPERATURE = SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE; + public static final String EVENT_FREEZER_SUPER_MODE = SETTING_FREEZER_SUPER_MODE; + public static final String EVENT_FRIDGE_SUPER_MODE = SETTING_REFRIGERATOR_SUPER_MODE; + public static final String EVENT_DRYER_DRYING_TARGET = "LaundryCare.Dryer.Option.DryingTarget"; + public static final String EVENT_COFFEEMAKER_BEAN_CONTAINER_EMPTY = "ConsumerProducts.CoffeeMaker.Event.BeanContainerEmpty"; + public static final String EVENT_COFFEEMAKER_WATER_TANK_EMPTY = "ConsumerProducts.CoffeeMaker.Event.WaterTankEmpty"; + public static final String EVENT_COFFEEMAKER_DRIP_TRAY_FULL = "ConsumerProducts.CoffeeMaker.Event.DripTrayFull"; + public static final String EVENT_HOOD_VENTING_LEVEL = "Cooking.Common.Option.Hood.VentingLevel"; + public static final String EVENT_HOOD_INTENSIVE_LEVEL = "Cooking.Common.Option.Hood.IntensiveLevel"; + public static final String EVENT_FUNCTIONAL_LIGHT_STATE = SETTING_LIGHTING; + public static final String EVENT_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE = SETTING_LIGHTING_BRIGHTNESS; + public static final String EVENT_AMBIENT_LIGHT_STATE = SETTING_AMBIENT_LIGHT_ENABLED; + public static final String EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE = SETTING_AMBIENT_LIGHT_BRIGHTNESS; + public static final String EVENT_AMBIENT_LIGHT_COLOR_STATE = SETTING_AMBIENT_LIGHT_COLOR; + public static final String EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE = SETTING_AMBIENT_LIGHT_CUSTOM_COLOR; + + // Channel IDs + public static final String CHANNEL_DOOR_STATE = "door_state"; + public static final String CHANNEL_ELAPSED_PROGRAM_TIME = "elapsed_program_time"; + public static final String CHANNEL_POWER_STATE = "power_state"; + public static final String CHANNEL_OPERATION_STATE = "operation_state"; + public static final String CHANNEL_ACTIVE_PROGRAM_STATE = "active_program_state"; + public static final String CHANNEL_SELECTED_PROGRAM_STATE = "selected_program_state"; + public static final String CHANNEL_BASIC_ACTIONS_STATE = "basic_actions_state"; + public static final String CHANNEL_REMOTE_START_ALLOWANCE_STATE = "remote_start_allowance_state"; + public static final String CHANNEL_REMOTE_CONTROL_ACTIVE_STATE = "remote_control_active_state"; + public static final String CHANNEL_LOCAL_CONTROL_ACTIVE_STATE = "local_control_active_state"; + public static final String CHANNEL_REMAINING_PROGRAM_TIME_STATE = "remaining_program_time_state"; + public static final String CHANNEL_PROGRAM_PROGRESS_STATE = "program_progress_state"; + public static final String CHANNEL_OVEN_CURRENT_CAVITY_TEMPERATURE = "oven_current_cavity_temperature"; + public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint_temperature"; + public static final String CHANNEL_DURATION = "duration"; + public static final String CHANNEL_WASHER_TEMPERATURE = "laundry_care_washer_temperature"; + public static final String CHANNEL_WASHER_SPIN_SPEED = "laundry_care_washer_spin_speed"; + public static final String CHANNEL_WASHER_IDOS1 = "laundry_care_washer_idos1"; + public static final String CHANNEL_WASHER_IDOS2 = "laundry_care_washer_idos2"; + public static final String CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE = "setpoint_temperature_refrigerator"; + public static final String CHANNEL_REFRIGERATOR_SUPER_MODE = "super_mode_refrigerator"; + public static final String CHANNEL_FREEZER_SETPOINT_TEMPERATURE = "setpoint_temperature_freezer"; + public static final String CHANNEL_FREEZER_SUPER_MODE = "super_mode_freezer"; + public static final String CHANNEL_DRYER_DRYING_TARGET = "dryer_drying_target"; + public static final String CHANNEL_COFFEEMAKER_DRIP_TRAY_FULL_STATE = "coffeemaker_drip_tray_full_state"; + public static final String CHANNEL_COFFEEMAKER_WATER_TANK_EMPTY_STATE = "coffeemaker_water_tank_empty_state"; + public static final String CHANNEL_COFFEEMAKER_BEAN_CONTAINER_EMPTY_STATE = "coffeemaker_bean_container_empty_state"; + public static final String CHANNEL_HOOD_VENTING_LEVEL = "hood_venting_level"; + public static final String CHANNEL_HOOD_INTENSIVE_LEVEL = "hood_intensive_level"; + public static final String CHANNEL_HOOD_ACTIONS_STATE = "hood_program_state"; + public static final String CHANNEL_FUNCTIONAL_LIGHT_STATE = "functional_light_state"; + public static final String CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE = "functional_light_brightness_state"; + public static final String CHANNEL_AMBIENT_LIGHT_STATE = "ambient_light_state"; + public static final String CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE = "ambient_light_brightness_state"; + public static final String CHANNEL_AMBIENT_LIGHT_COLOR_STATE = "ambient_light_color_state"; + public static final String CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE = "ambient_light_custom_color_state"; + + // List of all supported devices + public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_API_BRIDGE, + THING_TYPE_DISHWASHER, THING_TYPE_OVEN, THING_TYPE_WASHER, THING_TYPE_DRYER, THING_TYPE_WASHER_DRYER, + THING_TYPE_FRIDGE_FREEZER, THING_TYPE_COFFEE_MAKER, THING_TYPE_HOOD, THING_TYPE_COOKTOP); + + // Discoverable devices + public static final Set DISCOVERABLE_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_DISHWASHER, + THING_TYPE_OVEN, THING_TYPE_WASHER, THING_TYPE_DRYER, THING_TYPE_WASHER_DRYER, THING_TYPE_FRIDGE_FREEZER, + THING_TYPE_COFFEE_MAKER, THING_TYPE_HOOD, THING_TYPE_COOKTOP); + + // List of state values + public static final String STATE_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"; + public static final String STATE_POWER_ON = "BSH.Common.EnumType.PowerState.On"; + public static final String STATE_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"; + public static final String STATE_DOOR_OPEN = "BSH.Common.EnumType.DoorState.Open"; + public static final String STATE_DOOR_LOCKED = "BSH.Common.EnumType.DoorState.Locked"; + public static final String STATE_DOOR_CLOSED = "BSH.Common.EnumType.DoorState.Closed"; + public static final String STATE_OPERATION_READY = "BSH.Common.EnumType.OperationState.Ready"; + public static final String STATE_OPERATION_FINISHED = "BSH.Common.EnumType.OperationState.Finished"; + public static final String STATE_OPERATION_RUN = "BSH.Common.EnumType.OperationState.Run"; + public static final String STATE_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off"; + + // List of program options + public static final String OPTION_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime"; + public static final String OPTION_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress"; + public static final String OPTION_ELAPSED_PROGRAM_TIME = "BSH.Common.Option.ElapsedProgramTime"; + public static final String OPTION_SETPOINT_TEMPERATURE = "Cooking.Oven.Option.SetpointTemperature"; + public static final String OPTION_DURATION = "BSH.Common.Option.Duration"; + public static final String OPTION_WASHER_TEMPERATURE = "LaundryCare.Washer.Option.Temperature"; + public static final String OPTION_WASHER_SPIN_SPEED = "LaundryCare.Washer.Option.SpinSpeed"; + public static final String OPTION_WASHER_IDOS_1_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos1DosingLevel"; + public static final String OPTION_WASHER_IDOS_2_DOSING_LEVEL = "LaundryCare.Washer.Option.IDos2DosingLevel"; + public static final String OPTION_DRYER_DRYING_TARGET = "LaundryCare.Dryer.Option.DryingTarget"; + public static final String OPTION_HOOD_VENTING_LEVEL = "Cooking.Common.Option.Hood.VentingLevel"; + public static final String OPTION_HOOD_INTENSIVE_LEVEL = "Cooking.Common.Option.Hood.IntensiveLevel"; + + // List of stages + public static final String STAGE_FAN_OFF = "Cooking.Hood.EnumType.Stage.FanOff"; + public static final String STAGE_FAN_STAGE_01 = "Cooking.Hood.EnumType.Stage.FanStage01"; + public static final String STAGE_FAN_STAGE_02 = "Cooking.Hood.EnumType.Stage.FanStage02"; + public static final String STAGE_FAN_STAGE_03 = "Cooking.Hood.EnumType.Stage.FanStage03"; + public static final String STAGE_FAN_STAGE_04 = "Cooking.Hood.EnumType.Stage.FanStage04"; + public static final String STAGE_FAN_STAGE_05 = "Cooking.Hood.EnumType.Stage.FanStage05"; + public static final String STAGE_INTENSIVE_STAGE_OFF = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStageOff"; + public static final String STAGE_INTENSIVE_STAGE_1 = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1"; + public static final String STAGE_INTENSIVE_STAGE_2 = "Cooking.Hood.EnumType.IntensiveStage.IntensiveStage2"; + public static final String STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = "BSH.Common.EnumType.AmbientLightColor.CustomColor"; + + // List of programs + public static final String PROGRAM_HOOD_AUTOMATIC = "Cooking.Common.Program.Hood.Automatic"; + public static final String PROGRAM_HOOD_VENTING = "Cooking.Common.Program.Hood.Venting"; + public static final String PROGRAM_HOOD_DELAYED_SHUT_OFF = "Cooking.Common.Program.Hood.DelayedShutOff"; + + // Network and oAuth constants + public static final String API_BASE_URL = "https://api.home-connect.com"; + public static final String API_SIMULATOR_BASE_URL = "https://simulator.home-connect.com"; + public static final String OAUTH_TOKEN_PATH = "/security/oauth/token"; + public static final String OAUTH_AUTHORIZE_PATH = "/security/oauth/authorize"; + public static final String OAUTH_SCOPE = "IdentifyAppliance Monitor Settings Dishwasher-Control Washer-Control Dryer-Control WasherDryer-Control CoffeeMaker-Control Hood-Control CleaningRobot-Control"; + + // Operation states + public static final String OPERATION_STATE_INACTIVE = "BSH.Common.EnumType.OperationState.Inactive"; + public static final String OPERATION_STATE_READY = "BSH.Common.EnumType.OperationState.Ready"; + public static final String OPERATION_STATE_DELAYED_START = "BSH.Common.EnumType.OperationState.DelayedStart"; + public static final String OPERATION_STATE_RUN = "BSH.Common.EnumType.OperationState.Run"; + public static final String OPERATION_STATE_PAUSE = "BSH.Common.EnumType.OperationState.Pause"; + public static final String OPERATION_STATE_ACTION_REQUIRED = "BSH.Common.EnumType.OperationState.ActionRequired"; + public static final String OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished"; + public static final String OPERATION_STATE_ERROR = "BSH.Common.EnumType.OperationState.Error"; + public static final String OPERATION_STATE_ABORTING = "BSH.Common.EnumType.OperationState.Aborting"; + + // Commands + public static final String COMMAND_START = "start"; + public static final String COMMAND_STOP = "stop"; + public static final String COMMAND_SELECTED = "selected"; + public static final String COMMAND_VENTING_1 = "venting1"; + public static final String COMMAND_VENTING_2 = "venting2"; + public static final String COMMAND_VENTING_3 = "venting3"; + public static final String COMMAND_VENTING_4 = "venting4"; + public static final String COMMAND_VENTING_5 = "venting5"; + public static final String COMMAND_VENTING_INTENSIVE_1 = "ventingIntensive1"; + public static final String COMMAND_VENTING_INTENSIVE_2 = "ventingIntensive2"; + public static final String COMMAND_AUTOMATIC = "automatic"; + public static final String COMMAND_DELAYED_SHUT_OFF = "delayed"; + + // light + public static final int BRIGHTNESS_MIN = 10; + public static final int BRIGHTNESS_MAX = 100; + public static final int BRIGHTNESS_DIM_STEP = 10; +} diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/CircularQueue.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/CircularQueue.java new file mode 100644 index 0000000000..fada981c79 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/CircularQueue.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.homeconnect.internal.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ArrayBlockingQueue; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * FIFO queue (ring buffer implementation). + * + * @author Jonas Brüstel - Initial contribution + * + */ +@NonNullByDefault +public class CircularQueue { + + private final ArrayBlockingQueue queue; + + public CircularQueue(final int capacity) { + queue = new ArrayBlockingQueue<>(capacity); + } + + public synchronized void add(E element) { + ArrayBlockingQueue myQueue = queue; + if (myQueue.remainingCapacity() <= 0) { + myQueue.poll(); + } + myQueue.add(element); + } + + public synchronized void addAll(Collection collection) { + collection.forEach(this::add); + } + + public synchronized Collection getAll() { + return new ArrayList<>(queue); + } +} diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java new file mode 100644 index 0000000000..4ea338e7b7 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java @@ -0,0 +1,1102 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.homeconnect.internal.client; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*; +import static org.openhab.binding.homeconnect.internal.client.HttpHelper.formatJsonBody; +import static org.openhab.binding.homeconnect.internal.client.HttpHelper.getAuthorizationHeader; +import static org.openhab.binding.homeconnect.internal.client.HttpHelper.parseString; +import static org.openhab.binding.homeconnect.internal.client.HttpHelper.sendRequest; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.core.HttpHeaders; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException; +import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException; +import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException; +import org.openhab.binding.homeconnect.internal.client.model.ApiRequest; +import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram; +import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption; +import org.openhab.binding.homeconnect.internal.client.model.Data; +import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance; +import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest; +import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse; +import org.openhab.binding.homeconnect.internal.client.model.Option; +import org.openhab.binding.homeconnect.internal.client.model.Program; +import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * Client for Home Connect API. + * + * @author Jonas Brüstel - Initial contribution + * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework + * + */ +@NonNullByDefault +public class HomeConnectApiClient { + private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json"; + private static final String BASE = "/api/homeappliances"; + private static final String BASE_PATH = BASE + "/"; + private static final int REQUEST_TIMEOUT_SEC = 30; + private static final int VALUE_TYPE_STRING = 0; + private static final int VALUE_TYPE_INT = 1; + private static final int VALUE_TYPE_BOOLEAN = 2; + private static final int COMMUNICATION_QUEUE_SIZE = 50; + + private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class); + private final HttpClient client; + private final String apiUrl; + private final Map> availableProgramOptionsCache; + private final OAuthClientService oAuthClientService; + private final CircularQueue communicationQueue; + private final ApiBridgeConfiguration apiBridgeConfiguration; + + public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated, + @Nullable List apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) { + this.client = httpClient; + this.oAuthClientService = oAuthClientService; + this.apiBridgeConfiguration = apiBridgeConfiguration; + + availableProgramOptionsCache = new ConcurrentHashMap<>(); + apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL; + communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE); + if (apiRequestHistory != null) { + communicationQueue.addAll(apiRequestHistory); + } + } + + /** + * Get all home appliances + * + * @return list of {@link HomeAppliance} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + */ + public List getHomeAppliances() throws CommunicationException, AuthorizationException { + Request request = createRequest(HttpMethod.GET, BASE); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, null, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(null, request, null, response, responseBody); + + return mapToHomeAppliances(responseBody); + } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) { + logger.warn("Failed to fetch home appliances! error={}", e.getMessage()); + trackAndLogApiRequest(null, request, null, null, null); + throw new CommunicationException(e); + } + } + + /** + * Get home appliance by id + * + * @param haId home appliance id + * @return {@link HomeAppliance} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + */ + public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException { + Request request = createRequest(HttpMethod.GET, BASE_PATH + haId); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + return mapToHomeAppliance(responseBody); + } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) { + logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + } + + /** + * Get ambient light state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getAmbientLightState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED); + } + + /** + * Set ambient light state of device. + * + * @param haId home appliance id + * @param enable enable or disable ambient light + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setAmbientLightState(String haId, boolean enable) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN); + } + + /** + * Get functional light state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFunctionalLightState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_LIGHTING); + } + + /** + * Set functional light state of device. + * + * @param haId home appliance id + * @param enable enable or disable functional light + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFunctionalLightState(String haId, boolean enable) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN); + } + + /** + * Get functional light brightness state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFunctionalLightBrightnessState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS); + } + + /** + * Set functional light brightness of device. + * + * @param haId home appliance id + * @param value brightness value 10-100 + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFunctionalLightBrightnessState(String haId, int value) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT); + } + + /** + * Get ambient light brightness state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getAmbientLightBrightnessState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS); + } + + /** + * Set ambient light brightness of device. + * + * @param haId home appliance id + * @param value brightness value 10-100 + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setAmbientLightBrightnessState(String haId, int value) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT); + } + + /** + * Get ambient light color state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getAmbientLightColorState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR); + } + + /** + * Set ambient light color of device. + * + * @param haId home appliance id + * @param value color code + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setAmbientLightColorState(String haId, String value) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null)); + } + + /** + * Get ambient light custom color state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getAmbientLightCustomColorState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR); + } + + /** + * Set ambient light color of device. + * + * @param haId home appliance id + * @param value color code + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setAmbientLightCustomColorState(String haId, String value) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null)); + } + + /** + * Get power state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getPowerState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_POWER_STATE); + } + + /** + * Set power state of device. + * + * @param haId home appliance id + * @param state target state + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setPowerState(String haId, String state) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_POWER_STATE, state, null)); + } + + /** + * Get setpoint temperature of freezer + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFreezerSetpointTemperature(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE); + } + + /** + * Set setpoint temperature of freezer + * + * @param haId home appliance id + * @param state new temperature + * @param unit temperature unit + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFreezerSetpointTemperature(String haId, String state, String unit) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT); + } + + /** + * Get setpoint temperature of fridge + * + * @param haId home appliance id + * @return {@link Data} or null in case of communication error + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFridgeSetpointTemperature(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE); + } + + /** + * Set setpoint temperature of fridge + * + * @param haId home appliance id + * @param state new temperature + * @param unit temperature unit + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFridgeSetpointTemperature(String haId, String state, String unit) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT); + } + + /** + * Get fridge super mode + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFridgeSuperMode(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE); + } + + /** + * Set fridge super mode + * + * @param haId home appliance id + * @param enable enable or disable fridge super mode + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFridgeSuperMode(String haId, boolean enable) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN); + } + + /** + * Get freezer super mode + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getFreezerSuperMode(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getSetting(haId, SETTING_FREEZER_SUPER_MODE); + } + + /** + * Set freezer super mode + * + * @param haId home appliance id + * @param enable enable or disable freezer super mode + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public void setFreezerSuperMode(String haId, boolean enable) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN); + } + + /** + * Get door state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getDoorState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getStatus(haId, STATUS_DOOR_STATE); + } + + /** + * Get operation state of device. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getOperationState(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getStatus(haId, STATUS_OPERATION_STATE); + } + + /** + * Get current cavity temperature of oven. + * + * @param haId home appliance id + * @return {@link Data} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public Data getCurrentCavityTemperature(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE); + } + + /** + * Is remote start allowed? + * + * @param haId haId home appliance id + * @return true or false + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public boolean isRemoteControlStartAllowed(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED); + return Boolean.parseBoolean(data.getValue()); + } + + /** + * Is remote control allowed? + * + * @param haId haId home appliance id + * @return true or false + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public boolean isRemoteControlActive(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE); + return Boolean.parseBoolean(data.getValue()); + } + + /** + * Is local control allowed? + * + * @param haId haId home appliance id + * @return true or false + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public boolean isLocalControlActive(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE); + return Boolean.parseBoolean(data.getValue()); + } + + /** + * Get active program of device. + * + * @param haId home appliance id + * @return {@link Data} or null if there is no active program + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public @Nullable Program getActiveProgram(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getProgram(haId, BASE_PATH + haId + "/programs/active"); + } + + /** + * Get selected program of device. + * + * @param haId home appliance id + * @return {@link Data} or null if there is no selected program + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public @Nullable Program getSelectedProgram(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getProgram(haId, BASE_PATH + haId + "/programs/selected"); + } + + public void setSelectedProgram(String haId, String program) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING); + } + + public void startProgram(String haId, String program) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING); + } + + public void startSelectedProgram(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected"); + if (selectedProgram != null) { + putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram); + } + } + + public void startCustomProgram(String haId, String json) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putRaw(haId, BASE_PATH + haId + "/programs/active", json); + } + + public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt, + boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException { + String programState = isProgramActive ? "active" : "selected"; + + putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit), + valueAsInt); + } + + public void stopProgram(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + sendDelete(haId, BASE_PATH + haId + "/programs/active"); + } + + public List getPrograms(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getAvailablePrograms(haId, BASE_PATH + haId + "/programs"); + } + + public List getAvailablePrograms(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available"); + } + + public List getProgramOptions(String haId, String programKey) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + if (availableProgramOptionsCache.containsKey(programKey)) { + logger.debug("Returning cached options for '{}'.", programKey); + List availableProgramOptions = availableProgramOptionsCache.get(programKey); + return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList(); + } + + Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + List availableProgramOptions = mapToAvailableProgramOption(responseBody, haId); + availableProgramOptionsCache.put(programKey, availableProgramOptions); + return availableProgramOptions; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey, + e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + } + + /** + * Get latest API requests. + * + * @return communication queue + */ + public Collection getLatestApiRequests() { + return communicationQueue.getAll(); + } + + private Data getSetting(String haId, String setting) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getData(haId, BASE_PATH + haId + "/settings/" + setting); + } + + private void putSettings(String haId, Data data) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putSettings(haId, data, VALUE_TYPE_STRING); + } + + private void putSettings(String haId, Data data, int valueType) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType); + } + + private Data getStatus(String haId, String status) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getData(haId, BASE_PATH + haId + "/status/" + status); + } + + public @Nullable String getRaw(String haId, String path) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + return getRaw(haId, path, false); + } + + public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.GET, path); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) { + return responseBody; + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + return null; + } + + public String putRaw(String haId, String path, String requestBodyPayload) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload), + BSH_JSON_V1); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody); + return responseBody; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload, + e.getMessage()); + trackAndLogApiRequest(haId, request, requestBodyPayload, null, null); + throw new CommunicationException(e); + } + } + + private @Nullable Program getProgram(String haId, String path) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.GET, path); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(asList(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + if (response.getStatus() == HttpStatus.OK_200) { + return mapToProgram(responseBody); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + return null; + } + + private List getAvailablePrograms(String haId, String path) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.GET, path); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + return mapToAvailablePrograms(responseBody, haId); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + } + + private void sendDelete(String haId, String path) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.DELETE, path); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null); + + trackAndLogApiRequest(haId, request, null, response, response.getContentAsString()); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + } + + private Data getData(String haId, String path) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + Request request = createRequest(HttpMethod.GET, path); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, null, response, responseBody); + + return mapToState(responseBody); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage()); + trackAndLogApiRequest(haId, request, null, null, null); + throw new CommunicationException(e); + } + } + + private void putData(String haId, String path, Data data, int valueType) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + JsonObject innerObject = new JsonObject(); + innerObject.addProperty("key", data.getName()); + + if (data.getValue() != null) { + if (valueType == VALUE_TYPE_INT) { + innerObject.addProperty("value", data.getValueAsInt()); + } else if (valueType == VALUE_TYPE_BOOLEAN) { + innerObject.addProperty("value", data.getValueAsBoolean()); + } else { + innerObject.addProperty("value", data.getValue()); + } + } + + if (data.getUnit() != null) { + innerObject.addProperty("unit", data.getUnit()); + } + + JsonObject dataObject = new JsonObject(); + dataObject.add("data", innerObject); + String requestBodyPayload = dataObject.toString(); + + Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload), + BSH_JSON_V1); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload); + + trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString()); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data, + valueType, e.getMessage()); + trackAndLogApiRequest(haId, request, requestBodyPayload, null, null); + throw new CommunicationException(e); + } + } + + private void putOption(String haId, String path, Option option, boolean asInt) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + JsonObject innerObject = new JsonObject(); + innerObject.addProperty("key", option.getKey()); + + if (option.getValue() != null) { + if (asInt) { + innerObject.addProperty("value", option.getValueAsInt()); + } else { + innerObject.addProperty("value", option.getValue()); + } + } + + if (option.getUnit() != null) { + innerObject.addProperty("unit", option.getUnit()); + } + + JsonArray optionsArray = new JsonArray(); + optionsArray.add(innerObject); + + JsonObject optionsObject = new JsonObject(); + optionsObject.add("options", optionsArray); + + JsonObject dataObject = new JsonObject(); + dataObject.add("data", optionsObject); + + String requestBodyPayload = dataObject.toString(); + + Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload), + BSH_JSON_V1); + try { + ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); + checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload); + + trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString()); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option, + asInt, e.getMessage()); + trackAndLogApiRequest(haId, request, requestBodyPayload, null, null); + throw new CommunicationException(e); + } + } + + private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId, + @Nullable String requestPayload) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload); + } + + private void checkResponseCode(List desiredCodes, Request request, ContentResponse response, + @Nullable String haId, @Nullable String requestPayload) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401) + && response.getStatus() == HttpStatus.UNAUTHORIZED_401) { + logger.debug("Current access token is invalid."); + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, requestPayload, response, responseBody); + throw new AuthorizationException("Token invalid!"); + } + + if (!desiredCodes.contains(response.getStatus())) { + int code = response.getStatus(); + String message = response.getReason(); + + logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes); + String responseBody = response.getContentAsString(); + trackAndLogApiRequest(haId, request, requestPayload, response, responseBody); + + responseBody = responseBody == null ? "" : responseBody; + if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error") + && responseBody.toLowerCase().contains("offline")) { + throw new ApplianceOfflineException(code, message, responseBody); + } else { + throw new CommunicationException(code, message, responseBody); + } + } + } + + private Program mapToProgram(String json) { + ArrayList