mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[homeconnectdirect] Home Connect Direct binding initial contribution (#20653)
* [homeconnectdirect] Home Connect Direct Binding initial contribution Signed-off-by: Jonas Brüstel <openhab@bruestel.net>
This commit is contained in:
@@ -168,6 +168,7 @@
|
||||
/bundles/org.openhab.binding.herzborg/ @Sonic-Amiga
|
||||
/bundles/org.openhab.binding.homeassistant/ @ccutrer
|
||||
/bundles/org.openhab.binding.homeconnect/ @bruestel
|
||||
/bundles/org.openhab.binding.homeconnectdirect/ @bruestel
|
||||
/bundles/org.openhab.binding.homekit/ @andrewfg
|
||||
/bundles/org.openhab.binding.homie/ @ccutrer
|
||||
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
|
||||
|
||||
@@ -826,6 +826,11 @@
|
||||
<artifactId>org.openhab.binding.homeconnect</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.homeconnectdirect</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.homekit</artifactId>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
|
||||
== Third-party Content
|
||||
|
||||
conscrypt
|
||||
* License: Apache License 2.0
|
||||
* Project: https://www.conscrypt.org/
|
||||
* Source: https://github.com/google/conscrypt
|
||||
|
||||
pico
|
||||
* License: MIT License
|
||||
* Project: https://picocss.com/
|
||||
* Source: https://github.com/picocss/pico
|
||||
|
||||
Alpine.js
|
||||
* License: MIT License
|
||||
* Project: https://alpinejs.dev/
|
||||
* Source: https://github.com/alpinejs/alpine
|
||||
|
||||
heroicons
|
||||
* License: MIT License
|
||||
* Project: https://heroicons.com/
|
||||
* Source: https://github.com/tailwindlabs/heroicons
|
||||
@@ -0,0 +1,324 @@
|
||||
# Home Connect Direct Binding
|
||||
|
||||
The Home Connect Direct Binding integrates [Home Connect](https://www.home-connect.com/) enabled devices into openHAB via your local network.
|
||||
Unlike the standard Home Connect binding, this implementation communicates directly with your appliances, ensuring reliable, low-latency operation without requiring cloud access.
|
||||
|
||||
## Supported Appliances
|
||||
|
||||
The following appliance types are supported. Appliance types marked with an asterisk (*) are currently in a **beta stage** and have not yet been extensively tested.
|
||||
|
||||
| Appliance Type | Thing Type ID |
|
||||
|---------------------------------|-----------------|
|
||||
| Dishwasher | `dishwasher` |
|
||||
| Cook Processor (Cookit) | `cookprocessor` |
|
||||
| Washer | `washer` |
|
||||
| Washer / Dryer Combination | `washerdryer` |
|
||||
| Dryer | `dryer` |
|
||||
| Oven | `oven` |
|
||||
| Warming Drawer* | `warmingdrawer` |
|
||||
| Coffee Machine | `coffeemaker` |
|
||||
| Hood* | `hood` |
|
||||
| Cooktop (Hob)* | `cooktop` |
|
||||
| Fridge / Freezer* | `fridgefreezer` |
|
||||
| Generic (other types) | `generic` |
|
||||
|
||||
## Prerequisites & Initial Setup
|
||||
|
||||
Before adding things to openHAB, you must load the **Appliance Profiles**. These profiles contain the technical descriptions (keys, enumeration values, etc.) required for the binding to communicate with your specific hardware.
|
||||
|
||||
### Load Appliance Profiles
|
||||
|
||||
The binding includes a dedicated web console for profile management.
|
||||
|
||||
1. **Access the Console**: Open your browser and navigate to:
|
||||
`http(s)://[YOUR_OPENHAB_IP]:[PORT]/homeconnectdirect`
|
||||
_(e.g., `http://192.168.178.100:8080/homeconnectdirect`)_
|
||||
1. **Go to Appliance Profiles**: Open the **"Profiles"** menu item.
|
||||
1. **Upload Profiles**: Click the **`Upload Profile`** button in the top-right corner.
|
||||
|
||||
> **IMPORTANT:**
|
||||
> The binding cannot fetch profiles automatically from the BSH cloud. You must use an external tool to download your profiles first.
|
||||
>
|
||||
> **Recommended Tool**: [HomeConnect Profile Downloader](https://github.com/bruestel/homeconnect-profile-downloader)
|
||||
> Download the latest version from the [Releases page](https://github.com/bruestel/homeconnect-profile-downloader/releases).
|
||||
|
||||

|
||||
|
||||
## Discovery & Configuration
|
||||
|
||||
### Creating Things
|
||||
|
||||
Once profiles are uploaded, you can add your appliances:
|
||||
|
||||
- **Automatic Discovery**: The binding uses **mDNS** to scan your local network. Discovered devices will appear in the openHAB Inbox automatically.
|
||||
- **Manual Configuration**: You can manually create things via the UI or `.things` files using the parameters below.
|
||||
|
||||
#### Thing Configuration Parameters
|
||||
|
||||
| Parameter | Type | Description | Required |
|
||||
|------------------------|---------|--------------------------------------------------------------------------|----------|
|
||||
| `haId` | Text | The unique Home Appliance ID (found in the Home Connect Direct console). | Yes |
|
||||
| `address` | Text | The local IP address or hostname of the appliance. | Yes |
|
||||
| `connectionRetryDelay` | Integer | Delay in minutes before attempting a reconnect (1-60, default: 1). | No |
|
||||
|
||||
## Channels
|
||||
|
||||
The binding offers two types of channels: **Preconfigured** (standard functions) and **Custom** (custom attributes).
|
||||
|
||||
### Preconfigured Channels
|
||||
|
||||
These channels provide immediate access to common functions. Available channels vary by appliance type. Some channels are added dynamically based on the features supported by your specific appliance model (e.g., different heating modes for an oven).
|
||||
|
||||
#### General Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description | Supported Appliance Types |
|
||||
|--------------------------------|----------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
|
||||
| `power-state` | Switch | R/W | Controls and monitors the appliance power state. | Dishwasher, Cook Processor, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer, Hood, Cooktop |
|
||||
| `door` | Contact | R | Indicates if the door is Open or Closed. | Dishwasher, Washer, Washer/Dryer, Dryer |
|
||||
| `operation-state` | String | R | Current state (e.g., Run, Ready, Finished). | Dishwasher, Cook Processor, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer, Hood, Cooktop |
|
||||
| `remote-control-start-allowed` | Switch | R | Indicates if remote operation is enabled. | Dishwasher, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer, Hood |
|
||||
| `child-lock` | Switch | R/W | The child lock state. | Cook Processor, Washer, Washer/Dryer, Dryer, Oven, Cooktop |
|
||||
| `active-program` | String | R | The program currently running. Changes to UNDEF when no program is active (e.g., when paused or idle). | Dishwasher, Cook Processor, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer, Hood, Cooktop |
|
||||
| `selected-program` | String | R/W | The program currently selected on the device. | Dishwasher, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer |
|
||||
| `remaining-program-time` | Number:Time | R | Estimated time remaining. | Dishwasher, Washer, Washer/Dryer, Dryer, Oven, Warming Drawer |
|
||||
| `program-progress` | Number:Dimensionless | R | Progress in percent (0-100%). | Dishwasher, Cook Processor, Washer, Washer/Dryer, Dryer, Coffee Maker, Oven, Warming Drawer |
|
||||
| `program-command` | String | W | Send commands like `start`, `pause`, or `resume`. | Dishwasher, Cook Processor, Washer, Washer/Dryer, Dryer |
|
||||
| `command` | String | W | Send specific operation commands to the appliance. | All |
|
||||
| `raw-message` | String | W | Advanced: Send raw JSON payloads.<br>Example (Start coffee program): `{"action": "POST", "resource": "/ro/activeProgram", "data": [{"program": 8217}]}` | All |
|
||||
|
||||
#### Dishwasher Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|-------------------------------|-----------|--------|----------------------------------------------------------|
|
||||
| `salt-lack` | Switch | R | Indicates if dishwasher salt is empty. |
|
||||
| `rinse-aid-lack` | Switch | R | Indicates if dishwasher rinse aid is empty. |
|
||||
| `salt-nearly-empty` | Switch | R | Indicates when the dishwasher salt is almost empty. |
|
||||
| `rinse-aid-nearly-empty` | Switch | R | Indicates when the dishwasher rinse aid is almost empty. |
|
||||
| `machine-care-reminder` | Switch | R | Indicates whether the dishwasher needs cleaning. |
|
||||
| `program-phase` | String | R | The program or process phase of the dishwasher. |
|
||||
| `dishwasher-vario-speed-plus` | Switch | R/W | High-speed cleaning option. |
|
||||
| `dishwasher-intensiv-zone` | Switch | R/W | Increases spray pressure in the lower basket. |
|
||||
| `dishwasher-brilliance-dry` | Switch | R/W | Enhances the drying process for glassware. |
|
||||
|
||||
#### Washer, Dryer & Washer/Dryer Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|-------------------------------|-------------|--------|-------------------------------------------------------------------------|
|
||||
| `drum-clean-reminder` | Switch | R | Indicates whether the drum needs cleaning. |
|
||||
| `laundry-load-information` | Number:Mass | R | The current laundry load information. |
|
||||
| `laundry-load-recommendation` | Number:Mass | R | The recommended load for the current program. |
|
||||
| `process-phase` | String | R | The current process phase of the appliance. |
|
||||
| `washer-temperature` | String | R/W | The temperature of the washing program. |
|
||||
| `washer-spin-speed` | String | R/W | The spin speed of the washing program. |
|
||||
| `washer-speed-perfect` | Switch | R/W | Reduces the program duration. |
|
||||
| `washer-water-plus` | Switch | R/W | Increases the water level. |
|
||||
| `washer-prewash` | Switch | R/W | Adds a prewash cycle. |
|
||||
| `washer-rinse-hold` | Switch | R/W | Stops the cycle before the final spin. |
|
||||
| `washer-less-ironing` | Switch | R/W | Reduces creasing. |
|
||||
| `washer-silent-wash` | Switch | R/W | Reduces noise during operation. |
|
||||
| `washer-soak` | Switch | R/W | Adds an immersion phase for stubborn dirt. |
|
||||
| `washer-rinse-plus` | String | R/W | Adds additional rinse cycles. |
|
||||
| `washer-stains` | String | R/W | Optimizes the cycle for specific stain types. |
|
||||
| `drying-target` | String | R/W | Specifies the desired dryness setting (Dryer, Washer/Dryer). |
|
||||
| `wrinkle-guard` | String | R/W | Prevents laundry from creasing after the cycle (Dryer, Washer/Dryer). |
|
||||
| `idos1-active` | Switch | R/W | Whether i-Dos 1 is enabled for the washing program (dynamically added). |
|
||||
| `idos2-active` | Switch | R/W | Whether i-Dos 2 is enabled for the washing program (dynamically added). |
|
||||
| `idos1-fill-level-poor` | Switch | R | Indicates whether i-Dos 1 is almost empty (dynamically added). |
|
||||
| `idos2-fill-level-poor` | Switch | R | Indicates whether i-Dos 2 is almost empty (dynamically added). |
|
||||
|
||||
#### Oven & Warming Drawer Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|------------------------------|--------------------|--------|-------------------------------------------------------------------------------------------------------|
|
||||
| `oven-program-command` | String | W | Controls program execution (`start`, `pause`, `resume`, `stop`). |
|
||||
| `duration` | Number:Time | R/W | The duration of the program (Oven). |
|
||||
| `setpoint-temperature` | Number:Temperature | R/W | Target temperature. |
|
||||
| `temperature-{n}` | Number:Temperature | R | The current cavity temperature (dynamically added, where {n} is the cavity number). |
|
||||
| `cavity-light-{n}` | Switch | R/W | The cavity light state (dynamically added, where {n} is the cavity number). |
|
||||
| `door-{n}` | Contact | R | Indicates if the door is Open or Closed (dynamically added, where {n} is the cavity number). |
|
||||
| `meat-probe-temperature-{n}` | Number:Temperature | R | The current meat probe temperature (dynamically added, where {n} is the cavity number). |
|
||||
| `meat-probe-plugged-{n}` | Switch | R | Indicates if the meat probe sensor is plugged in (dynamically added, where {n} is the cavity number). |
|
||||
|
||||
#### Coffee Maker Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|-------------------------------|-----------|--------|------------------------------------------------|
|
||||
| `cleaning` | Number | R | Countdown until cleaning is due. |
|
||||
| `calc-n-clean` | Number | R | Countdown until calc 'n' clean is due. |
|
||||
| `descaling` | Number | R | Countdown until descaling is due. |
|
||||
| `water-filter` | Number | R | Countdown for water filter replacement. |
|
||||
| `water-tank-empty` | Switch | R | Indicates when the water tank is empty. |
|
||||
| `water-tank-nearly-empty` | Switch | R | Indicates when the water tank is almost empty. |
|
||||
| `drip-tray-full` | Switch | R | Indicates when the drip tray is full. |
|
||||
| `empty-milk-tank` | Switch | R | Indicates when the milk tank is empty. |
|
||||
| `bean-container-empty` | Switch | R | Indicates when the bean container is empty. |
|
||||
| `process-phase` | String | R | The current process phase of the coffee maker. |
|
||||
| `coffeemaker-program-command` | String | W | Controls program execution (`start`, `stop`). |
|
||||
|
||||
#### Hood Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|----------------------------|-----------|--------|-----------------------------------------|
|
||||
| `cooking-light` | Switch | R/W | The functional light state. |
|
||||
| `cooking-light-brightness` | Number | R/W | The brightness of the functional light. |
|
||||
| `button-tones` | Switch | R/W | The button tones setting. |
|
||||
| `venting-level` | String | R/W | Venting level of the hood. |
|
||||
| `intensive-level` | String | R/W | Intensive venting level of the hood. |
|
||||
|
||||
#### Cooktop Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|----------------|-----------|--------|---------------------------|
|
||||
| `button-tones` | Switch | R/W | The button tones setting. |
|
||||
|
||||
#### Fridge / Freezer Channels
|
||||
|
||||
| Channel ID | Item Type | Access | Description |
|
||||
|-------------------------------------|----------------------|--------|--------------------------------------------------------------------------------------------------|
|
||||
| `refrigerator-door` | Contact | R | Indicates if the refrigerator door is Open or Closed (dynamically added). |
|
||||
| `freezer-door` | Contact | R | Indicates if the freezer door is Open or Closed (dynamically added). |
|
||||
| `door` | Contact | R | Indicates if the door is Open or Closed (dynamically added - fallback if specific doors absent). |
|
||||
| `setpoint-temperature-refrigerator` | Number:Temperature | R/W | Target temperature of the refrigerator compartment (dynamically added). |
|
||||
| `setpoint-temperature-freezer` | Number:Temperature | R/W | Target temperature of the freezer compartment (dynamically added). |
|
||||
| `setpoint-temperature-chiller` | Number:Temperature | R/W | Target temperature of the chiller compartment (dynamically added). |
|
||||
| `super-mode-refrigerator` | Switch | R/W | Enables Super Cooling mode for the refrigerator (dynamically added). |
|
||||
| `super-mode-freezer` | Switch | R/W | Enables Super Freezing mode for the freezer (dynamically added). |
|
||||
| `dispenser-enabled` | Switch | R/W | Enables or disables the dispenser (dynamically added). |
|
||||
| `dispenser-party-mode` | Switch | R/W | Enables or disables the dispenser party mode (dynamically added). |
|
||||
| `dispenser-water-filter-saturation` | Number:Dimensionless | R | Water filter saturation level of the dispenser in percent (dynamically added). |
|
||||
| `chiller-preset` | String | R/W | Allows selecting a predefined cooling mode for the chiller compartment (dynamically added). |
|
||||
|
||||
### Custom Channels
|
||||
|
||||
Custom channels allow you to monitor **any** internal value or device description attribute available in the appliance profile. This is useful for advanced scenarios or accessing functions not covered by standard channels.
|
||||
|
||||
The binding's web console includes an **openHAB Configuration Generator** to help create the necessary YAML or DSL for these channels. You can access it by monitoring an appliance and clicking the openHAB icon next to any value or description key.
|
||||
|
||||
#### 1. Value Channels
|
||||
|
||||
These channels reflect the **current actual values** of the home appliance. They answer the question: _"What is the current state?"_
|
||||
|
||||
Examples: "How hot is the oven?", "Is the door open?", "Which program is running?"
|
||||
|
||||
| Channel Type ID | Item Type | Config Parameters | Description |
|
||||
|-----------------|-----------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `switch` | Switch | `valueKey` | For boolean values (true/false). |
|
||||
| `string` | String | `valueKey` | For text or enumeration values. |
|
||||
| `number` | Number | `valueKey`, `unit` | For numeric values. `unit` is optional (e.g., "°C", "%"). |
|
||||
| `trigger` | — | `valueKey` | Trigger channel that fires events (e.g., for event keys). |
|
||||
| `enum-switch` | Switch | `valueKey`, `onValue` | Read-only switch that maps enumeration values to ON/OFF. `onValue` is a comma-separated list of enum value keys that map to ON (case-insensitive). |
|
||||
|
||||
#### 2. Device Description Channels
|
||||
|
||||
These channels describe the **capabilities and constraints** of the appliance. They answer the question: _"What is currently possible?"_
|
||||
|
||||
Home Connect appliances are dynamic; allowed ranges or available options change based on the selected program or operation state.
|
||||
Examples: "What is the maximum allowed temperature for the current program?", "Can I currently change the power state (Read/Write) or is it locked (Read-Only)?", "Is a specific option available right now?"
|
||||
|
||||
| Channel Type ID | Item Type | Config Parameters | Description |
|
||||
|-----------------------------|-----------|-------------------------------|---------------------------------------------------------|
|
||||
| `device-description-switch` | Switch | `descriptionKey`, `attribute` | For boolean attributes (e.g. `available`). |
|
||||
| `device-description-string` | String | `descriptionKey`, `attribute` | For text attributes (e.g. `access`, `enumerationType`). |
|
||||
| `device-description-number` | Number | `descriptionKey`, `attribute` | For numeric attributes (e.g. `min`, `max`, `stepSize`). |
|
||||
|
||||
**Supported Attributes:**
|
||||
|
||||
- `access`: Current access rights (read/write).
|
||||
- `available`: Whether the feature is currently available.
|
||||
- `min`: Minimum allowed value.
|
||||
- `max`: Maximum allowed value.
|
||||
- `stepSize`: Step size for value adjustments.
|
||||
- `enumerationType`: The UID of the enumeration type.
|
||||
- `enumerationTypeKey`: The key of the enumeration type.
|
||||
|
||||
## Full Example
|
||||
|
||||
### `demo.things`
|
||||
|
||||
```java
|
||||
Thing homeconnectdirect:dishwasher:myDishwasher "Dishwasher" [ haId="BSH-DISHWASHER-XXXX", address="192.168.1.50" ]
|
||||
Thing homeconnectdirect:oven:myOven "Oven" [ haId="BSH-OVEN-XXXX", address="192.168.1.51" ] {
|
||||
Channels:
|
||||
Type number : currentTemp "Internal Temp" [ valueKey="Cooking.Oven.Status.CurrentCavityTemperature", unit="°C" ]
|
||||
Type trigger : programFinished "Program Finished" [ valueKey="BSH.Common.Event.ProgramFinished" ]
|
||||
Type enum-switch : doorOpen "Door Open" [ valueKey="BSH.Common.Status.DoorState", onValue="Open" ]
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items`
|
||||
|
||||
```java
|
||||
// Dishwasher
|
||||
Switch Dishwasher_Power "Power" { channel="homeconnectdirect:dishwasher:myDishwasher:power-state" }
|
||||
Contact Dishwasher_Door "Door" { channel="homeconnectdirect:dishwasher:myDishwasher:door" }
|
||||
String Dishwasher_Status "Status [%s]" { channel="homeconnectdirect:dishwasher:myDishwasher:operation-state" }
|
||||
Number:Time Dishwasher_Time "Remaining [%d s]" { channel="homeconnectdirect:dishwasher:myDishwasher:remaining-program-time" }
|
||||
|
||||
// Oven
|
||||
Number:Temperature Oven_Temp "Temp [%.1f %unit%]" { channel="homeconnectdirect:oven:myOven:currentTemp" }
|
||||
```
|
||||
|
||||
## Binding Console (Web UI)
|
||||
|
||||
The binding includes a built-in web console (accessible at `http://[YOUR_OPENHAB_IP]:[PORT]/homeconnectdirect`) that serves as a powerful tool for setup, monitoring and troubleshooting.
|
||||
|
||||
### Main Sections
|
||||
|
||||
- **Home Appliances**: Lists all discovered and configured appliances. It provides a quick overview of their connection status (ONLINE/OFFLINE), hardware details (Brand, Type, VIB), and local network addresses.
|
||||
- **Profiles**: Central management for **Appliance Profiles**. Here you can upload new profiles (required for initial setup), download existing ones for backup, or delete outdated profiles.
|
||||
- **Logs**: Allows management of message logs stored on the openHAB server. You can import external logs (e.g., from [HomeCap](https://github.com/bruestel/HomeCap) or the [Home Connect Appliance Proxy](https://github.com/bruestel/homeconnect-appliance-proxy)), download logs for analysis or view them directly in the browser.
|
||||
|
||||

|
||||
|
||||
### Monitoring and Control
|
||||
|
||||
By clicking **"Monitor and Control"** on an appliance, you enter a real-time diagnostic view:
|
||||
|
||||
- **Live Message Stream**: View every message exchanged between openHAB and the appliance in real-time via WebSockets.
|
||||
- **Advanced Filtering**: Narrow down the message stream by value keys, description keys, resource paths, actions (GET/POST/NOTIFY), or specific time ranges.
|
||||
- **Deep Inspection**: Click on any message to view its raw JSON payload, formatted values, or device description changes. You can also browse the internal **Device Description** tree.
|
||||
- **openHAB Configuration Generator**: Next to each value or description attribute, you'll find an openHAB icon. Clicking it opens a generator that provides ready-to-use **YAML** or **DSL** snippets for creating custom channels (see [Custom Channels](#custom-channels)).
|
||||
- **Raw Messaging**: Send manual `GET`, `POST`, or `NOTIFY` commands directly to the appliance for testing and advanced control.
|
||||
|
||||

|
||||

|
||||
|
||||
## Binding Configuration
|
||||
|
||||
The binding offers global settings to secure the web console and manage the internal message log.
|
||||
|
||||
- **Console Security**: You can enable a login for the Home Connect Direct web console (`loginEnabled`). When enabled, a password (`loginPassword`) must be provided.
|
||||
- **Message Log Size**: The `messageQueueSize` determines how many messages are kept in the internal log displayed in the console (default: 300).
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
- **openHAB UI**: Navigate to `Settings` -> `Add-on Settings` -> `Home Connect Direct`.
|
||||
- **OSGi Console**:
|
||||
|
||||
```shell
|
||||
config:edit binding.homeconnectdirect
|
||||
config:property-set loginEnabled true
|
||||
config:property-set loginPassword your_secure_password
|
||||
config:property-set messageQueueSize 500
|
||||
config:update
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Are robot vacuums supported?**
|
||||
**A:** No. Robot vacuums appear to communicate exclusively via the cloud. This binding is designed for direct local communication only.
|
||||
|
||||
**Q: How do I find the `haId` of my device?**
|
||||
**A:** You can find it via the openHAB Inbox scan (Discovery). Alternatively, open the binding's web console and navigate to the **Profiles** tab. After importing a profile, the specific Device ID is displayed there.
|
||||
|
||||
**Q: I have a problem or found a bug.**
|
||||
**A:** Please use the openHAB Community channels. It is helpful to create a log file using the **Logs** feature in the binding's web console, which you can provide for troubleshooting.
|
||||
|
||||
## Credits
|
||||
|
||||
This binding is built upon the outstanding research and work of the following projects:
|
||||
|
||||
- [osresearch/hcpy](https://github.com/osresearch/hcpy)
|
||||
- [hcpy2-0/hcpy](https://github.com/hcpy2-0/hcpy)
|
||||
|
||||
A big thank you to the contributors of these projects for their excellent work!
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>5.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.homeconnectdirect</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Home Connect Direct Binding</name>
|
||||
|
||||
<properties>
|
||||
<bnd.importpackage>org.eclipse.jetty.websocket.server,android.util.*;resolution:="optional",sun.*;resolution:="optional"</bnd.importpackage>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!--
|
||||
Conscrypt Java Security Provider.
|
||||
|
||||
Home Connect appliances secure the local connection with a TLS-PSK handshake using the cipher
|
||||
suite TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256. The JDK default provider (SunJSSE) does not
|
||||
implement TLS-PSK cipher suites, so a JSSE provider that supports them is required. Conscrypt
|
||||
(Google's BoringSSL based provider) supports this cipher suite and is therefore used here.
|
||||
|
||||
Conscrypt primarily targets Android; the official "openjdk-uber" artifact is only published as
|
||||
an outdated version on Maven Central (latest stable 2.5.2) and does not bundle the native
|
||||
libraries for all platforms this binding needs. The dependency below is therefore a custom
|
||||
build (https://github.com/bruestel/conscrypt) that ships the native libraries for the supported
|
||||
platforms: Linux (X64, ARM64), Windows (X64), Mac OS (X64, ARM64).
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>net.bruestel</groupId>
|
||||
<artifactId>conscrypt-openjdk-uber</artifactId>
|
||||
<version>2.6.0-20260113</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.homeconnectdirect-${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-homeconnectdirect" description="Home Connect Direct Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.homeconnectdirect/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.common.utils.StringUtils.SLASH;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectBindingConstants {
|
||||
|
||||
private HomeConnectDirectBindingConstants() {
|
||||
// prevent instantiation
|
||||
}
|
||||
|
||||
public static final String BINDING_ID = "homeconnectdirect";
|
||||
public static final String HOME_APPLIANCE_ID = "haId";
|
||||
public static final String CONFIGURATION_PID = "binding.homeconnectdirect";
|
||||
public static final ZoneId ZONE_ID = ZoneId.systemDefault();
|
||||
public static final Locale LOCALE = Locale.ENGLISH;
|
||||
|
||||
// Binding configuration parameters
|
||||
public static final String CONFIGURATION_LOGIN_ENABLED = "loginEnabled";
|
||||
public static final String CONFIGURATION_LOGIN_PASSWORD = "loginPassword";
|
||||
public static final String CONFIGURATION_MESSAGE_QUEUE_SIZE = "messageQueueSize";
|
||||
|
||||
// List of all appliances
|
||||
public static final String APPLIANCE_TYPE_GENERIC = "generic";
|
||||
public static final String APPLIANCE_TYPE_WASHER = "washer";
|
||||
public static final String APPLIANCE_TYPE_WASHER_AND_DRYER = "washerdryer";
|
||||
public static final String APPLIANCE_TYPE_DRYER = "dryer";
|
||||
public static final String APPLIANCE_TYPE_DISHWASHER = "dishwasher";
|
||||
public static final String APPLIANCE_TYPE_COOK_PROCESSOR = "cookprocessor";
|
||||
public static final String APPLIANCE_TYPE_COFFEE_MAKER = "coffeemaker";
|
||||
public static final String APPLIANCE_TYPE_OVEN = "oven";
|
||||
public static final String APPLIANCE_TYPE_WARMING_DRAWER = "warmingdrawer";
|
||||
public static final String APPLIANCE_TYPE_HOOD = "hood";
|
||||
public static final String APPLIANCE_TYPE_COOKTOP = "cooktop";
|
||||
public static final String APPLIANCE_TYPE_COOKTOP_ALTERNATIVE = "hob";
|
||||
public static final String APPLIANCE_TYPE_FRIDGE_FREEZER = "fridgefreezer";
|
||||
|
||||
// List of all thing type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_GENERIC);
|
||||
public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_DISHWASHER);
|
||||
public static final ThingTypeUID THING_TYPE_WASHER = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_WASHER);
|
||||
public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID,
|
||||
APPLIANCE_TYPE_WASHER_AND_DRYER);
|
||||
public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_DRYER);
|
||||
public static final ThingTypeUID THING_TYPE_COFFEE_MAKER = new ThingTypeUID(BINDING_ID,
|
||||
APPLIANCE_TYPE_COFFEE_MAKER);
|
||||
public static final ThingTypeUID THING_TYPE_COOK_PROCESSOR = new ThingTypeUID(BINDING_ID,
|
||||
APPLIANCE_TYPE_COOK_PROCESSOR);
|
||||
public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_OVEN);
|
||||
public static final ThingTypeUID THING_TYPE_WARMING_DRAWER = new ThingTypeUID(BINDING_ID,
|
||||
APPLIANCE_TYPE_WARMING_DRAWER);
|
||||
public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_HOOD);
|
||||
public static final ThingTypeUID THING_TYPE_COOKTOP = new ThingTypeUID(BINDING_ID, APPLIANCE_TYPE_COOKTOP);
|
||||
public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID,
|
||||
APPLIANCE_TYPE_FRIDGE_FREEZER);
|
||||
|
||||
// List of all channel type UIDs
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_SWITCH_VALUE = new ChannelTypeUID(BINDING_ID, "switch");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_STRING_VALUE = new ChannelTypeUID(BINDING_ID, "string");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_NUMBER_VALUE = new ChannelTypeUID(BINDING_ID, "number");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_TRIGGER_VALUE = new ChannelTypeUID(BINDING_ID, "trigger");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_ENUM_SWITCH_VALUE = new ChannelTypeUID(BINDING_ID, "enum-switch");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_SWITCH_DESCRIPTION = new ChannelTypeUID(BINDING_ID,
|
||||
"device-description-switch");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_STRING_DESCRIPTION = new ChannelTypeUID(BINDING_ID,
|
||||
"device-description-string");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_NUMBER_DESCRIPTION = new ChannelTypeUID(BINDING_ID,
|
||||
"device-description-number");
|
||||
|
||||
// Supported thing types
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_DISHWASHER,
|
||||
THING_TYPE_WASHER, THING_TYPE_WASHER_DRYER, THING_TYPE_DRYER, THING_TYPE_COFFEE_MAKER,
|
||||
THING_TYPE_COOK_PROCESSOR, THING_TYPE_OVEN, THING_TYPE_WARMING_DRAWER, THING_TYPE_HOOD, THING_TYPE_COOKTOP,
|
||||
THING_TYPE_FRIDGE_FREEZER);
|
||||
|
||||
// Configuration properties
|
||||
public static final String PROPERTY_HOME_APPLIANCE_ID = HOME_APPLIANCE_ID;
|
||||
public static final String PROPERTY_ADDRESS = "address";
|
||||
|
||||
// Configuration property values
|
||||
public static final int CONNECTION_TYPE_AES_PORT = 80;
|
||||
public static final int CONNECTION_TYPE_TLS_PORT = 443;
|
||||
|
||||
// Misc
|
||||
public static final String USER_DATA_PATH = OpenHAB.getUserDataFolder();
|
||||
public static final String BINDING_PROFILES_PATH = USER_DATA_PATH + File.separator + BINDING_ID + File.separator
|
||||
+ "profiles";
|
||||
public static final String BINDING_LOGS_PATH = USER_DATA_PATH + File.separator + BINDING_ID + File.separator
|
||||
+ "logs";
|
||||
public static final String CONSCRYPT_PROVIDER = "Conscrypt";
|
||||
public static final String CONSCRYPT_REQUIRED_GLIBC_MIN_VERSION = "2.35";
|
||||
public static final String CONFIGURATION_VALUE_KEY = "valueKey";
|
||||
public static final String CONFIGURATION_UNIT_KEY = "unit";
|
||||
public static final String CONFIGURATION_ON_VALUE_KEY = "onValue";
|
||||
public static final String CONFIGURATION_DESCRIPTION_KEY = "descriptionKey";
|
||||
public static final String CONFIGURATION_ATTRIBUTE = "attribute";
|
||||
public static final Duration UPDATE_ALL_MANDATORY_VALUES_INTERVAL = Duration.ofMinutes(60);
|
||||
|
||||
// Home Connect WebSocket Client
|
||||
public static final String WS_DEVICE_TYPE_APPLICATION_V2 = "Application";
|
||||
public static final Integer WS_DEVICE_TYPE_APPLICATION_V1 = 2;
|
||||
public static final String WS_DEVICE_NAME = "HC Direct";
|
||||
public static final String WS_AES_URI_TEMPLATE = "ws://%s:80/homeconnect";
|
||||
public static final String WS_TLS_URI_TEMPLATE = "wss://%s:443/homeconnect";
|
||||
public static final Duration WS_INACTIVITY_TIMEOUT = Duration.ofSeconds(60);
|
||||
public static final Duration WS_INACTIVITY_CHECK_INTERVAL = Duration.ofSeconds(10);
|
||||
public static final Duration WS_INACTIVITY_CHECK_INITIAL_DELAY = Duration.ofSeconds(5);
|
||||
public static final Duration WS_PING_INTERVAL = Duration.ofSeconds(30);
|
||||
public static final Duration WS_PING_INITIAL_DELAY = Duration.ofSeconds(5);
|
||||
|
||||
// Servlet
|
||||
public static final String SERVLET_BASE_PATH = SLASH + BINDING_ID;
|
||||
public static final String SERVLET_ASSETS_PATH = SERVLET_BASE_PATH + "/assets";
|
||||
public static final String SERVLET_WEB_SOCKET_PATH = SERVLET_BASE_PATH + "/ws";
|
||||
public static final String SERVLET_WEB_SOCKET_PATTERN = SERVLET_WEB_SOCKET_PATH + "/*";
|
||||
|
||||
// Appliance Keys
|
||||
public static final String POWER_STATE_KEY = "BSH.Common.Setting.PowerState";
|
||||
public static final String ROOT_OPTION_LIST_KEY = "BSH.Common.Root.OptionList";
|
||||
public static final String LAUNDRY_CARE_OPTION_LIST_KEY = "LaundryCare.Common.OptionList.Options";
|
||||
public static final String OPERATION_STATE_KEY = "BSH.Common.Status.OperationState";
|
||||
public static final String POWER_STATE_ENUM_KEY = "BSH.Common.EnumType.PowerState";
|
||||
public static final String ACTIVE_PROGRAM_KEY = "BSH.Common.Root.ActiveProgram";
|
||||
public static final String SELECTED_PROGRAM_KEY = "BSH.Common.Root.SelectedProgram";
|
||||
public static final String PAUSE_PROGRAM_KEY = "BSH.Common.Command.PauseProgram";
|
||||
public static final String RESUME_PROGRAM_KEY = "BSH.Common.Command.ResumeProgram";
|
||||
public static final String ABORT_PROGRAM_KEY = "BSH.Common.Command.AbortProgram";
|
||||
public static final String WASHER_I_DOS_1_ACTIVE_KEY = "LaundryCare.Washer.Option.IDos1.Active";
|
||||
public static final String WASHER_I_DOS_2_ACTIVE_KEY = "LaundryCare.Washer.Option.IDos2.Active";
|
||||
public static final String WASHER_I_DOS_1_FILL_LEVEL_POOR_KEY = "LaundryCare.Washer.Event.IDos1FillLevelPoor";
|
||||
public static final String WASHER_I_DOS_2_FILL_LEVEL_POOR_KEY = "LaundryCare.Washer.Event.IDos2FillLevelPoor";
|
||||
public static final String WASHER_TEMPERATURE_KEY = "LaundryCare.Washer.Option.Temperature";
|
||||
public static final String WASHER_SPIN_SPEED_KEY = "LaundryCare.Washer.Option.SpinSpeed";
|
||||
public static final String WASHER_SPEED_PERFECT_KEY = "LaundryCare.Washer.Option.SpeedPerfect";
|
||||
public static final String WASHER_WATER_PLUS_KEY = "LaundryCare.Washer.Option.WaterPlus";
|
||||
public static final String WASHER_PREWASH_KEY = "LaundryCare.Washer.Option.Prewash";
|
||||
public static final String WASHER_RINSE_HOLD_KEY = "LaundryCare.Washer.Option.RinseHold";
|
||||
public static final String WASHER_LESS_IRONING_KEY = "LaundryCare.Washer.Option.LessIroning";
|
||||
public static final String WASHER_SILENT_WASH_KEY = "LaundryCare.Washer.Option.SilentWash";
|
||||
public static final String WASHER_SOAK_KEY = "LaundryCare.Washer.Option.Soak";
|
||||
public static final String WASHER_RINSE_PLUS_KEY = "LaundryCare.Washer.Option.RinsePlus";
|
||||
public static final String WASHER_STAINS_KEY = "LaundryCare.Washer.Option.Stains";
|
||||
public static final String LAUNDRY_CARE_PROCESS_PHASE_KEY = "LaundryCare.Common.Option.ProcessPhase";
|
||||
public static final String LAUNDRY_LOAD_INFORMATION_KEY = "LaundryCare.Common.Status.LoadInformation";
|
||||
public static final String LAUNDRY_LOAD_KEY = "LaundryCare.Washer.Status.ProcessingData.LaundryLoad";
|
||||
public static final String LAUNDRY_LOAD_RECOMMENDATION_KEY = "LaundryCare.Common.Option.LoadRecommendation";
|
||||
public static final String LAUNDRY_DRUM_CLEAN_REMINDER_KEY = "LaundryCare.Washer.Event.DrumCleanReminder";
|
||||
public static final String DRYER_PROCESS_PHASE_KEY = "LaundryCare.Dryer.Option.ProcessPhase";
|
||||
public static final String DRYER_WRINKLE_GUARD_KEY = "LaundryCare.Dryer.Option.WrinkleGuard";
|
||||
public static final String DRYER_DRYING_TARGET_KEY = "LaundryCare.Dryer.Option.DryingTarget";
|
||||
public static final String COOKING_LIGHTING_KEY = "Cooking.Common.Setting.Lighting";
|
||||
public static final String CHILD_LOCK_KEY = "BSH.Common.Setting.ChildLock";
|
||||
public static final String COOKING_LIGHTING_BRIGHTNESS_KEY = "Cooking.Common.Setting.LightingBrightness";
|
||||
public static final String COOKING_BUTTON_TONES_KEY = "Cooking.Common.Setting.ButtonTones";
|
||||
public static final String HOOD_VENTING_LEVEL_KEY = "Cooking.Common.Option.Hood.VentingLevel";
|
||||
public static final String HOOD_VENTING_LEVEL_ENUM_KEY = "Cooking.Hood.EnumType.Stage";
|
||||
public static final String HOOD_INTENSIVE_LEVEL_KEY = "Cooking.Common.Option.Hood.IntensiveLevel";
|
||||
public static final String HOOD_INTENSIVE_LEVEL_ENUM_KEY = "Cooking.Hood.EnumType.IntensiveStage";
|
||||
public static final String PROGRAM_PROGRESS_KEY = "BSH.Common.Option.ProgramProgress";
|
||||
public static final String REMAINING_PROGRAM_TIME_KEY = "BSH.Common.Option.RemainingProgramTime";
|
||||
public static final String REMOTE_CONTROL_START_ALLOWED_KEY = "BSH.Common.Status.RemoteControlStartAllowed";
|
||||
public static final String DOOR_STATE_KEY = "BSH.Common.Status.DoorState";
|
||||
public static final String DISHWASHER_SALT_LACK_KEY = "Dishcare.Dishwasher.Event.SaltLack";
|
||||
public static final String DISHWASHER_RINSE_AID_LACK_KEY = "Dishcare.Dishwasher.Event.RinseAidLack";
|
||||
public static final String DISHWASHER_SALT_NEARLY_EMPTY_KEY = "Dishcare.Dishwasher.Event.SaltNearlyEmpty";
|
||||
public static final String DISHWASHER_RINSE_AID_NEARLY_EMPTY_KEY = "Dishcare.Dishwasher.Event.RinseAidNearlyEmpty";
|
||||
public static final String DISHWASHER_MACHINE_CARE_REMINDER_KEY = "Dishcare.Dishwasher.Event.MachineCareReminder";
|
||||
public static final String DISHWASHER_PROGRAM_PHASE_KEY = "Dishcare.Dishwasher.Status.ProgramPhase";
|
||||
public static final String DISHWASHER_VARIO_SPEED_PLUS_KEY = "Dishcare.Dishwasher.Option.VarioSpeedPlus";
|
||||
public static final String DISHWASHER_INTENSIV_ZONE_KEY = "Dishcare.Dishwasher.Option.IntensivZone";
|
||||
public static final String DISHWASHER_BRILLIANCE_DRY_KEY = "Dishcare.Dishwasher.Option.BrillianceDry";
|
||||
public static final String COFFEE_MAKER_PROCESS_PHASE_KEY = "ConsumerProducts.CoffeeMaker.Status.ProcessPhase";
|
||||
public static final String COFFEE_MAKER_COUNTDOWN_CLEANING_KEY = "ConsumerProducts.CoffeeMaker.Status.BeverageCountdownCleaning";
|
||||
public static final String COFFEE_MAKER_COUNTDOWN_CALC_AND_CLEAN_KEY = "ConsumerProducts.CoffeeMaker.Status.BeverageCountdownCalcNClean";
|
||||
public static final String COFFEE_MAKER_COUNTDOWN_DESCALING_KEY = "ConsumerProducts.CoffeeMaker.Status.BeverageCountdownDescaling";
|
||||
public static final String COFFEE_MAKER_COUNTDOWN_WATER_FILTER_KEY = "ConsumerProducts.CoffeeMaker.Status.BeverageCountdownWaterfilter";
|
||||
public static final String COFFEE_MAKER_WATER_TANK_EMPTY_KEY = "ConsumerProducts.CoffeeMaker.Event.WaterTankEmpty";
|
||||
public static final String COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY_KEY = "ConsumerProducts.CoffeeMaker.Event.WaterTankNearlyEmpty";
|
||||
public static final String COFFEE_MAKER_DRIP_TRAY_FULL_KEY = "ConsumerProducts.CoffeeMaker.Event.DripTrayFull";
|
||||
public static final String COFFEE_MAKER_EMPTY_MILK_TANK_KEY = "ConsumerProducts.CoffeeMaker.Event.EmptyMilkTank";
|
||||
public static final String COFFEE_MAKER_BEAN_CONTAINER_EMPTY_KEY = "ConsumerProducts.CoffeeMaker.Event.BeanContainerEmpty";
|
||||
public static final String OVEN_DURATION_KEY = "BSH.Common.Option.Duration";
|
||||
public static final String OVEN_SET_POINT_TEMPERATURE_KEY = "Cooking.Oven.Option.SetpointTemperature";
|
||||
public static final String OVEN_CURRENT_TEMPERATURE_KEY = "Cooking.Oven.Status.CurrentCavityTemperature";
|
||||
public static final String OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_KEY = "Cooking.Oven.Status.CurrentMeatprobeTemperature";
|
||||
public static final String OVEN_MEAT_PROBE_PLUGGED_KEY = "Cooking.Oven.Status.MeatprobePlugged";
|
||||
public static final String OVEN_CAVITY_SELECTOR_ENUM_KEY = "Cooking.Oven.EnumType.CavitySelector";
|
||||
public static final String FRIDGE_DOOR_STATE_KEY = "Refrigeration.FridgeFreezer.Status.DoorRefrigerator";
|
||||
public static final String FRIDGE_DOOR_STATE_2_KEY = "Refrigeration.Common.Status.Door.Refrigerator";
|
||||
public static final String FRIDGE_CHILLER_DOOR_STATE_KEY = "Refrigeration.Common.Status.Door.ChillerCommon";
|
||||
public static final String FRIDGE_SET_POINT_TEMPERATURE_KEY = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator";
|
||||
public static final String FRIDGE_SET_POINT_TEMPERATURE_2_KEY = "Refrigeration.Common.Setting.Refrigerator.SetpointTemperature";
|
||||
public static final String FRIDGE_CHILLER_SET_POINT_TEMPERATURE_KEY = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureChiller";
|
||||
public static final String FRIDGE_CHILLER_SET_POINT_TEMPERATURE_2_KEY = "Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature";
|
||||
public static final String FRIDGE_CHILLER_PRESET_KEY = "Refrigeration.Common.Setting.ChillerCommon.Preset";
|
||||
public static final String FRIDGE_SUPER_MODE_KEY = "Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator";
|
||||
public static final String FRIDGE_SUPER_MODE_2_KEY = "Refrigeration.Common.Setting.Refrigerator.SuperMode";
|
||||
public static final String FRIDGE_DISPENSER_WATER_FILTER_SATURATION_KEY = "Refrigeration.Common.Status.Dispenser.WaterFilterSaturation";
|
||||
public static final String FRIDGE_DISPENSER_PARTY_MODE_KEY = "Refrigeration.Common.Setting.Dispenser.PartyMode";
|
||||
public static final String FRIDGE_DISPENSER_ENABLED_KEY = "Refrigeration.Common.Setting.Dispenser.Enabled";
|
||||
public static final String FREEZER_SET_POINT_TEMPERATURE_KEY = "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer";
|
||||
public static final String FREEZER_SET_POINT_TEMPERATURE_2_KEY = "Refrigeration.Common.Setting.Freezer.SetpointTemperature";
|
||||
public static final String FREEZER_DOOR_STATE_KEY = "Refrigeration.FridgeFreezer.Status.DoorFreezer";
|
||||
public static final String FREEZER_DOOR_STATE_2_KEY = "Refrigeration.Common.Status.Door.Freezer";
|
||||
public static final String FREEZER_SUPER_MODE_KEY = "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer";
|
||||
public static final String FREEZER_SUPER_MODE_2_KEY = "Refrigeration.Common.Setting.Freezer.SuperMode";
|
||||
|
||||
// Appliance Template Keys
|
||||
public static final String OVEN_DOOR_STATE_KEY_TEMPLATE = "Cooking.Oven.Status.Cavity.%03d.DoorState";
|
||||
public static final String OVEN_CURRENT_TEMPERATURE_KEY_TEMPLATE = "Cooking.Oven.Status.Cavity.%03d.CurrentTemperature";
|
||||
public static final String OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_KEY_TEMPLATE = "Cooking.Oven.Status.Cavity.%03d.CurrentMeatprobeTemperature";
|
||||
public static final String OVEN_CAVITY_LIGHT_KEY_TEMPLATE = "Cooking.Oven.Setting.Light.Cavity.%03d.Power";
|
||||
public static final String OVEN_MEAT_PROBE_PLUGGED_KEY_TEMPLATE = "Cooking.Oven.Status.Cavity.%03d.MeatprobePlugged";
|
||||
public static final String PROGRAM_FAVORITE_KEY_TEMPLATE = "BSH.Common.Program.Favorite.%03d";
|
||||
|
||||
// Channels
|
||||
public static final String CHANNEL_COMMAND = "command";
|
||||
public static final String CHANNEL_REMOTE_CONTROL_OR_START_ALLOWED = "remote-control-start-allowed";
|
||||
public static final String CHANNEL_POWER_STATE = "power-state";
|
||||
public static final String CHANNEL_DOOR = "door";
|
||||
public static final String CHANNEL_OPERATION_STATE = "operation-state";
|
||||
public static final String CHANNEL_ACTIVE_PROGRAM = "active-program";
|
||||
public static final String CHANNEL_SELECTED_PROGRAM = "selected-program";
|
||||
public static final String CHANNEL_REMAINING_PROGRAM_TIME = "remaining-program-time";
|
||||
public static final String CHANNEL_PROGRAM_PROGRESS = "program-progress";
|
||||
public static final String CHANNEL_PROGRAM_COMMAND = "program-command";
|
||||
public static final String CHANNEL_WASHER_I_DOS_1_FILL_LEVEL_POOR = "idos1-fill-level-poor";
|
||||
public static final String CHANNEL_WASHER_I_DOS_2_FILL_LEVEL_POOR = "idos2-fill-level-poor";
|
||||
public static final String CHANNEL_WASHER_I_DOS_1_ACTIVE = "idos1-active";
|
||||
public static final String CHANNEL_WASHER_I_DOS_2_ACTIVE = "idos2-active";
|
||||
public static final String CHANNEL_WASHER_TEMPERATURE = "washer-temperature";
|
||||
public static final String CHANNEL_WASHER_SPIN_SPEED = "washer-spin-speed";
|
||||
public static final String CHANNEL_WASHER_SPEED_PERFECT = "washer-speed-perfect";
|
||||
public static final String CHANNEL_WASHER_WATER_PLUS = "washer-water-plus";
|
||||
public static final String CHANNEL_WASHER_PREWASH = "washer-prewash";
|
||||
public static final String CHANNEL_WASHER_RINSE_HOLD = "washer-rinse-hold";
|
||||
public static final String CHANNEL_WASHER_LESS_IRONING = "washer-less-ironing";
|
||||
public static final String CHANNEL_WASHER_SILENT_WASH = "washer-silent-wash";
|
||||
public static final String CHANNEL_WASHER_SOAK = "washer-soak";
|
||||
public static final String CHANNEL_WASHER_RINSE_PLUS = "washer-rinse-plus";
|
||||
public static final String CHANNEL_WASHER_STAINS = "washer-stains";
|
||||
public static final String CHANNEL_DRYER_DRYING_TARGET = "drying-target";
|
||||
public static final String CHANNEL_DRYER_WRINKLE_GUARD = "wrinkle-guard";
|
||||
public static final String CHANNEL_LAUNDRY_LOAD_INFORMATION = "laundry-load-information";
|
||||
public static final String CHANNEL_LAUNDRY_LOAD_RECOMMENDATION = "laundry-load-recommendation";
|
||||
public static final String CHANNEL_LAUNDRY_DRUM_CLEAN_REMINDER = "drum-clean-reminder";
|
||||
public static final String CHANNEL_LAUNDRY_CARE_PROCESS_PHASE = "process-phase";
|
||||
public static final String CHANNEL_DISHWASHER_SALT_LACK = "salt-lack";
|
||||
public static final String CHANNEL_DISHWASHER_RINSE_AID_LACK = "rinse-aid-lack";
|
||||
public static final String CHANNEL_DISHWASHER_SALT_NEARLY_EMPTY = "salt-nearly-empty";
|
||||
public static final String CHANNEL_DISHWASHER_RINSE_AID_NEARLY_EMPTY = "rinse-aid-nearly-empty";
|
||||
public static final String CHANNEL_DISHWASHER_MACHINE_CARE_REMINDER = "machine-care-reminder";
|
||||
public static final String CHANNEL_DISHWASHER_PROGRAM_PHASE = "program-phase";
|
||||
public static final String CHANNEL_DISHWASHER_VARIO_SPEED_PLUS = "dishwasher-vario-speed-plus";
|
||||
public static final String CHANNEL_DISHWASHER_INTENSIV_ZONE = "dishwasher-intensiv-zone";
|
||||
public static final String CHANNEL_DISHWASHER_BRILLIANCE_DRY = "dishwasher-brilliance-dry";
|
||||
public static final String CHANNEL_COFFEE_MAKER_COUNTDOWN_CLEANING = "cleaning";
|
||||
public static final String CHANNEL_COFFEE_MAKER_COUNTDOWN_CALC_AND_CLEAN = "calc-n-clean";
|
||||
public static final String CHANNEL_COFFEE_MAKER_COUNTDOWN_DESCALING = "descaling";
|
||||
public static final String CHANNEL_COFFEE_MAKER_COUNTDOWN_WATER_FILTER = "water-filter";
|
||||
public static final String CHANNEL_COFFEE_MAKER_WATER_TANK_EMPTY = "water-tank-empty";
|
||||
public static final String CHANNEL_COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY = "water-tank-nearly-empty";
|
||||
public static final String CHANNEL_COFFEE_MAKER_DRIP_TRAY_FULL = "drip-tray-full";
|
||||
public static final String CHANNEL_COFFEE_MAKER_EMPTY_MILK_TANK = "empty-milk-tank";
|
||||
public static final String CHANNEL_COFFEE_MAKER_BEAN_CONTAINER_EMPTY = "bean-container-empty";
|
||||
public static final String CHANNEL_COFFEE_MAKER_PROCESS_PHASE = "process-phase";
|
||||
public static final String CHANNEL_COFFEE_MAKER_PROGRAM_COMMAND = "coffeemaker-program-command";
|
||||
public static final String CHANNEL_OVEN_DURATION = "duration";
|
||||
public static final String CHANNEL_OVEN_SET_POINT_TEMPERATURE = "setpoint-temperature";
|
||||
public static final String CHANNEL_OVEN_PROGRAM_COMMAND = "oven-program-command";
|
||||
public static final String CHANNEL_COOKING_LIGHT = "cooking-light";
|
||||
public static final String CHANNEL_COOKING_LIGHT_BRIGHTNESS = "cooking-light-brightness";
|
||||
public static final String CHANNEL_BUTTON_TONES = "button-tones";
|
||||
public static final String CHANNEL_HOOD_VENTING_LEVEL = "venting-level";
|
||||
public static final String CHANNEL_HOOD_INTENSIVE_LEVEL = "intensive-level";
|
||||
public static final String CHANNEL_CHILD_LOCK = "child-lock";
|
||||
public static final String CHANNEL_FRIDGE_DOOR_STATE = "refrigerator-door";
|
||||
public static final String CHANNEL_FREEZER_DOOR_STATE = "freezer-door";
|
||||
public static final String CHANNEL_FRIDGE_CHILLER_DOOR_STATE = "chiller-door";
|
||||
public static final String CHANNEL_FRIDGE_DISPENSER_WATER_FILTER_SATURATION = "dispenser-water-filter-saturation";
|
||||
public static final String CHANNEL_FRIDGE_DISPENSER_ENABLED = "dispenser-enabled";
|
||||
public static final String CHANNEL_FRIDGE_DISPENSER_PARTY_MODE = "dispenser-party-mode";
|
||||
public static final String CHANNEL_FRIDGE_SET_POINT_TEMPERATURE = "setpoint-temperature-refrigerator";
|
||||
public static final String CHANNEL_FREEZER_SET_POINT_TEMPERATURE = "setpoint-temperature-freezer";
|
||||
public static final String CHANNEL_FRIDGE_CHILLER_SET_POINT_TEMPERATURE = "setpoint-temperature-chiller";
|
||||
public static final String CHANNEL_FRIDGE_CHILLER_PRESET = "chiller-preset";
|
||||
public static final String CHANNEL_FRIDGE_SUPER_MODE = "super-mode-refrigerator";
|
||||
public static final String CHANNEL_FREEZER_SUPER_MODE = "super-mode-freezer";
|
||||
public static final String CHANNEL_RAW_MESSAGE = "raw-message";
|
||||
|
||||
// Channel Templates
|
||||
public static final String CHANNEL_OVEN_DOOR_TEMPLATE = "door-%d";
|
||||
public static final String CHANNEL_OVEN_TEMPERATURE_TEMPLATE = "temperature-%d";
|
||||
public static final String CHANNEL_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_TEMPLATE = "meat-probe-temperature-%d";
|
||||
public static final String CHANNEL_OVEN_MEAT_PROBE_PLUGGED_TEMPLATE = "meat-probe-plugged-%d";
|
||||
public static final String CHANNEL_OVEN_CAVITY_LIGHT_TEMPLATE = "cavity-light-%d";
|
||||
|
||||
// Channel Types
|
||||
public static final String CHANNEL_TYPE_I_DOS_FILL_LEVEL_POOR = "washer-idos-fill-level-poor-channel";
|
||||
public static final String CHANNEL_TYPE_I_DOS_ACTIVE = "washer-idos-active-channel";
|
||||
public static final String CHANNEL_TYPE_DOOR = "door-channel";
|
||||
public static final String CHANNEL_TYPE_OVEN_CURRENT_TEMPERATURE = "oven-current-cavity-temperature-channel";
|
||||
public static final String CHANNEL_TYPE_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE = "oven-current-meat-probe-temperature-channel";
|
||||
public static final String CHANNEL_TYPE_OVEN_MEAT_PROBE_PLUGGED = "oven-meat-probe-plugged-channel";
|
||||
public static final String CHANNEL_TYPE_OVEN_CAVITY_LIGHT = "oven-cavity-light-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_FREEZER_SET_POINT_TEMPERATURE = "fridgefreezer-setpoint-temperature-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_FREEZER_SUPER_MODE = "fridgefreezer-super-mode-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_DISPENSER_WATER_FILTER_SATURATION = "fridgefreezer-dispenser-water-filter-saturation-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_DISPENSER_ENABLED = "fridgefreezer-dispenser-enabled-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_DISPENSER_PARTY_MODE = "fridgefreezer-dispenser-party-mode-channel";
|
||||
public static final String CHANNEL_TYPE_FRIDGE_CHILLER_PRESET = "fridgefreezer-chiller-preset-channel";
|
||||
|
||||
// State values
|
||||
public static final String STATE_OPEN = "Open";
|
||||
public static final String STATE_AJAR = "Ajar";
|
||||
public static final String STATE_ON = "On";
|
||||
public static final String STATE_OFF = "Off";
|
||||
public static final String STATE_MAINS_OFF = "MainsOff";
|
||||
public static final String STATE_STANDBY = "Standby";
|
||||
public static final String STATE_FINISHED = "Finished";
|
||||
public static final String STATE_RUN = "Run";
|
||||
public static final String STATE_NO_PROGRAM = "0";
|
||||
public static final String STATE_HOOD_VENTING = "Cooking.Common.Program.Hood.Venting";
|
||||
public static final String STATE_PRESENT = "Present";
|
||||
public static final String STATE_CONFIRMED = "Confirmed";
|
||||
public static final String STATE_FAN_OFF = "FanOff";
|
||||
public static final String STATE_FAN_STAGE_1 = "FanStage01";
|
||||
public static final String STATE_FAN_STAGE_2 = "FanStage02";
|
||||
public static final String STATE_FAN_STAGE_3 = "FanStage03";
|
||||
public static final String STATE_FAN_STAGE_4 = "FanStage04";
|
||||
public static final String STATE_FAN_STAGE_5 = "FanStage05";
|
||||
public static final String STATE_INTENSIVE_STAGE_OFF = "IntensiveStageOff";
|
||||
public static final String STATE_INTENSIVE_STAGE_1 = "IntensiveStage1";
|
||||
public static final String STATE_INTENSIVE_STAGE_2 = "IntensiveStage2";
|
||||
public static final String STATE_POOR = "Poor";
|
||||
|
||||
// Commands
|
||||
public static final String COMMAND_START = "start";
|
||||
public static final String COMMAND_PAUSE = "pause";
|
||||
public static final String COMMAND_RESUME = "resume";
|
||||
public static final String COMMAND_STOP = "stop";
|
||||
|
||||
// Dimensions
|
||||
public static final String NUMBER_TEMPERATURE = CoreItemFactory.NUMBER + ":Temperature";
|
||||
public static final String NUMBER_DIMENSIONLESS = CoreItemFactory.NUMBER + ":Dimensionless";
|
||||
|
||||
// i18n
|
||||
public static final String I18N_I_DOS_FILL_LEVEL_POOR = "channel.idos.fill-level-poor";
|
||||
public static final String I18N_I_DOS_ACTIVE = "channel.idos.active";
|
||||
public static final String I18N_START_PROGRAM = "command.program.start";
|
||||
public static final String I18N_PAUSE_PROGRAM = "command.program.pause";
|
||||
public static final String I18N_RESUME_PROGRAM = "command.program.resume";
|
||||
public static final String I18N_STOP_PROGRAM = "command.program.stop";
|
||||
public static final String I18N_OVEN_DOOR = "channel.oven.door";
|
||||
public static final String I18N_OVEN_CURRENT_TEMPERATURE = "channel.oven.current-temperature";
|
||||
public static final String I18N_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE = "channel.oven.meat-probe-temperature";
|
||||
public static final String I18N_OVEN_MEAT_PROBE_PLUGGED = "channel.oven.meat-probe-plugged";
|
||||
public static final String I18N_OVEN_CAVITY_LIGHT = "channel.oven.cavity-light";
|
||||
public static final String I18N_FRIDGE_FREEZER_SET_POINT_TEMPERATURE = "channel.fridgefreezer.setpoint";
|
||||
public static final String I18N_FRIDGE_FREEZER_DOOR = "channel.fridgefreezer.door";
|
||||
public static final String I18N_FRIDGE_FREEZER_SUPER_MODE = "channel.fridgefreezer.super-mode";
|
||||
public static final String I18N_FRIDGE_DISPENSER_WATER_FILTER_SATURATION = "channel.fridgefreezer.water-filter-saturation";
|
||||
public static final String I18N_FRIDGE_DISPENSER_ENABLED = "channel.fridgefreezer.dispenser-enabled";
|
||||
public static final String I18N_FRIDGE_DISPENSER_PARTY_MODE = "channel.fridgefreezer.dispenser-party-mode";
|
||||
public static final String I18N_FRIDGE_CHILLER_PRESET = "channel.fridgefreezer.chiller-preset";
|
||||
public static final String I18N_DOOR = "channel.door";
|
||||
|
||||
// Device Description Attributes
|
||||
public static final String ATTRIBUTE_ACCESS = "access";
|
||||
public static final String ATTRIBUTE_AVAILABLE = "available";
|
||||
public static final String ATTRIBUTE_MIN = "min";
|
||||
public static final String ATTRIBUTE_MAX = "max";
|
||||
public static final String ATTRIBUTE_STEP_SIZE = "stepSize";
|
||||
public static final String ATTRIBUTE_ENUMERATION_TYPE = "enumerationType";
|
||||
public static final String ATTRIBUTE_ENUMERATION_TYPE_KEY = "enumerationTypeKey";
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A memory-optimized, thread-safe map that allows looking up values by two different unique keys.
|
||||
* It stores values only once (referenced by K1) and maintains key mappings to allow lookup by K2.
|
||||
*
|
||||
* @param <K1> The first key type (Primary Key)
|
||||
* @param <K2> The second key type (Secondary Key)
|
||||
* @param <V> The value type
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DoubleKeyMap<K1, K2, V> {
|
||||
|
||||
// Primary storage: K1 -> Value
|
||||
private final Map<K1, V> k1ToVal = new HashMap<>();
|
||||
|
||||
// Key mappings to maintain 1:1 relationship and allow bi-directional lookups/removals
|
||||
private final Map<K1, K2> k1ToK2 = new HashMap<>();
|
||||
private final Map<K2, K1> k2ToK1 = new HashMap<>();
|
||||
|
||||
public synchronized void put(K1 k1, K2 k2, V value) {
|
||||
removeByKey1(k1);
|
||||
removeByKey2(k2);
|
||||
|
||||
k1ToVal.put(k1, value);
|
||||
k1ToK2.put(k1, k2);
|
||||
k2ToK1.put(k2, k1);
|
||||
}
|
||||
|
||||
public synchronized @Nullable V getByKey1(@Nullable K1 k1) {
|
||||
if (k1 == null) {
|
||||
return null;
|
||||
}
|
||||
return k1ToVal.get(k1);
|
||||
}
|
||||
|
||||
public synchronized @Nullable V getByKey2(@Nullable K2 k2) {
|
||||
@Nullable
|
||||
K1 k1 = k2ToK1.get(k2);
|
||||
if (k1 == null) {
|
||||
return null;
|
||||
}
|
||||
return k1ToVal.get(k1);
|
||||
}
|
||||
|
||||
public synchronized @Nullable V removeByKey1(@Nullable K1 k1) {
|
||||
if (k1 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
V value = k1ToVal.remove(k1);
|
||||
@Nullable
|
||||
K2 k2 = k1ToK2.remove(k1);
|
||||
|
||||
if (k2 != null) {
|
||||
k2ToK1.remove(k2);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public synchronized @Nullable V removeByKey2(@Nullable K2 k2) {
|
||||
@Nullable
|
||||
K1 k1 = k2ToK1.remove(k2);
|
||||
if (k1 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
V value = k1ToVal.remove(k1);
|
||||
k1ToK2.remove(k1);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
k1ToVal.clear();
|
||||
k1ToK2.clear();
|
||||
k2ToK1.clear();
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
return k1ToVal.size();
|
||||
}
|
||||
|
||||
public synchronized Set<Map.Entry<K1, V>> entrySet() {
|
||||
return Set.copyOf(k1ToVal.entrySet());
|
||||
}
|
||||
|
||||
public synchronized Collection<V> values() {
|
||||
return List.copyOf(k1ToVal.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String toString() {
|
||||
return "DoubleKeyMap{size=" + k1ToVal.size() + ", data=" + k1ToVal + "}";
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* A list implementation with a fixed maximum size that discards oldest entries.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LimitedSizeList<E> {
|
||||
private final LinkedList<E> list = new LinkedList<>();
|
||||
private final int capacity;
|
||||
|
||||
public LimitedSizeList(int capacity) {
|
||||
if (capacity <= 0) {
|
||||
throw new IllegalArgumentException("Capacity must be greater than 0!");
|
||||
}
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public synchronized void add(E element) {
|
||||
if (list.size() >= capacity) {
|
||||
list.removeFirst();
|
||||
}
|
||||
list.addLast(element);
|
||||
}
|
||||
|
||||
public synchronized List<E> getAllElements() {
|
||||
return Collections.unmodifiableList(new LinkedList<>(list));
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
public synchronized boolean isEmpty() {
|
||||
return list.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String toString() {
|
||||
return list.toString();
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.json.adapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Gson adapter for {@link DoubleKeyMap} serialization and deserialization.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DoubleKeyMapAdapter implements JsonSerializer<DoubleKeyMap<?, ?, ?>> {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public JsonElement serialize(@Nullable DoubleKeyMap<?, ?, ?> src, @Nullable Type typeOfSrc,
|
||||
@Nullable JsonSerializationContext context) {
|
||||
if (src == null || context == null) {
|
||||
return new JsonArray();
|
||||
}
|
||||
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
src.entrySet().stream().sorted((e1, e2) -> {
|
||||
Object k1 = e1.getKey();
|
||||
Object k2 = e2.getKey();
|
||||
if (k1 instanceof Comparable && k2 instanceof Comparable) {
|
||||
return ((Comparable<Object>) k1).compareTo(k2);
|
||||
}
|
||||
return 0;
|
||||
}).map(Map.Entry::getValue).forEach(value -> jsonArray.add(context.serialize(value)));
|
||||
|
||||
return jsonArray;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.json.adapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Gson adapter for {@link java.time.OffsetDateTime} serialization and deserialization.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OffsetDateTimeAdapter implements JsonSerializer<OffsetDateTime>, JsonDeserializer<OffsetDateTime> {
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(@Nullable OffsetDateTime src, @Nullable Type typeOfSrc,
|
||||
@Nullable JsonSerializationContext context) {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE;
|
||||
}
|
||||
return new JsonPrimitive(src.format(FORMATTER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable OffsetDateTime deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
|
||||
@Nullable JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json.isJsonNull()) {
|
||||
return null;
|
||||
}
|
||||
String value = json.getAsString();
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return OffsetDateTime.parse(value, FORMATTER);
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.utils;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.json.adapter.DoubleKeyMapAdapter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.json.adapter.OffsetDateTimeAdapter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.serializer.ResourceAdapter;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.util.StringUtils;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
|
||||
/**
|
||||
*
|
||||
* Home Connect Direct configuration utilities.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigurationUtils {
|
||||
|
||||
public static final String WS_DEVICE_ID_PREFIX = "cafebabe0000";
|
||||
public static final String WS_DEVICE_ID_PATH = OpenHAB.getUserDataFolder() + File.separator + BINDING_ID
|
||||
+ File.separator + "device.id";
|
||||
|
||||
private static final String DEVICE_ID_HASH_ALGORITHM = "SHA-256";
|
||||
private static final int DEVICE_ID_HASH_LENGTH = 12;
|
||||
private static final HexFormat HEX_FORMATTER = HexFormat.of();
|
||||
|
||||
private ConfigurationUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the binding configuration.
|
||||
*
|
||||
* @param configurationAdmin the configuration admin service
|
||||
* @return binding configuration
|
||||
*/
|
||||
public static HomeConnectDirectConfiguration getConfiguration(ConfigurationAdmin configurationAdmin) {
|
||||
var configuration = new HomeConnectDirectConfiguration();
|
||||
try {
|
||||
var config = configurationAdmin.getConfiguration(CONFIGURATION_PID);
|
||||
var properties = config.getProperties();
|
||||
|
||||
if (properties != null) {
|
||||
configuration.loginEnabled = Boolean
|
||||
.parseBoolean(String.valueOf(properties.get(CONFIGURATION_LOGIN_ENABLED)));
|
||||
var passwordObject = properties.get(CONFIGURATION_LOGIN_PASSWORD);
|
||||
if (passwordObject instanceof String password && !password.isBlank()) {
|
||||
configuration.loginPassword = password;
|
||||
}
|
||||
var queueSizeObject = properties.get(CONFIGURATION_MESSAGE_QUEUE_SIZE);
|
||||
if (queueSizeObject != null) {
|
||||
int queueSize = Integer.parseInt(String.valueOf(queueSizeObject));
|
||||
configuration.messageQueueSize = Math.min(86400, Math.max(10, queueSize));
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// could not get configuration, use defaults
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Gson instance with custom serializers and deserializers.
|
||||
*
|
||||
* @return Gson instance
|
||||
*/
|
||||
public static Gson createGson() {
|
||||
return new GsonBuilder().registerTypeAdapter(Resource.class, new ResourceAdapter())
|
||||
.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter())
|
||||
.registerTypeAdapter(DoubleKeyMap.class, new DoubleKeyMapAdapter())
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
|
||||
}
|
||||
|
||||
public static String getDeviceId() {
|
||||
var path = Path.of(WS_DEVICE_ID_PATH);
|
||||
try {
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectories(path.getParent());
|
||||
var deviceId = generateDeviceId();
|
||||
Files.writeString(path, deviceId, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
return deviceId;
|
||||
} else {
|
||||
return Files.readString(path).trim();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// use fallback device id
|
||||
return generateDeviceId();
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateDeviceId() {
|
||||
try {
|
||||
var hostname = InetAddress.getLocalHost().getHostName().trim();
|
||||
var digest = MessageDigest.getInstance(DEVICE_ID_HASH_ALGORITHM);
|
||||
byte[] fullHash = digest.digest(hostname.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
byte[] truncatedHash = Arrays.copyOf(fullHash, DEVICE_ID_HASH_LENGTH);
|
||||
|
||||
return HEX_FORMATTER.formatHex(truncatedHash);
|
||||
} catch (SecurityException | UnknownHostException | NoSuchAlgorithmException e) {
|
||||
return WS_DEVICE_ID_PREFIX + StringUtils
|
||||
.getRandomHex(2 * DEVICE_ID_HASH_LENGTH - WS_DEVICE_ID_PREFIX.length()).toLowerCase(LOCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.utils;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Operating system utility methods.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OSUtils {
|
||||
private static final String NAME = getSystemProperty("os.name", "Unknown OS");
|
||||
private static final String ARCH = getSystemProperty("os.arch", "Unknown");
|
||||
private static final String LINUX = "Linux";
|
||||
|
||||
private OSUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
public static String getOSName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
public static String getOSArch() {
|
||||
return ARCH;
|
||||
}
|
||||
|
||||
public static boolean isLinux() {
|
||||
return LINUX.equalsIgnoreCase(NAME);
|
||||
}
|
||||
|
||||
private static String getSystemProperty(String key, String defaultValue) {
|
||||
String value;
|
||||
try {
|
||||
value = System.getProperty(key, defaultValue);
|
||||
} catch (SecurityException e) {
|
||||
value = defaultValue;
|
||||
}
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.utils;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
|
||||
/**
|
||||
* Home Connect Direct string utilities.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class StringUtils {
|
||||
|
||||
public static final String EMPTY_STRING = "";
|
||||
public static final String SPACE = " ";
|
||||
public static final String HYPHEN = "-";
|
||||
public static final String SLASH = "/";
|
||||
public static final String OPENING_BRACE = "{";
|
||||
public static final String CLOSING_BRACE = "}";
|
||||
|
||||
private static final Pattern CAMEL_CASE_SPLIT_PATTERN = Pattern
|
||||
.compile("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])");
|
||||
|
||||
private static final Map<String, String> LABEL_REPLACEMENTS = Map.ofEntries(Map.entry("Wi Fi", "WiFi"),
|
||||
Map.entry("Auto40", "Auto 40°"), Map.entry("Auto30", "Auto 30°"), Map.entry("Eco4060", "Eco 40°-60°"),
|
||||
Map.entry("Power Speed59", "powerSpeed 59′"), Map.entry("Super1530", "Super 15′/30′"),
|
||||
Map.entry("Plus1", "+1"), Map.entry("Plus2", "+2"), Map.entry("Plus3", "+3"),
|
||||
Map.entry("Intensiv70", "Intensiv 70°"), Map.entry("Auto2", "Auto 45°-65°"), Map.entry("Eco50", "Eco 50°"),
|
||||
Map.entry("Glas40", "Glas 40°"), Map.entry("Quick45", "Quick 45′"), Map.entry("Kurz60", "Short 60°"),
|
||||
Map.entry("Auto1", "Auto 35°-45°"), Map.entry("Auto3", "Auto 65°-75°"),
|
||||
Map.entry(STATE_INTENSIVE_STAGE_OFF, "Off"), Map.entry(STATE_INTENSIVE_STAGE_1, "1"),
|
||||
Map.entry(STATE_INTENSIVE_STAGE_2, "2"), Map.entry(STATE_FAN_OFF, "Off"), Map.entry(STATE_FAN_STAGE_1, "1"),
|
||||
Map.entry(STATE_FAN_STAGE_2, "2"), Map.entry(STATE_FAN_STAGE_3, "3"), Map.entry(STATE_FAN_STAGE_4, "4"),
|
||||
Map.entry(STATE_FAN_STAGE_5, "5"));
|
||||
|
||||
private static final Pattern GC_PATTERN = Pattern.compile("GC(\\d{2})");
|
||||
private static final Pattern RPM_PATTERN = Pattern.compile("RPM(\\d+)");
|
||||
private static final Pattern UL_PREFIX_PATTERN = Pattern.compile("\\bUl\\s+");
|
||||
|
||||
private StringUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given string is {@code null}, empty or contains only whitespace.
|
||||
*
|
||||
* @param str the string to check (may be {@code null})
|
||||
* @return {@code true} if the string is {@code null}, empty or blank
|
||||
*/
|
||||
public static boolean isBlank(@Nullable String str) {
|
||||
return str == null || str.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe check whether the given string ends with the given suffix.
|
||||
*
|
||||
* @param str the string to check (may be {@code null})
|
||||
* @param suffix the suffix to look for (may be {@code null})
|
||||
* @return {@code true} if the string ends with the suffix, {@code false} if either argument is {@code null}
|
||||
*/
|
||||
public static boolean endsWith(@Nullable String str, @Nullable String suffix) {
|
||||
if (str == null || suffix == null) {
|
||||
return false;
|
||||
}
|
||||
return str.endsWith(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe check whether the given string starts with the given prefix.
|
||||
*
|
||||
* @param str the string to check (may be {@code null})
|
||||
* @param prefix the prefix to look for (may be {@code null})
|
||||
* @return {@code true} if the string starts with the prefix, {@code false} if either argument is {@code null}
|
||||
*/
|
||||
public static boolean startsWith(@Nullable String str, @Nullable String prefix) {
|
||||
if (str == null || prefix == null) {
|
||||
return false;
|
||||
}
|
||||
return str.startsWith(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the substring after the first occurrence of the given separator.
|
||||
*
|
||||
* @param str the string to extract from (may be {@code null})
|
||||
* @param separator the separator whose first occurrence marks the start of the result (may be {@code null})
|
||||
* @return the substring after the first occurrence of the separator, or an empty string if the string is
|
||||
* {@code null}/empty, the separator is {@code null} or the separator is not found
|
||||
*/
|
||||
public static String substringAfter(@Nullable String str, @Nullable String separator) {
|
||||
if (str == null || str.isEmpty() || separator == null) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
|
||||
int index = str.indexOf(separator);
|
||||
if (index == -1) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
|
||||
return str.substring(index + separator.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe, case-insensitive comparison of two strings.
|
||||
*
|
||||
* @param str1 the first string (may be {@code null})
|
||||
* @param str2 the second string (may be {@code null})
|
||||
* @return {@code true} if both are {@code null} or equal ignoring case, {@code false} otherwise
|
||||
*/
|
||||
public static boolean equalsIgnoreCase(@Nullable String str1, @Nullable String str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
return str1.equalsIgnoreCase(str2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe check whether the given string contains the given search sequence.
|
||||
*
|
||||
* @param str the string to search in (may be {@code null})
|
||||
* @param search the sequence to look for (may be {@code null})
|
||||
* @return {@code true} if the string contains the search sequence, {@code false} if either argument is {@code null}
|
||||
*/
|
||||
public static boolean contains(@Nullable String str, @Nullable String search) {
|
||||
if (str == null || search == null) {
|
||||
return false;
|
||||
}
|
||||
return str.contains(search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a camelCase string to kebab-case (e.g. {@code fooBar} becomes {@code foo-bar}).
|
||||
*
|
||||
* @param input the string to convert (may be {@code null})
|
||||
* @return the kebab-case representation, or an empty string if the input is {@code null} or blank
|
||||
*/
|
||||
public static String convertToKebabCase(@Nullable String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile("([a-z])([A-Z])");
|
||||
Matcher matcher = pattern.matcher(input);
|
||||
return matcher.replaceAll("$1-$2").toLowerCase(LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given string to lower case.
|
||||
*
|
||||
* @param input the string to convert (may be {@code null})
|
||||
* @return the lower-case representation, or an empty string if the input is {@code null} or blank
|
||||
*/
|
||||
public static String toLowercase(@Nullable String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
return input.toLowerCase(LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe, case-insensitive check whether the given string contains the given search sequence.
|
||||
*
|
||||
* @param str the string to search in (may be {@code null})
|
||||
* @param search the sequence to look for (may be {@code null})
|
||||
* @return {@code true} if the string contains the search sequence ignoring case, {@code false} if either argument
|
||||
* is
|
||||
* {@code null}
|
||||
*/
|
||||
public static boolean containsIgnoreCase(@Nullable String str, @Nullable String search) {
|
||||
if (str == null || search == null) {
|
||||
return false;
|
||||
}
|
||||
return str.toLowerCase(LOCALE).contains(search.toLowerCase(LOCALE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Home Connect key and value names to label (Using the translation provider if submitted).
|
||||
* e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
|
||||
* Start
|
||||
*
|
||||
* @param key key
|
||||
* @param translationProvider translation provider
|
||||
* @return human readable label
|
||||
*/
|
||||
public static String mapKeyToLabel(String key, @Nullable HomeConnectDirectTranslationProvider translationProvider) {
|
||||
if (translationProvider != null) {
|
||||
var translation = translationProvider.getText(key);
|
||||
if (!translation.equals(key)) {
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
|
||||
String sub = key.substring(key.lastIndexOf(".") + 1);
|
||||
String label = String.join(" ", CAMEL_CASE_SPLIT_PATTERN.split(sub));
|
||||
|
||||
label = LABEL_REPLACEMENTS.entrySet().stream().reduce(label,
|
||||
(res, entry) -> res.replace(entry.getKey(), entry.getValue()), (s1, s2) -> s1);
|
||||
|
||||
label = GC_PATTERN.matcher(label).replaceAll("$1 °C");
|
||||
label = RPM_PATTERN.matcher(label).replaceAll("$1");
|
||||
label = UL_PREFIX_PATTERN.matcher(label).replaceAll("");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristically check whether the given payload is likely a JSON object (starts with <code>{</code> and ends with
|
||||
* <code>}</code>).
|
||||
*
|
||||
* @param payload the payload to check
|
||||
* @return {@code true} if the trimmed payload looks like a JSON object
|
||||
*/
|
||||
public static boolean isMostLikelyAJsonObject(String payload) {
|
||||
var json = payload.trim();
|
||||
return (json.startsWith("{") && json.endsWith("}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristically check whether the given payload is likely a JSON array (starts with <code>[</code> and ends with
|
||||
* <code>]</code>).
|
||||
*
|
||||
* @param payload the payload to check
|
||||
* @return {@code true} if the trimmed payload looks like a JSON array
|
||||
*/
|
||||
public static boolean isMostLikelyAJsonArray(String payload) {
|
||||
var json = payload.trim();
|
||||
return (json.startsWith("[") && json.endsWith("]"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all characters that are not alphanumeric or one of {@code . _ -} from the given string.
|
||||
*
|
||||
* @param input the string to sanitize (may be {@code null})
|
||||
* @return the sanitized string, or an empty string if the input is {@code null} or blank
|
||||
*/
|
||||
public static String sanitize(@Nullable String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
return input.replaceAll("[^a-zA-Z0-9._-]", EMPTY_STRING);
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.xml.converter;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.thoughtworks.xstream.converters.Converter;
|
||||
import com.thoughtworks.xstream.converters.MarshallingContext;
|
||||
import com.thoughtworks.xstream.converters.UnmarshallingContext;
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
|
||||
|
||||
/**
|
||||
* Abstract base class for XStream XML converters.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractConverter<T, C> implements Converter {
|
||||
|
||||
private final Class<T> type;
|
||||
private final @Nullable Class<C> contextClass;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected AbstractConverter() {
|
||||
Type superClass = getClass().getGenericSuperclass();
|
||||
if (superClass instanceof ParameterizedType parameterizedType) {
|
||||
Type[] typeArguments = parameterizedType.getActualTypeArguments();
|
||||
this.type = (Class<T>) typeArguments[0];
|
||||
this.contextClass = typeArguments.length > 1 && typeArguments[1] instanceof Class<?> clazz
|
||||
&& !Void.class.equals(clazz) ? (Class<C>) clazz : null;
|
||||
} else {
|
||||
throw new IllegalStateException("AbstractConverter must be extended with type arguments");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public boolean canConvert(@Nullable Class type) {
|
||||
return this.type.equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void marshal(@Nullable Object o, @Nullable HierarchicalStreamWriter hierarchicalStreamWriter,
|
||||
@Nullable MarshallingContext marshallingContext) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @Nullable Object unmarshal(@Nullable HierarchicalStreamReader reader,
|
||||
@Nullable UnmarshallingContext context) {
|
||||
if (reader != null) {
|
||||
var ctxClass = contextClass;
|
||||
if (context != null && ctxClass != null) {
|
||||
return process(reader, ctxClass.cast(context.get(ctxClass.getName())));
|
||||
} else {
|
||||
return process(reader, null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract T process(HierarchicalStreamReader reader, @Nullable C contextObject);
|
||||
|
||||
protected Integer mapHexId(String hexIdString) {
|
||||
return mapHexId(hexIdString, 0);
|
||||
}
|
||||
|
||||
protected Integer mapHexId(String hexIdString, Integer defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(hexIdString, 16);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable Integer mapHexIdNullable(@Nullable String hexIdString) {
|
||||
if (hexIdString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(hexIdString, 16);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable Integer mapIntegerNullable(@Nullable String integer) {
|
||||
if (integer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(integer);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean mapBoolean(@Nullable String bool, boolean defaultValue) {
|
||||
if (bool == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return Boolean.parseBoolean(bool);
|
||||
}
|
||||
|
||||
protected @Nullable Number mapNumberNullable(@Nullable String doubleOrInteger) {
|
||||
if (doubleOrInteger == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!doubleOrInteger.contains(".")) {
|
||||
return Integer.parseInt(doubleOrInteger);
|
||||
}
|
||||
return Double.parseDouble(doubleOrInteger);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.common.xml.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception thrown when XML parsing fails.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ParseException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1_644_080_964_145_839_845L;
|
||||
|
||||
public ParseException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ParseException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.configuration;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectApplianceConfiguration} class represents the appliance configuration options.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectApplianceConfiguration {
|
||||
public String haId = EMPTY;
|
||||
public String address = EMPTY;
|
||||
public int connectionRetryDelay = 1;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectConfiguration} class represents the binding configuration.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectConfiguration {
|
||||
public boolean loginEnabled = false;
|
||||
public @Nullable String loginPassword = null;
|
||||
public int messageQueueSize = 300;
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.common.utils.StringUtils.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.util.StringUtils;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectMDNSDiscoveryParticipant} is responsible for discovering Home
|
||||
* Connect appliances in your network environment.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "discovery.homeconnectdirect")
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private static final String SERVICE_TYPE = "_homeconnect._tcp.local.";
|
||||
private static final String PROPERTY_ID = "id";
|
||||
private static final String PROPERTY_BRAND = "brand";
|
||||
private static final String PROPERTY_TYPE = "type";
|
||||
private static final String PROPERTY_VIB = "vib";
|
||||
private static final String PROPERTY_MAC = "mac";
|
||||
|
||||
private final Logger logger;
|
||||
private final ThingRegistry thingRegistry;
|
||||
|
||||
@Activate
|
||||
public HomeConnectDirectMDNSDiscoveryParticipant(@Reference ThingRegistry thingRegistry) {
|
||||
this.thingRegistry = thingRegistry;
|
||||
logger = LoggerFactory.getLogger(HomeConnectDirectMDNSDiscoveryParticipant.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return SUPPORTED_THING_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return SERVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DiscoveryResult createResult(final ServiceInfo serviceInfo) {
|
||||
var ipv4List = List.of(serviceInfo.getInet4Addresses());
|
||||
var haId = serviceInfo.getPropertyString(PROPERTY_ID);
|
||||
var type = serviceInfo.getPropertyString(PROPERTY_TYPE);
|
||||
var brand = serviceInfo.getPropertyString(PROPERTY_BRAND);
|
||||
var port = serviceInfo.getPort();
|
||||
var thingUID = mapThingUID(serviceInfo);
|
||||
|
||||
logger.trace("Found appliance. haId={}, addresses={}, port={}", haId, ipv4List, port);
|
||||
if (!ipv4List.isEmpty() && !isBlank(haId)
|
||||
&& (port == CONNECTION_TYPE_AES_PORT || port == CONNECTION_TYPE_TLS_PORT) && !isBlank(type)
|
||||
&& !isBlank(brand) && thingUID != null && !isAlreadyRegisteredThing(haId)) {
|
||||
var thingTypeUID = mapType(type);
|
||||
var friendlyName = getLabel(thingTypeUID,
|
||||
Objects.requireNonNull(StringUtils.capitalize(brand.toLowerCase(LOCALE))));
|
||||
|
||||
Map<String, Object> properties = Map.of(PROPERTY_HOME_APPLIANCE_ID, haId, PROPERTY_ADDRESS,
|
||||
ipv4List.getFirst().getHostAddress());
|
||||
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(friendlyName)
|
||||
.withRepresentationProperty(PROPERTY_HOME_APPLIANCE_ID).build();
|
||||
} else {
|
||||
logger.trace("Ignore device due to missing attributes or already registered (haId={}).", haId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ThingUID getThingUID(ServiceInfo serviceInfo) {
|
||||
return mapThingUID(serviceInfo);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ThingUID mapThingUID(ServiceInfo serviceInfo) {
|
||||
var haId = serviceInfo.getPropertyString(PROPERTY_ID);
|
||||
var type = serviceInfo.getPropertyString(PROPERTY_TYPE);
|
||||
var mac = serviceInfo.getPropertyString(PROPERTY_MAC);
|
||||
var vib = serviceInfo.getPropertyString(PROPERTY_VIB);
|
||||
var brand = serviceInfo.getPropertyString(PROPERTY_BRAND);
|
||||
|
||||
if (!isBlank(haId) && !isBlank(brand) && !isBlank(type) && !isBlank(mac) && !isBlank(vib)) {
|
||||
var idProposal = brand.toLowerCase(LOCALE) + "-" + vib.toLowerCase(LOCALE) + "-" + mac.toLowerCase(LOCALE);
|
||||
return new ThingUID(mapType(type), idProposal);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAlreadyRegisteredThing(String haId) {
|
||||
return thingRegistry.stream().filter(thing -> SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID()))
|
||||
.filter(thing -> thing.getConfiguration().containsKey(PROPERTY_HOME_APPLIANCE_ID))
|
||||
.anyMatch(thing -> equalsIgnoreCase(
|
||||
String.valueOf(thing.getConfiguration().get(PROPERTY_HOME_APPLIANCE_ID)), haId));
|
||||
}
|
||||
|
||||
private ThingTypeUID mapType(String type) {
|
||||
if (equalsIgnoreCase(type, APPLIANCE_TYPE_WASHER)) {
|
||||
return THING_TYPE_WASHER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_DRYER)) {
|
||||
return THING_TYPE_DRYER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_WASHER_AND_DRYER)) {
|
||||
return THING_TYPE_WASHER_DRYER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_DISHWASHER)) {
|
||||
return THING_TYPE_DISHWASHER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_COFFEE_MAKER)) {
|
||||
return THING_TYPE_COFFEE_MAKER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_COOK_PROCESSOR)) {
|
||||
return THING_TYPE_COOK_PROCESSOR;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_OVEN)) {
|
||||
return THING_TYPE_OVEN;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_WARMING_DRAWER)) {
|
||||
return THING_TYPE_WARMING_DRAWER;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_HOOD)) {
|
||||
return THING_TYPE_HOOD;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_COOKTOP_ALTERNATIVE)
|
||||
|| equalsIgnoreCase(type, APPLIANCE_TYPE_COOKTOP)) {
|
||||
return THING_TYPE_COOKTOP;
|
||||
} else if (equalsIgnoreCase(type, APPLIANCE_TYPE_FRIDGE_FREEZER)) {
|
||||
return THING_TYPE_FRIDGE_FREEZER;
|
||||
} else {
|
||||
return THING_TYPE_GENERIC;
|
||||
}
|
||||
}
|
||||
|
||||
private String getLabel(ThingTypeUID thingTypeUID, String brand) {
|
||||
if (THING_TYPE_WASHER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.washer.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_DRYER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.dryer.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_WASHER_DRYER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.washerdryer.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_DISHWASHER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.dishwasher.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_COFFEE_MAKER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.coffeemaker.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_COOK_PROCESSOR.equals(thingTypeUID)) {
|
||||
return "@text/appliance.cookprocessor.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_OVEN.equals(thingTypeUID)) {
|
||||
return "@text/appliance.oven.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_WARMING_DRAWER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.warmingdrawer.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_HOOD.equals(thingTypeUID)) {
|
||||
return "@text/appliance.hood.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_COOKTOP.equals(thingTypeUID)) {
|
||||
return "@text/appliance.cooktop.label [\"" + brand + "\"]";
|
||||
} else if (THING_TYPE_FRIDGE_FREEZER.equals(thingTypeUID)) {
|
||||
return "@text/appliance.fridgefreezer.label [\"" + brand + "\"]";
|
||||
} else {
|
||||
return "@text/appliance.generic.label [\"" + brand + "\"]";
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.factory;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.common.utils.ConfigurationUtils.getDeviceId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.utils.ConfigurationUtils;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.BaseHomeConnectDirectHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectCoffeeMakerHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectCooktopHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectDishwasherHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectFridgeFreezerHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectHoodHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectOvenHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.HomeConnectDirectWasherDryerHandler;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
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.cm.ConfigurationAdmin;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = CONFIGURATION_PID, service = ThingHandlerFactory.class)
|
||||
public class HomeConnectDirectHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final ApplianceProfileService applianceProfileService;
|
||||
private final HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider;
|
||||
private final HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider;
|
||||
private final HomeConnectDirectConfiguration configuration;
|
||||
private final HomeConnectDirectTranslationProvider translationProvider;
|
||||
|
||||
@Activate
|
||||
public HomeConnectDirectHandlerFactory(@Reference ApplianceProfileService applianceProfileService,
|
||||
@Reference HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider,
|
||||
@Reference HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
@Reference ConfigurationAdmin configurationAdmin, @Reference TranslationProvider translationProvider,
|
||||
@Reference LocaleProvider localeProvider) {
|
||||
this.applianceProfileService = applianceProfileService;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.commandDescriptionProvider = commandDescriptionProvider;
|
||||
this.configuration = ConfigurationUtils.getConfiguration(configurationAdmin);
|
||||
this.translationProvider = new HomeConnectDirectTranslationProvider(translationProvider, localeProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
var deviceId = getDeviceId();
|
||||
|
||||
if (THING_TYPE_WASHER.equals(thingTypeUID) || THING_TYPE_WASHER_DRYER.equals(thingTypeUID)
|
||||
|| THING_TYPE_DRYER.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectWasherDryerHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_DISHWASHER.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectDishwasherHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_COFFEE_MAKER.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectCoffeeMakerHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_OVEN.equals(thingTypeUID) || THING_TYPE_WARMING_DRAWER.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectOvenHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_HOOD.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectHoodHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_COOKTOP.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectCooktopHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else if (THING_TYPE_FRIDGE_FREEZER.equals(thingTypeUID)) {
|
||||
return new HomeConnectDirectFridgeFreezerHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
} else {
|
||||
return new BaseHomeConnectDirectHandler(thing, applianceProfileService, commandDescriptionProvider,
|
||||
stateDescriptionProvider, deviceId, configuration, translationProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1444
File diff suppressed because it is too large
Load Diff
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource.RO_ACTIVE_PROGRAM;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescriptionType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Action;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.data.ProgramData;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectCoffeeMakerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectCoffeeMakerHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
public HomeConnectDirectCoffeeMakerHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
|
||||
this.logger = LoggerFactory.getLogger(HomeConnectDirectCoffeeMakerHandler.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_COFFEE_MAKER_PROGRAM_COMMAND.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
|
||||
var selectedProgram = getKeyValueStore().get(SELECTED_PROGRAM_KEY);
|
||||
if (selectedProgram != null) {
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
if (deviceDescriptionService.getActiveProgram(true) != null) {
|
||||
mapProgramKey(selectedProgram).ifPresent(programUid -> send(Action.POST, RO_ACTIVE_PROGRAM,
|
||||
List.of(new ProgramData(programUid, null)), null, 1));
|
||||
} else {
|
||||
logger.warn(
|
||||
"The '{}' control is either unavailable or in read-only mode. Cannot start program.",
|
||||
ACTIVE_PROGRAM_KEY);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
|
||||
sendBooleanCommandIfAllowed(ABORT_PROGRAM_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
if (DeviceDescriptionType.COMMAND.equals(deviceDescriptionChange.type())
|
||||
|| DeviceDescriptionType.COMMAND_LIST.equals(deviceDescriptionChange.type())) {
|
||||
updateProgramCommandDescription();
|
||||
} else if (deviceDescriptionChange.key().equals(COFFEE_MAKER_PROCESS_PHASE_KEY)) {
|
||||
updateStatusDescriptionIfLinked(CHANNEL_COFFEE_MAKER_PROCESS_PHASE, COFFEE_MAKER_PROCESS_PHASE_KEY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case COFFEE_MAKER_PROCESS_PHASE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_COFFEE_MAKER_PROCESS_PHASE, new StringType(value.getValueAsString()));
|
||||
case COFFEE_MAKER_COUNTDOWN_CLEANING_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_COUNTDOWN_CLEANING,
|
||||
() -> new DecimalType(value.getValueAsInt()));
|
||||
case COFFEE_MAKER_COUNTDOWN_CALC_AND_CLEAN_KEY -> updateStateIfLinked(
|
||||
CHANNEL_COFFEE_MAKER_COUNTDOWN_CALC_AND_CLEAN, () -> new DecimalType(value.getValueAsInt()));
|
||||
case COFFEE_MAKER_COUNTDOWN_DESCALING_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_COUNTDOWN_DESCALING,
|
||||
() -> new DecimalType(value.getValueAsInt()));
|
||||
case COFFEE_MAKER_COUNTDOWN_WATER_FILTER_KEY -> updateStateIfLinked(
|
||||
CHANNEL_COFFEE_MAKER_COUNTDOWN_WATER_FILTER, () -> new DecimalType(value.getValueAsInt()));
|
||||
case COFFEE_MAKER_WATER_TANK_EMPTY_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_WATER_TANK_EMPTY,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY_KEY ->
|
||||
updateStateIfLinked(CHANNEL_COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case COFFEE_MAKER_DRIP_TRAY_FULL_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_DRIP_TRAY_FULL,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case COFFEE_MAKER_EMPTY_MILK_TANK_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_EMPTY_MILK_TANK,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case COFFEE_MAKER_BEAN_CONTAINER_EMPTY_KEY -> updateStateIfLinked(CHANNEL_COFFEE_MAKER_BEAN_CONTAINER_EMPTY,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case SELECTED_PROGRAM_KEY, ACTIVE_PROGRAM_KEY -> updateProgramCommandDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
Set.of(CHANNEL_COFFEE_MAKER_PROCESS_PHASE, CHANNEL_COFFEE_MAKER_WATER_TANK_EMPTY,
|
||||
CHANNEL_COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY, CHANNEL_COFFEE_MAKER_DRIP_TRAY_FULL,
|
||||
CHANNEL_COFFEE_MAKER_EMPTY_MILK_TANK, CHANNEL_COFFEE_MAKER_BEAN_CONTAINER_EMPTY,
|
||||
CHANNEL_COFFEE_MAKER_PROGRAM_COMMAND).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
switch (channelId) {
|
||||
case CHANNEL_COFFEE_MAKER_PROCESS_PHASE ->
|
||||
updateStatusDescriptionIfLinked(channelId, COFFEE_MAKER_PROCESS_PHASE_KEY);
|
||||
case CHANNEL_COFFEE_MAKER_PROGRAM_COMMAND -> updateProgramCommandDescription();
|
||||
case CHANNEL_COFFEE_MAKER_WATER_TANK_EMPTY, CHANNEL_COFFEE_MAKER_WATER_TANK_NEARLY_EMPTY,
|
||||
CHANNEL_COFFEE_MAKER_DRIP_TRAY_FULL, CHANNEL_COFFEE_MAKER_EMPTY_MILK_TANK,
|
||||
CHANNEL_COFFEE_MAKER_BEAN_CONTAINER_EMPTY ->
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgramCommandDescription() {
|
||||
getLinkedChannel(CHANNEL_COFFEE_MAKER_PROGRAM_COMMAND).ifPresent(channel -> {
|
||||
var commandOptions = new ArrayList<CommandOption>();
|
||||
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
if (deviceDescriptionService.isCommandAvailableAndWritable(ABORT_PROGRAM_KEY)) {
|
||||
commandOptions
|
||||
.add(new CommandOption(COMMAND_STOP, getTranslationProvider().getText(I18N_STOP_PROGRAM)));
|
||||
} else {
|
||||
commandOptions.add(
|
||||
new CommandOption(COMMAND_START, getTranslationProvider().getText(I18N_START_PROGRAM)));
|
||||
}
|
||||
});
|
||||
|
||||
setCommandOptions(channel.getUID(), commandOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.CHANNEL_BUTTON_TONES;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.COOKING_BUTTON_TONES_KEY;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectCooktopHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectCooktopHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
public HomeConnectDirectCooktopHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_BUTTON_TONES.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanSettingIfAllowed(command, COOKING_BUTTON_TONES_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
if (value.key().equals(COOKING_BUTTON_TONES_KEY)) {
|
||||
updateStateIfLinked(CHANNEL_BUTTON_TONES, OnOffType.from(value.getValueAsBoolean()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
initializeState(CHANNEL_BUTTON_TONES);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
if (CHANNEL_BUTTON_TONES.equals(channelId)) {
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectDishwasherHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectDishwasherHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
public HomeConnectDirectDishwasherHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_DISHWASHER_VARIO_SPEED_PLUS.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, DISHWASHER_VARIO_SPEED_PLUS_KEY);
|
||||
} else if (CHANNEL_DISHWASHER_INTENSIV_ZONE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, DISHWASHER_INTENSIV_ZONE_KEY);
|
||||
} else if (CHANNEL_DISHWASHER_BRILLIANCE_DRY.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, DISHWASHER_BRILLIANCE_DRY_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
if (deviceDescriptionChange.key().equals(DISHWASHER_PROGRAM_PHASE_KEY)) {
|
||||
updateStatusDescriptionIfLinked(CHANNEL_DISHWASHER_PROGRAM_PHASE, DISHWASHER_PROGRAM_PHASE_KEY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case DISHWASHER_PROGRAM_PHASE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_DISHWASHER_PROGRAM_PHASE, new StringType(value.getValueAsString()));
|
||||
case DISHWASHER_SALT_LACK_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_SALT_LACK,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DISHWASHER_RINSE_AID_LACK_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_RINSE_AID_LACK,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DISHWASHER_SALT_NEARLY_EMPTY_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_SALT_NEARLY_EMPTY,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DISHWASHER_RINSE_AID_NEARLY_EMPTY_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_RINSE_AID_NEARLY_EMPTY,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DISHWASHER_MACHINE_CARE_REMINDER_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_MACHINE_CARE_REMINDER,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DISHWASHER_VARIO_SPEED_PLUS_KEY -> updateStateIfLinked(CHANNEL_DISHWASHER_VARIO_SPEED_PLUS,
|
||||
() -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case DISHWASHER_INTENSIV_ZONE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_DISHWASHER_INTENSIV_ZONE, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case DISHWASHER_BRILLIANCE_DRY_KEY ->
|
||||
updateStateIfLinked(CHANNEL_DISHWASHER_BRILLIANCE_DRY, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
Arrays.asList(CHANNEL_DISHWASHER_PROGRAM_PHASE, CHANNEL_DISHWASHER_SALT_LACK, CHANNEL_DISHWASHER_RINSE_AID_LACK,
|
||||
CHANNEL_DISHWASHER_SALT_NEARLY_EMPTY, CHANNEL_DISHWASHER_RINSE_AID_NEARLY_EMPTY,
|
||||
CHANNEL_DISHWASHER_MACHINE_CARE_REMINDER, CHANNEL_DISHWASHER_VARIO_SPEED_PLUS,
|
||||
CHANNEL_DISHWASHER_INTENSIV_ZONE, CHANNEL_DISHWASHER_BRILLIANCE_DRY).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
switch (channelId) {
|
||||
case CHANNEL_DISHWASHER_PROGRAM_PHASE ->
|
||||
updateStatusDescriptionIfLinked(channelId, DISHWASHER_PROGRAM_PHASE_KEY);
|
||||
case CHANNEL_DISHWASHER_SALT_LACK, CHANNEL_DISHWASHER_RINSE_AID_LACK, CHANNEL_DISHWASHER_SALT_NEARLY_EMPTY,
|
||||
CHANNEL_DISHWASHER_RINSE_AID_NEARLY_EMPTY, CHANNEL_DISHWASHER_MACHINE_CARE_REMINDER,
|
||||
CHANNEL_DISHWASHER_VARIO_SPEED_PLUS, CHANNEL_DISHWASHER_INTENSIV_ZONE,
|
||||
CHANNEL_DISHWASHER_BRILLIANCE_DRY ->
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
import static org.openhab.core.library.unit.Units.PERCENT;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.DeviceDescriptionService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectFridgeFreezerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectFridgeFreezerHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
private static final String FREEZER = "Freezer";
|
||||
private static final String FRIDGE = "Fridge";
|
||||
private static final String CHILLER = "Chiller";
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
public HomeConnectDirectFridgeFreezerHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
|
||||
this.logger = LoggerFactory.getLogger(HomeConnectDirectFridgeFreezerHandler.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeStarted() {
|
||||
addDynamicChannels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_FRIDGE_SUPER_MODE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
if (settingKeyExists(FRIDGE_SUPER_MODE_2_KEY)) {
|
||||
sendBooleanSettingIfAllowed(command, FRIDGE_SUPER_MODE_2_KEY);
|
||||
} else {
|
||||
sendBooleanSettingIfAllowed(command, FRIDGE_SUPER_MODE_KEY);
|
||||
}
|
||||
} else if (CHANNEL_FREEZER_SUPER_MODE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
if (settingKeyExists(FREEZER_SUPER_MODE_2_KEY)) {
|
||||
sendBooleanSettingIfAllowed(command, FREEZER_SUPER_MODE_2_KEY);
|
||||
} else {
|
||||
sendBooleanSettingIfAllowed(command, FREEZER_SUPER_MODE_KEY);
|
||||
}
|
||||
} else if (CHANNEL_FRIDGE_SET_POINT_TEMPERATURE.equals(channelUID.getId())
|
||||
&& (command instanceof QuantityType<?> || command instanceof DecimalType)) {
|
||||
if (settingKeyExists(FRIDGE_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
sendTemperatureSetting(FRIDGE_SET_POINT_TEMPERATURE_2_KEY, command);
|
||||
} else {
|
||||
sendTemperatureSetting(FRIDGE_SET_POINT_TEMPERATURE_KEY, command);
|
||||
}
|
||||
} else if (CHANNEL_FREEZER_SET_POINT_TEMPERATURE.equals(channelUID.getId())
|
||||
&& (command instanceof QuantityType<?> || command instanceof DecimalType)) {
|
||||
if (settingKeyExists(FREEZER_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
sendTemperatureSetting(FREEZER_SET_POINT_TEMPERATURE_2_KEY, command);
|
||||
} else {
|
||||
sendTemperatureSetting(FREEZER_SET_POINT_TEMPERATURE_KEY, command);
|
||||
}
|
||||
} else if (CHANNEL_FRIDGE_CHILLER_SET_POINT_TEMPERATURE.equals(channelUID.getId())
|
||||
&& (command instanceof QuantityType<?> || command instanceof DecimalType)) {
|
||||
if (settingKeyExists(FRIDGE_CHILLER_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
sendTemperatureSetting(FRIDGE_CHILLER_SET_POINT_TEMPERATURE_2_KEY, command);
|
||||
} else {
|
||||
sendTemperatureSetting(FRIDGE_CHILLER_SET_POINT_TEMPERATURE_KEY, command);
|
||||
}
|
||||
} else if (CHANNEL_FRIDGE_CHILLER_PRESET.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumSettingIfAllowed(command, FRIDGE_CHILLER_PRESET_KEY);
|
||||
} else if (CHANNEL_FRIDGE_DISPENSER_ENABLED.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanSettingIfAllowed(command, FRIDGE_DISPENSER_ENABLED_KEY);
|
||||
} else if (CHANNEL_FRIDGE_DISPENSER_PARTY_MODE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanSettingIfAllowed(command, FRIDGE_DISPENSER_PARTY_MODE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
var key = deviceDescriptionChange.key();
|
||||
if (key.equals(FRIDGE_CHILLER_PRESET_KEY)) {
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_FRIDGE_CHILLER_PRESET, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case FRIDGE_SET_POINT_TEMPERATURE_KEY, FRIDGE_SET_POINT_TEMPERATURE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_SET_POINT_TEMPERATURE,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), getTemperatureUnitOfSetting(value.key())));
|
||||
case FREEZER_SET_POINT_TEMPERATURE_KEY, FREEZER_SET_POINT_TEMPERATURE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FREEZER_SET_POINT_TEMPERATURE,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), getTemperatureUnitOfSetting(value.key())));
|
||||
case FRIDGE_CHILLER_SET_POINT_TEMPERATURE_KEY, FRIDGE_CHILLER_SET_POINT_TEMPERATURE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_CHILLER_SET_POINT_TEMPERATURE,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), getTemperatureUnitOfSetting(value.key())));
|
||||
case FRIDGE_SUPER_MODE_KEY, FRIDGE_SUPER_MODE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_SUPER_MODE, OnOffType.from(value.getValueAsBoolean()));
|
||||
case FREEZER_SUPER_MODE_KEY, FREEZER_SUPER_MODE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FREEZER_SUPER_MODE, OnOffType.from(value.getValueAsBoolean()));
|
||||
case FRIDGE_DOOR_STATE_KEY,
|
||||
FRIDGE_DOOR_STATE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_DOOR_STATE,
|
||||
() -> STATE_OPEN.equals(value.value()) || STATE_AJAR.equals(value.value()) ? OpenClosedType.OPEN
|
||||
: OpenClosedType.CLOSED);
|
||||
case FREEZER_DOOR_STATE_KEY,
|
||||
FREEZER_DOOR_STATE_2_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FREEZER_DOOR_STATE,
|
||||
() -> STATE_OPEN.equals(value.value()) || STATE_AJAR.equals(value.value()) ? OpenClosedType.OPEN
|
||||
: OpenClosedType.CLOSED);
|
||||
case FRIDGE_CHILLER_DOOR_STATE_KEY -> updateStateIfLinked(CHANNEL_FRIDGE_CHILLER_DOOR_STATE,
|
||||
() -> STATE_OPEN.equals(value.value()) || STATE_AJAR.equals(value.value()) ? OpenClosedType.OPEN
|
||||
: OpenClosedType.CLOSED);
|
||||
case FRIDGE_DISPENSER_WATER_FILTER_SATURATION_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_DISPENSER_WATER_FILTER_SATURATION,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), PERCENT));
|
||||
case FRIDGE_CHILLER_PRESET_KEY -> getLinkedChannel(CHANNEL_FRIDGE_CHILLER_PRESET)
|
||||
.ifPresent(channel -> updateState(channel.getUID(), new StringType(value.getValueAsString())));
|
||||
case FRIDGE_DISPENSER_ENABLED_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_DISPENSER_ENABLED, OnOffType.from(value.getValueAsBoolean()));
|
||||
case FRIDGE_DISPENSER_PARTY_MODE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_FRIDGE_DISPENSER_PARTY_MODE, OnOffType.from(value.getValueAsBoolean()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
Set.of(CHANNEL_FRIDGE_SUPER_MODE, CHANNEL_FREEZER_SUPER_MODE, CHANNEL_FRIDGE_DOOR_STATE,
|
||||
CHANNEL_FREEZER_DOOR_STATE, CHANNEL_FRIDGE_CHILLER_DOOR_STATE,
|
||||
CHANNEL_FRIDGE_DISPENSER_WATER_FILTER_SATURATION, CHANNEL_FRIDGE_CHILLER_PRESET,
|
||||
CHANNEL_FRIDGE_DISPENSER_PARTY_MODE, CHANNEL_FRIDGE_DISPENSER_ENABLED).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
switch (channelId) {
|
||||
case CHANNEL_FRIDGE_SUPER_MODE, CHANNEL_FREEZER_SUPER_MODE, CHANNEL_FRIDGE_DISPENSER_PARTY_MODE,
|
||||
CHANNEL_FRIDGE_DISPENSER_ENABLED ->
|
||||
updateState(channelId, OnOffType.OFF);
|
||||
case CHANNEL_FRIDGE_DOOR_STATE, CHANNEL_FREEZER_DOOR_STATE, CHANNEL_FRIDGE_CHILLER_DOOR_STATE ->
|
||||
updateState(channelId, OpenClosedType.CLOSED);
|
||||
case CHANNEL_FRIDGE_DISPENSER_WATER_FILTER_SATURATION ->
|
||||
updateState(channelId, new QuantityType<>(0, PERCENT));
|
||||
case CHANNEL_FRIDGE_CHILLER_PRESET ->
|
||||
updateEnumOptionDescriptionIfLinked(channelId, FRIDGE_CHILLER_PRESET_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
private void addDynamicChannels() {
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
var thingBuilder = editThing();
|
||||
boolean channelsChanged = false;
|
||||
|
||||
// setpoint temperatures
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_SET_POINT_TEMPERATURE_KEY)
|
||||
|| settingKeyExists(deviceDescriptionService, FRIDGE_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_SET_POINT_TEMPERATURE,
|
||||
CHANNEL_TYPE_FRIDGE_FREEZER_SET_POINT_TEMPERATURE, NUMBER_TEMPERATURE,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_FREEZER_SET_POINT_TEMPERATURE,
|
||||
getTranslationProvider().getText(FRIDGE)));
|
||||
}
|
||||
|
||||
if (settingKeyExists(deviceDescriptionService, FREEZER_SET_POINT_TEMPERATURE_KEY)
|
||||
|| settingKeyExists(deviceDescriptionService, FREEZER_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FREEZER_SET_POINT_TEMPERATURE,
|
||||
CHANNEL_TYPE_FRIDGE_FREEZER_SET_POINT_TEMPERATURE, NUMBER_TEMPERATURE,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_FREEZER_SET_POINT_TEMPERATURE,
|
||||
getTranslationProvider().getText(FREEZER)));
|
||||
}
|
||||
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_CHILLER_SET_POINT_TEMPERATURE_KEY)
|
||||
|| settingKeyExists(deviceDescriptionService, FRIDGE_CHILLER_SET_POINT_TEMPERATURE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_CHILLER_SET_POINT_TEMPERATURE,
|
||||
CHANNEL_TYPE_FRIDGE_FREEZER_SET_POINT_TEMPERATURE, NUMBER_TEMPERATURE,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_FREEZER_SET_POINT_TEMPERATURE,
|
||||
getTranslationProvider().getText(CHILLER)));
|
||||
}
|
||||
|
||||
// door states
|
||||
var doorAdded = false;
|
||||
if (statusKeyExists(deviceDescriptionService, FRIDGE_DOOR_STATE_KEY)
|
||||
|| statusKeyExists(deviceDescriptionService, FRIDGE_DOOR_STATE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_DOOR_STATE, CHANNEL_TYPE_DOOR,
|
||||
CoreItemFactory.CONTACT, getTranslationProvider().getText(I18N_FRIDGE_FREEZER_DOOR,
|
||||
getTranslationProvider().getText(FRIDGE)));
|
||||
doorAdded = true;
|
||||
}
|
||||
if (statusKeyExists(deviceDescriptionService, FRIDGE_CHILLER_DOOR_STATE_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_CHILLER_DOOR_STATE,
|
||||
CHANNEL_TYPE_DOOR, CoreItemFactory.CONTACT, getTranslationProvider()
|
||||
.getText(I18N_FRIDGE_FREEZER_DOOR, getTranslationProvider().getText(CHILLER)));
|
||||
doorAdded = true;
|
||||
}
|
||||
if (statusKeyExists(deviceDescriptionService, FREEZER_DOOR_STATE_KEY)
|
||||
|| statusKeyExists(deviceDescriptionService, FREEZER_DOOR_STATE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FREEZER_DOOR_STATE, CHANNEL_TYPE_DOOR,
|
||||
CoreItemFactory.CONTACT, getTranslationProvider().getText(I18N_FRIDGE_FREEZER_DOOR,
|
||||
getTranslationProvider().getText(FREEZER)));
|
||||
doorAdded = true;
|
||||
}
|
||||
|
||||
if (!doorAdded) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_DOOR, CHANNEL_TYPE_DOOR,
|
||||
CoreItemFactory.CONTACT, getTranslationProvider().getText(I18N_DOOR));
|
||||
}
|
||||
|
||||
// super modes
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_SUPER_MODE_KEY)
|
||||
|| settingKeyExists(deviceDescriptionService, FRIDGE_SUPER_MODE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_SUPER_MODE,
|
||||
CHANNEL_TYPE_FRIDGE_FREEZER_SUPER_MODE, CoreItemFactory.SWITCH, getTranslationProvider()
|
||||
.getText(I18N_FRIDGE_FREEZER_SUPER_MODE, getTranslationProvider().getText(FRIDGE)));
|
||||
}
|
||||
|
||||
if (settingKeyExists(deviceDescriptionService, FREEZER_SUPER_MODE_KEY)
|
||||
|| settingKeyExists(deviceDescriptionService, FREEZER_SUPER_MODE_2_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FREEZER_SUPER_MODE,
|
||||
CHANNEL_TYPE_FRIDGE_FREEZER_SUPER_MODE, CoreItemFactory.SWITCH, getTranslationProvider()
|
||||
.getText(I18N_FRIDGE_FREEZER_SUPER_MODE, getTranslationProvider().getText(FREEZER)));
|
||||
}
|
||||
|
||||
// dispenser
|
||||
if (statusKeyExists(deviceDescriptionService, FRIDGE_DISPENSER_WATER_FILTER_SATURATION_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_DISPENSER_WATER_FILTER_SATURATION,
|
||||
CHANNEL_TYPE_FRIDGE_DISPENSER_WATER_FILTER_SATURATION, NUMBER_DIMENSIONLESS,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_DISPENSER_WATER_FILTER_SATURATION));
|
||||
}
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_DISPENSER_ENABLED_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_DISPENSER_ENABLED,
|
||||
CHANNEL_TYPE_FRIDGE_DISPENSER_ENABLED, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_DISPENSER_ENABLED));
|
||||
}
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_DISPENSER_PARTY_MODE_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_DISPENSER_PARTY_MODE,
|
||||
CHANNEL_TYPE_FRIDGE_DISPENSER_PARTY_MODE, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_DISPENSER_PARTY_MODE));
|
||||
}
|
||||
|
||||
// chiller preset
|
||||
if (settingKeyExists(deviceDescriptionService, FRIDGE_CHILLER_PRESET_KEY)) {
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_FRIDGE_CHILLER_PRESET,
|
||||
CHANNEL_TYPE_FRIDGE_CHILLER_PRESET, CoreItemFactory.STRING,
|
||||
getTranslationProvider().getText(I18N_FRIDGE_CHILLER_PRESET));
|
||||
}
|
||||
|
||||
// update channels
|
||||
if (channelsChanged) {
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Unit<Temperature> getTemperatureUnitOfSetting(String settingKey) {
|
||||
var unit = getDeviceDescriptionServiceOptional().map(deviceDescriptionService -> {
|
||||
var setting = deviceDescriptionService.findSettingByKey(settingKey);
|
||||
if (setting != null) {
|
||||
return ContentType.TEMPERATURE_CELSIUS.equals(setting.contentType()) ? CELSIUS : FAHRENHEIT;
|
||||
}
|
||||
return CELSIUS;
|
||||
}).orElse(null);
|
||||
return Objects.requireNonNullElse(unit, CELSIUS);
|
||||
}
|
||||
|
||||
private void sendTemperatureSetting(String settingKey, Command command) {
|
||||
var unit = getTemperatureUnitOfSetting(settingKey);
|
||||
QuantityType<?> quantity;
|
||||
if (command instanceof QuantityType<?> qt) {
|
||||
quantity = qt;
|
||||
} else if (command instanceof DecimalType dt) {
|
||||
quantity = new QuantityType<>(dt, unit);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported command type: " + command.getClass().getName() + " for setting " + settingKey);
|
||||
}
|
||||
var temperatureQuantityType = quantity.toUnit(unit);
|
||||
if (temperatureQuantityType != null) {
|
||||
sendIntegerSettingIfAllowed(temperatureQuantityType, settingKey);
|
||||
} else {
|
||||
logger.warn("Could not set temperature! uid={}", getThing().getUID());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settingKeyExists(String settingKey) {
|
||||
var deviceDescriptionService = getDeviceDescriptionService();
|
||||
return deviceDescriptionService != null && settingKeyExists(deviceDescriptionService, settingKey);
|
||||
}
|
||||
|
||||
private boolean settingKeyExists(DeviceDescriptionService deviceDescriptionService, String settingKey) {
|
||||
return deviceDescriptionService.getSetting(settingKey, false, false, false) != null;
|
||||
}
|
||||
|
||||
private boolean statusKeyExists(DeviceDescriptionService deviceDescriptionService, String statusKey) {
|
||||
return deviceDescriptionService.getStatus(statusKey, false, false, false) != null;
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource.RO_ACTIVE_PROGRAM;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource.RO_VALUES;
|
||||
import static org.openhab.core.library.unit.Units.PERCENT;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Action;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.data.ProgramData;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.data.ValueData;
|
||||
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.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectHoodHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectHoodHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
public HomeConnectDirectHoodHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
|
||||
this.logger = LoggerFactory.getLogger(HomeConnectDirectHoodHandler.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_COOKING_LIGHT.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanSettingIfAllowed(command, COOKING_LIGHTING_KEY);
|
||||
} else if (CHANNEL_COOKING_LIGHT_BRIGHTNESS.equals(channelUID.getId()) && command instanceof Number quantity) {
|
||||
mapSettingKey(COOKING_LIGHTING_BRIGHTNESS_KEY).ifPresent(optionUid -> {
|
||||
double value = quantity.doubleValue();
|
||||
|
||||
// percent value
|
||||
if (value > 0 && value < 1) {
|
||||
value *= 100;
|
||||
}
|
||||
if (value > 100) {
|
||||
value = 100;
|
||||
}
|
||||
send(Action.POST, RO_VALUES, List.of(new ValueData(optionUid, (int) value)), null, 1);
|
||||
});
|
||||
} else if (CHANNEL_BUTTON_TONES.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanSettingIfAllowed(command, COOKING_BUTTON_TONES_KEY);
|
||||
} else if (CHANNEL_HOOD_VENTING_LEVEL.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
var programUid = mapProgramKey(STATE_HOOD_VENTING);
|
||||
var optionUid = mapOptionKey(HOOD_VENTING_LEVEL_KEY);
|
||||
var optionEnumValue = mapEnumerationValueKey(HOOD_VENTING_LEVEL_ENUM_KEY, command.toFullString());
|
||||
|
||||
logger.debug("Setting up venting level state. programUid={} optionUid={}, optionEnumValue={}", programUid,
|
||||
optionUid, optionEnumValue);
|
||||
if (Stream.of(programUid, optionUid, optionEnumValue).allMatch(Optional::isPresent)) {
|
||||
send(Action.POST, RO_ACTIVE_PROGRAM, List.of(new ProgramData(programUid.get(),
|
||||
List.of(new ValueData(optionUid.get(), optionEnumValue.get())))), null, 1);
|
||||
}
|
||||
} else if (CHANNEL_HOOD_INTENSIVE_LEVEL.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
var programUid = mapProgramKey(STATE_HOOD_VENTING);
|
||||
var optionUid = mapOptionKey(HOOD_INTENSIVE_LEVEL_KEY);
|
||||
var optionEnumValue = mapEnumerationValueKey(HOOD_INTENSIVE_LEVEL_ENUM_KEY, command.toFullString());
|
||||
|
||||
logger.debug("Setting up intensive level state. programUid={} optionUid={}, optionEnumValue={}", programUid,
|
||||
optionUid, optionEnumValue);
|
||||
if (Stream.of(programUid, optionUid, optionEnumValue).allMatch(Optional::isPresent)) {
|
||||
send(Action.POST, RO_ACTIVE_PROGRAM, List.of(new ProgramData(programUid.get(),
|
||||
List.of(new ValueData(optionUid.get(), optionEnumValue.get())))), null, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
var key = deviceDescriptionChange.key();
|
||||
switch (key) {
|
||||
case HOOD_VENTING_LEVEL_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_HOOD_VENTING_LEVEL, key);
|
||||
case HOOD_INTENSIVE_LEVEL_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_HOOD_INTENSIVE_LEVEL, key);
|
||||
case ROOT_OPTION_LIST_KEY -> {
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_HOOD_VENTING_LEVEL, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_HOOD_INTENSIVE_LEVEL, key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case COOKING_LIGHTING_KEY ->
|
||||
updateStateIfLinked(CHANNEL_COOKING_LIGHT, OnOffType.from(value.getValueAsBoolean()));
|
||||
case COOKING_LIGHTING_BRIGHTNESS_KEY -> updateStateIfLinked(CHANNEL_COOKING_LIGHT_BRIGHTNESS,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), PERCENT));
|
||||
case COOKING_BUTTON_TONES_KEY ->
|
||||
updateStateIfLinked(CHANNEL_BUTTON_TONES, OnOffType.from(value.getValueAsBoolean()));
|
||||
case HOOD_VENTING_LEVEL_KEY -> {
|
||||
updateStateIfLinked(CHANNEL_HOOD_VENTING_LEVEL, new StringType(value.getValueAsString()));
|
||||
if (!STATE_FAN_OFF.equals(value.value())) {
|
||||
updateStateIfLinked(CHANNEL_HOOD_INTENSIVE_LEVEL, new StringType(STATE_INTENSIVE_STAGE_OFF));
|
||||
}
|
||||
}
|
||||
case HOOD_INTENSIVE_LEVEL_KEY -> {
|
||||
updateStateIfLinked(CHANNEL_HOOD_INTENSIVE_LEVEL, new StringType(value.getValueAsString()));
|
||||
if (!STATE_INTENSIVE_STAGE_OFF.equals(value.value())) {
|
||||
updateStateIfLinked(CHANNEL_HOOD_VENTING_LEVEL, new StringType(STATE_FAN_OFF));
|
||||
}
|
||||
}
|
||||
case POWER_STATE_KEY -> {
|
||||
if (STATE_OFF.equalsIgnoreCase(value.getValueAsString())) {
|
||||
updateStateIfLinked(CHANNEL_HOOD_VENTING_LEVEL, new StringType(STATE_FAN_OFF));
|
||||
updateStateIfLinked(CHANNEL_HOOD_INTENSIVE_LEVEL, new StringType(STATE_INTENSIVE_STAGE_OFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
Set.of(CHANNEL_BUTTON_TONES, CHANNEL_HOOD_VENTING_LEVEL, CHANNEL_HOOD_INTENSIVE_LEVEL, CHANNEL_COOKING_LIGHT,
|
||||
CHANNEL_COOKING_LIGHT_BRIGHTNESS).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
switch (channelId) {
|
||||
case CHANNEL_BUTTON_TONES, CHANNEL_COOKING_LIGHT -> updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
case CHANNEL_HOOD_VENTING_LEVEL -> {
|
||||
updateState(channelId, new StringType(STATE_FAN_OFF));
|
||||
updateEnumOptionDescriptionIfLinked(channelId, HOOD_VENTING_LEVEL_KEY);
|
||||
}
|
||||
case CHANNEL_HOOD_INTENSIVE_LEVEL -> {
|
||||
updateState(channelId, new StringType(STATE_INTENSIVE_STAGE_OFF));
|
||||
updateEnumOptionDescriptionIfLinked(channelId, HOOD_INTENSIVE_LEVEL_KEY);
|
||||
}
|
||||
case CHANNEL_COOKING_LIGHT_BRIGHTNESS -> updateStateIfLinked(channelId, new QuantityType<>(0, PERCENT));
|
||||
}
|
||||
}
|
||||
}
|
||||
+537
@@ -0,0 +1,537 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource.RO_ACTIVE_PROGRAM;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource.RO_ALL_MANDATORY_VALUES;
|
||||
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
import static org.openhab.core.library.unit.Units.SECOND;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.DynamicChannel;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.DeviceDescriptionService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescriptionType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Enumeration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Action;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.data.ProgramData;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectOvenHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectOvenHandler extends BaseHomeConnectDirectHandler {
|
||||
|
||||
private static final String DEFAULT_CAVITY_NAME = "Main";
|
||||
private static final int DEFAULT_CAVITY_INDEX = 0;
|
||||
private static final long POLLING_INTERVAL_RUN_SECONDS = 60;
|
||||
private static final long POLLING_INTERVAL_COOLDOWN_SECONDS = 300;
|
||||
private static final long POLLING_INTERVAL_IDLE_SECONDS = 900;
|
||||
private static final int TEMPERATURE_THRESHOLD_HIGH = 50;
|
||||
private static final int TEMPERATURE_THRESHOLD_LOW = 30;
|
||||
|
||||
private final Logger logger;
|
||||
private final CopyOnWriteArraySet<DynamicChannel> doorChannels;
|
||||
private final CopyOnWriteArraySet<DynamicChannel> currentTemperatureChannels;
|
||||
private final CopyOnWriteArraySet<DynamicChannel> meatProbeChannels;
|
||||
private final CopyOnWriteArraySet<DynamicChannel> lightChannels;
|
||||
private final CopyOnWriteArraySet<DynamicChannel> meatProbePluggedChannels;
|
||||
private final AtomicInteger maxCavityTemperature;
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingFuture;
|
||||
private long currentPollingIntervalSeconds;
|
||||
|
||||
public HomeConnectDirectOvenHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
|
||||
this.logger = LoggerFactory.getLogger(HomeConnectDirectOvenHandler.class);
|
||||
this.doorChannels = new CopyOnWriteArraySet<>();
|
||||
this.currentTemperatureChannels = new CopyOnWriteArraySet<>();
|
||||
this.meatProbeChannels = new CopyOnWriteArraySet<>();
|
||||
this.lightChannels = new CopyOnWriteArraySet<>();
|
||||
this.meatProbePluggedChannels = new CopyOnWriteArraySet<>();
|
||||
this.maxCavityTemperature = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeStarted() {
|
||||
addCavityChannels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
updateValuesPolling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopValuesPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_OVEN_DURATION.equals(channelUID.getId()) && command instanceof QuantityType<?> quantity) {
|
||||
var durationQuantityType = quantity.toUnit(SECOND);
|
||||
if (durationQuantityType != null) {
|
||||
sendIntegerOptionIfAllowed(durationQuantityType, OVEN_DURATION_KEY);
|
||||
} else {
|
||||
logger.warn("Could not set duration! uid={}", getThing().getUID());
|
||||
}
|
||||
} else if (CHANNEL_OVEN_SET_POINT_TEMPERATURE.equals(channelUID.getId())
|
||||
&& (command instanceof QuantityType<?> || command instanceof DecimalType)) {
|
||||
var unit = getTemperatureUnitOfOption(OVEN_SET_POINT_TEMPERATURE_KEY);
|
||||
QuantityType<?> quantity = (command instanceof QuantityType<?> qt) ? qt
|
||||
: new QuantityType<>(((DecimalType) command), unit);
|
||||
var temperatureQuantityType = quantity.toUnit(unit);
|
||||
if (temperatureQuantityType != null) {
|
||||
sendIntegerOptionIfAllowed(temperatureQuantityType, OVEN_SET_POINT_TEMPERATURE_KEY);
|
||||
} else {
|
||||
logger.warn("Could not set temperature! uid={}", getThing().getUID());
|
||||
}
|
||||
} else if (CHANNEL_OVEN_PROGRAM_COMMAND.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
|
||||
var selectedProgram = getKeyValueStore().get(SELECTED_PROGRAM_KEY);
|
||||
if (selectedProgram != null) {
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
if (deviceDescriptionService.getActiveProgram(true) != null) {
|
||||
mapProgramKey(selectedProgram).ifPresent(programUid -> send(Action.POST, RO_ACTIVE_PROGRAM,
|
||||
List.of(new ProgramData(programUid, null)), null, 1));
|
||||
} else {
|
||||
logger.info(
|
||||
"The '{}' control is either unavailable or in read-only mode. Cannot start program.",
|
||||
ACTIVE_PROGRAM_KEY);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} else if (COMMAND_PAUSE.equalsIgnoreCase(command.toFullString())) {
|
||||
sendBooleanCommandIfAllowed(PAUSE_PROGRAM_KEY);
|
||||
} else if (COMMAND_RESUME.equalsIgnoreCase(command.toFullString())) {
|
||||
sendBooleanCommandIfAllowed(RESUME_PROGRAM_KEY);
|
||||
} else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
|
||||
sendBooleanCommandIfAllowed(ABORT_PROGRAM_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic stuff
|
||||
lightChannels.stream().filter(dynamicChannel -> channelUID.getId().equals(dynamicChannel.channelName()))
|
||||
.forEach(dynamicChannel -> sendBooleanSettingIfAllowed(command, dynamicChannel.key()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
if (OVEN_DURATION_KEY.equals(deviceDescriptionChange.key())) {
|
||||
updateIntegerOptionDescriptionIfLinked(CHANNEL_OVEN_DURATION, deviceDescriptionChange.key());
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
var durationOption = deviceDescriptionService.getOption(deviceDescriptionChange.key(), true, true,
|
||||
false);
|
||||
if (durationOption == null) {
|
||||
updateStateIfLinked(CHANNEL_OVEN_DURATION, UnDefType.UNDEF);
|
||||
}
|
||||
});
|
||||
} else if (OVEN_SET_POINT_TEMPERATURE_KEY.equals(deviceDescriptionChange.key())) {
|
||||
updateIntegerOptionDescriptionIfLinked(CHANNEL_OVEN_SET_POINT_TEMPERATURE,
|
||||
deviceDescriptionChange.key());
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
var setPointOption = deviceDescriptionService.getOption(deviceDescriptionChange.key(), true, true,
|
||||
false);
|
||||
if (setPointOption == null) {
|
||||
updateStateIfLinked(CHANNEL_OVEN_SET_POINT_TEMPERATURE, UnDefType.UNDEF);
|
||||
}
|
||||
});
|
||||
} else if (DeviceDescriptionType.COMMAND.equals(deviceDescriptionChange.type())
|
||||
|| DeviceDescriptionType.COMMAND_LIST.equals(deviceDescriptionChange.type())) {
|
||||
updateProgramCommandDescription();
|
||||
}
|
||||
|
||||
// if temperature isn't available -> set to UNDEF
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> Stream
|
||||
.of(currentTemperatureChannels, meatProbeChannels).flatMap(Set::stream)
|
||||
.filter(dynamicChannel -> deviceDescriptionChange.key().equals(dynamicChannel.key()))
|
||||
.filter(dynamicChannel -> !deviceDescriptionService
|
||||
.isStatusAvailableAndReadable(dynamicChannel.key()))
|
||||
.forEach(dynamicChannel -> updateStateIfLinked(dynamicChannel.channelName(), UnDefType.UNDEF)));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case OVEN_DURATION_KEY ->
|
||||
updateStateIfLinked(CHANNEL_OVEN_DURATION, () -> new QuantityType<>(value.getValueAsInt(), SECOND));
|
||||
case OVEN_SET_POINT_TEMPERATURE_KEY -> updateStateIfLinked(CHANNEL_OVEN_SET_POINT_TEMPERATURE,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), getTemperatureUnitOfOption(value.key())));
|
||||
case SELECTED_PROGRAM_KEY, ACTIVE_PROGRAM_KEY -> updateProgramCommandDescription();
|
||||
case OPERATION_STATE_KEY -> updateValuesPolling();
|
||||
}
|
||||
|
||||
// track current cavity temperature and re-evaluate polling
|
||||
if (currentTemperatureChannels.stream().anyMatch(dc -> value.key().equals(dc.key()))) {
|
||||
maxCavityTemperature.set(value.getValueAsInt());
|
||||
updateValuesPolling();
|
||||
}
|
||||
|
||||
// dynamic stuff
|
||||
doorChannels.forEach(dynamicChannel -> {
|
||||
if (value.key().equals(dynamicChannel.key())) {
|
||||
updateStateIfLinked(dynamicChannel.channelName(),
|
||||
() -> STATE_OPEN.equals(value.value()) || STATE_AJAR.equals(value.value()) ? OpenClosedType.OPEN
|
||||
: OpenClosedType.CLOSED);
|
||||
}
|
||||
});
|
||||
Stream.of(currentTemperatureChannels, meatProbeChannels).flatMap(Set::stream)
|
||||
.filter(dynamicChannel -> value.key().equals(dynamicChannel.key())).forEach(dynamicChannel -> {
|
||||
var unit = ContentType.TEMPERATURE_CELSIUS.equals(dynamicChannel.contentType()) ? CELSIUS
|
||||
: FAHRENHEIT;
|
||||
updateStateIfLinked(dynamicChannel.channelName(), new QuantityType<>(value.getValueAsInt(), unit));
|
||||
});
|
||||
Stream.of(lightChannels, meatProbePluggedChannels).flatMap(Set::stream)
|
||||
.filter(dynamicChannel -> value.key().equals(dynamicChannel.key()))
|
||||
.forEach(dynamicChannel -> updateStateIfLinked(dynamicChannel.channelName(),
|
||||
() -> OnOffType.from(value.getValueAsBoolean())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
initializeState(CHANNEL_OVEN_DURATION);
|
||||
initializeState(CHANNEL_OVEN_SET_POINT_TEMPERATURE);
|
||||
initializeState(CHANNEL_OVEN_PROGRAM_COMMAND);
|
||||
Stream.of(doorChannels, currentTemperatureChannels, meatProbeChannels, lightChannels, meatProbePluggedChannels)
|
||||
.flatMap(Set::stream).map(DynamicChannel::channelName).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
if (CHANNEL_OVEN_DURATION.equals(channelId)) {
|
||||
updateIntegerOptionDescriptionIfLinked(channelId, OVEN_DURATION_KEY);
|
||||
updateStateIfLinked(channelId, UnDefType.UNDEF);
|
||||
} else if (CHANNEL_OVEN_SET_POINT_TEMPERATURE.equals(channelId)) {
|
||||
updateIntegerOptionDescriptionIfLinked(channelId, OVEN_SET_POINT_TEMPERATURE_KEY);
|
||||
updateStateIfLinked(channelId, UnDefType.UNDEF);
|
||||
} else if (CHANNEL_OVEN_PROGRAM_COMMAND.equals(channelId)) {
|
||||
updateProgramCommandDescription();
|
||||
} else if (isChannelInSet(doorChannels, channelId)) {
|
||||
updateStateIfLinked(channelId, OpenClosedType.CLOSED);
|
||||
} else if (isChannelInSet(currentTemperatureChannels, channelId)
|
||||
|| isChannelInSet(meatProbeChannels, channelId)) {
|
||||
updateStateIfLinked(channelId, UnDefType.UNDEF);
|
||||
} else if (isChannelInSet(lightChannels, channelId)) {
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
} else if (isChannelInSet(meatProbePluggedChannels, channelId)) {
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChannelInSet(Set<DynamicChannel> channels, String channelId) {
|
||||
return channels.stream().anyMatch(dynamicChannel -> channelId.equals(dynamicChannel.channelName()));
|
||||
}
|
||||
|
||||
private void addCavityChannels() {
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
var thingBuilder = editThing();
|
||||
var channelsChanged = false;
|
||||
|
||||
var cavitySelectorEnumType = deviceDescriptionService.findEnumerationType(OVEN_CAVITY_SELECTOR_ENUM_KEY);
|
||||
if (cavitySelectorEnumType != null) {
|
||||
for (Enumeration enumeration : cavitySelectorEnumType.enumerations().values()) {
|
||||
var cavityIndex = enumeration.value();
|
||||
var cavityKey = enumeration.valueKey();
|
||||
|
||||
// door state
|
||||
var doorStateKey = String.format(OVEN_DOOR_STATE_KEY_TEMPLATE, cavityIndex);
|
||||
var doorChannel = String.format(CHANNEL_OVEN_DOOR_TEMPLATE, cavityIndex);
|
||||
var doorStateStatusDescription = deviceDescriptionService.findStatusByKey(doorStateKey);
|
||||
if (doorStateStatusDescription != null) {
|
||||
doorChannels.add(new DynamicChannel(doorChannel, doorStateKey,
|
||||
doorStateStatusDescription.contentType()));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, doorChannel, CHANNEL_TYPE_DOOR,
|
||||
CoreItemFactory.CONTACT, getTranslationProvider().getText(I18N_OVEN_DOOR,
|
||||
getTranslationProvider().getText(cavityKey)));
|
||||
}
|
||||
|
||||
// temperatures
|
||||
channelsChanged |= registerTemperatureChannelWithKeyTemplate(deviceDescriptionService, thingBuilder,
|
||||
cavityIndex, OVEN_CURRENT_TEMPERATURE_KEY_TEMPLATE, CHANNEL_OVEN_TEMPERATURE_TEMPLATE,
|
||||
CHANNEL_TYPE_OVEN_CURRENT_TEMPERATURE, I18N_OVEN_CURRENT_TEMPERATURE,
|
||||
currentTemperatureChannels, cavityKey);
|
||||
channelsChanged |= registerTemperatureChannelWithKeyTemplate(deviceDescriptionService, thingBuilder,
|
||||
cavityIndex, OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_KEY_TEMPLATE,
|
||||
CHANNEL_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_TEMPLATE,
|
||||
CHANNEL_TYPE_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE, I18N_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE,
|
||||
meatProbeChannels, cavityKey);
|
||||
|
||||
// lights
|
||||
var lightKey = String.format(OVEN_CAVITY_LIGHT_KEY_TEMPLATE, cavityIndex);
|
||||
var lightChannel = String.format(CHANNEL_OVEN_CAVITY_LIGHT_TEMPLATE, cavityIndex);
|
||||
var lightSettingDescription = deviceDescriptionService.findSettingByKey(lightKey);
|
||||
if (lightSettingDescription != null) {
|
||||
lightChannels
|
||||
.add(new DynamicChannel(lightChannel, lightKey, lightSettingDescription.contentType()));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, lightChannel,
|
||||
CHANNEL_TYPE_OVEN_CAVITY_LIGHT, CoreItemFactory.SWITCH, getTranslationProvider()
|
||||
.getText(I18N_OVEN_CAVITY_LIGHT, getTranslationProvider().getText(cavityKey)));
|
||||
}
|
||||
|
||||
// meat probe plugged
|
||||
var meatProbePluggedKey = String.format(OVEN_MEAT_PROBE_PLUGGED_KEY_TEMPLATE, cavityIndex);
|
||||
var meatProbePluggedChannel = String.format(CHANNEL_OVEN_MEAT_PROBE_PLUGGED_TEMPLATE, cavityIndex);
|
||||
var meatProbePluggedStatusDescription = deviceDescriptionService
|
||||
.findStatusByKey(meatProbePluggedKey);
|
||||
if (meatProbePluggedStatusDescription != null) {
|
||||
meatProbePluggedChannels.add(new DynamicChannel(meatProbePluggedChannel, meatProbePluggedKey,
|
||||
meatProbePluggedStatusDescription.contentType()));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, meatProbePluggedChannel,
|
||||
CHANNEL_TYPE_OVEN_MEAT_PROBE_PLUGGED, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_OVEN_MEAT_PROBE_PLUGGED,
|
||||
getTranslationProvider().getText(cavityKey)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// temperatures (main cavity)
|
||||
channelsChanged |= registerTemperatureChannel(deviceDescriptionService, thingBuilder, DEFAULT_CAVITY_INDEX,
|
||||
OVEN_CURRENT_TEMPERATURE_KEY, CHANNEL_OVEN_TEMPERATURE_TEMPLATE,
|
||||
CHANNEL_TYPE_OVEN_CURRENT_TEMPERATURE, I18N_OVEN_CURRENT_TEMPERATURE, currentTemperatureChannels,
|
||||
DEFAULT_CAVITY_NAME);
|
||||
channelsChanged |= registerTemperatureChannel(deviceDescriptionService, thingBuilder, DEFAULT_CAVITY_INDEX,
|
||||
OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_KEY, CHANNEL_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE_TEMPLATE,
|
||||
CHANNEL_TYPE_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE, I18N_OVEN_CURRENT_MEAT_PROBE_TEMPERATURE,
|
||||
meatProbeChannels, DEFAULT_CAVITY_NAME);
|
||||
|
||||
// meat probe plugged (main cavity)
|
||||
if (deviceDescriptionService.findStatusByKey(OVEN_MEAT_PROBE_PLUGGED_KEY) != null) {
|
||||
var meatProbePluggedChannel = String.format(CHANNEL_OVEN_MEAT_PROBE_PLUGGED_TEMPLATE,
|
||||
DEFAULT_CAVITY_INDEX);
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, meatProbePluggedChannel,
|
||||
CHANNEL_TYPE_OVEN_MEAT_PROBE_PLUGGED, CoreItemFactory.SWITCH, getTranslationProvider().getText(
|
||||
I18N_OVEN_MEAT_PROBE_PLUGGED, getTranslationProvider().getText(DEFAULT_CAVITY_NAME)));
|
||||
}
|
||||
|
||||
// door fallback
|
||||
if (doorChannels.isEmpty()) {
|
||||
var doorChannel = String.format(CHANNEL_OVEN_DOOR_TEMPLATE, DEFAULT_CAVITY_INDEX);
|
||||
doorChannels.add(new DynamicChannel(doorChannel, DOOR_STATE_KEY, ContentType.ENUMERATION));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, doorChannel, CHANNEL_TYPE_DOOR,
|
||||
CoreItemFactory.CONTACT, getTranslationProvider().getText(I18N_OVEN_DOOR,
|
||||
getTranslationProvider().getText(DEFAULT_CAVITY_NAME)));
|
||||
}
|
||||
|
||||
if (channelsChanged) {
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean registerTemperatureChannel(DeviceDescriptionService deviceDescriptionService,
|
||||
ThingBuilder thingBuilder, int cavityIndex, String key, String channelTemplate, String channelType,
|
||||
String i18nKey, Set<DynamicChannel> targetSet, String cavityKey) {
|
||||
var channelName = String.format(channelTemplate, cavityIndex);
|
||||
var statusDescription = deviceDescriptionService.findStatusByKey(key);
|
||||
|
||||
if (statusDescription != null) {
|
||||
targetSet.add(new DynamicChannel(channelName, key, statusDescription.contentType()));
|
||||
return addChannelIfNotExist(thingBuilder, channelName, channelType, NUMBER_TEMPERATURE,
|
||||
getTranslationProvider().getText(i18nKey, getTranslationProvider().getText(cavityKey)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean registerTemperatureChannelWithKeyTemplate(DeviceDescriptionService deviceDescriptionService,
|
||||
ThingBuilder thingBuilder, int cavityIndex, String keyTemplate, String channelTemplate, String channelType,
|
||||
String i18nKey, Set<DynamicChannel> targetSet, String cavityKey) {
|
||||
var key = String.format(keyTemplate, cavityIndex);
|
||||
return registerTemperatureChannel(deviceDescriptionService, thingBuilder, cavityIndex, key, channelTemplate,
|
||||
channelType, i18nKey, targetSet, cavityKey);
|
||||
}
|
||||
|
||||
private Unit<Temperature> getTemperatureUnitOfOption(String optionKey) {
|
||||
var unit = getDeviceDescriptionServiceOptional().map(deviceDescriptionService -> {
|
||||
var option = deviceDescriptionService.findOptionByKey(optionKey);
|
||||
if (option != null) {
|
||||
return ContentType.TEMPERATURE_CELSIUS.equals(option.contentType()) ? CELSIUS : FAHRENHEIT;
|
||||
}
|
||||
return CELSIUS;
|
||||
}).orElse(null);
|
||||
return Objects.requireNonNullElse(unit, CELSIUS);
|
||||
}
|
||||
|
||||
private void updateProgramCommandDescription() {
|
||||
getLinkedChannel(CHANNEL_OVEN_PROGRAM_COMMAND).ifPresent(channel -> {
|
||||
var commandOptions = new ArrayList<CommandOption>();
|
||||
|
||||
if (!STATE_NO_PROGRAM.equals(getKeyValueStore().getOrDefault(SELECTED_PROGRAM_KEY, STATE_NO_PROGRAM))
|
||||
|| !STATE_NO_PROGRAM
|
||||
.equals(getKeyValueStore().getOrDefault(ACTIVE_PROGRAM_KEY, STATE_NO_PROGRAM))) {
|
||||
if (STATE_NO_PROGRAM.equals(getKeyValueStore().getOrDefault(ACTIVE_PROGRAM_KEY, STATE_NO_PROGRAM))
|
||||
&& !STATE_NO_PROGRAM
|
||||
.equals(getKeyValueStore().getOrDefault(SELECTED_PROGRAM_KEY, STATE_NO_PROGRAM))) {
|
||||
// no active program and program selected -> show start option
|
||||
commandOptions.add(
|
||||
new CommandOption(COMMAND_START, getTranslationProvider().getText(I18N_START_PROGRAM)));
|
||||
} else {
|
||||
commandOptions
|
||||
.add(new CommandOption(COMMAND_STOP, getTranslationProvider().getText(I18N_STOP_PROGRAM)));
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
if (deviceDescriptionService.isCommandAvailableAndWritable(RESUME_PROGRAM_KEY)) {
|
||||
commandOptions.add(new CommandOption(COMMAND_RESUME,
|
||||
getTranslationProvider().getText(I18N_RESUME_PROGRAM)));
|
||||
} else if (deviceDescriptionService.isCommandAvailableAndWritable(PAUSE_PROGRAM_KEY)) {
|
||||
commandOptions.add(new CommandOption(COMMAND_PAUSE,
|
||||
getTranslationProvider().getText(I18N_PAUSE_PROGRAM)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCommandOptions(channel.getUID(), commandOptions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any registered temperature status has notifyOnChange set to false.
|
||||
*/
|
||||
private boolean hasTemperatureStatusWithoutNotifyOnChange() {
|
||||
var deviceDescriptionService = getDeviceDescriptionServiceOptional().orElse(null);
|
||||
if (deviceDescriptionService == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Stream.of(currentTemperatureChannels, meatProbeChannels).flatMap(Set::stream).map(DynamicChannel::key)
|
||||
.map(deviceDescriptionService::findStatusByKey).filter(Objects::nonNull)
|
||||
.anyMatch(status -> !status.notifyOnChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the required polling interval based on the current operation state and cavity temperature,
|
||||
* then starts, adjusts, or stops polling accordingly.
|
||||
*
|
||||
* Polling rules:
|
||||
* - Operation state "Run" and notifyOnChange is false: poll every 60 seconds
|
||||
* - Not running, temperature > 50°C: poll every 60 seconds
|
||||
* - Not running, temperature 30-50°C: poll every 5 minutes
|
||||
* - Not running, temperature < 30°C: stop polling
|
||||
*/
|
||||
private void updateValuesPolling() {
|
||||
if (!hasTemperatureStatusWithoutNotifyOnChange()) {
|
||||
stopValuesPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
var operationState = getKeyValueStore().get(OPERATION_STATE_KEY);
|
||||
if (STATE_RUN.equals(operationState)) {
|
||||
scheduleValuesPolling(POLLING_INTERVAL_RUN_SECONDS);
|
||||
return;
|
||||
}
|
||||
|
||||
// after leaving "Run" state, keep polling based on the current cavity temperature
|
||||
// to track the cool-down phase
|
||||
var temperature = maxCavityTemperature.get();
|
||||
if (temperature > TEMPERATURE_THRESHOLD_HIGH) {
|
||||
scheduleValuesPolling(POLLING_INTERVAL_RUN_SECONDS);
|
||||
} else if (temperature >= TEMPERATURE_THRESHOLD_LOW) {
|
||||
scheduleValuesPolling(POLLING_INTERVAL_COOLDOWN_SECONDS);
|
||||
} else {
|
||||
scheduleValuesPolling(POLLING_INTERVAL_IDLE_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scheduleValuesPolling(long intervalSeconds) {
|
||||
var pollingFuture = this.pollingFuture;
|
||||
|
||||
// restart polling if interval changed
|
||||
if (pollingFuture != null && !pollingFuture.isCancelled() && !pollingFuture.isDone()
|
||||
&& currentPollingIntervalSeconds != intervalSeconds) {
|
||||
logger.debug("Polling interval changed from {} to {} second(s), rescheduling ({}).",
|
||||
currentPollingIntervalSeconds, intervalSeconds, getThing().getUID());
|
||||
pollingFuture.cancel(false);
|
||||
pollingFuture = null;
|
||||
}
|
||||
|
||||
if (pollingFuture == null || pollingFuture.isCancelled() || pollingFuture.isDone()) {
|
||||
logger.debug("Schedule mandatory values polling every {} second(s) ({}).", intervalSeconds,
|
||||
getThing().getUID());
|
||||
currentPollingIntervalSeconds = intervalSeconds;
|
||||
this.pollingFuture = scheduler.scheduleWithFixedDelay(() -> sendGet(RO_ALL_MANDATORY_VALUES),
|
||||
intervalSeconds, intervalSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopValuesPolling() {
|
||||
var pollingFuture = this.pollingFuture;
|
||||
|
||||
if (pollingFuture != null) {
|
||||
logger.debug("Stop mandatory values polling ({}).", getThing().getUID());
|
||||
pollingFuture.cancel(true);
|
||||
this.pollingFuture = null;
|
||||
currentPollingIntervalSeconds = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.SIUnits.GRAM;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.configuration.HomeConnectDirectConfiguration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.i18n.HomeConnectDirectTranslationProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.provider.HomeConnectDirectDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.DeviceDescriptionService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.ApplianceProfileService;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
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.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectWasherDryerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectWasherDryerHandler extends BaseHomeConnectDirectHandler {
|
||||
public HomeConnectDirectWasherDryerHandler(Thing thing, ApplianceProfileService applianceProfileService,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider commandDescriptionProvider,
|
||||
HomeConnectDirectDynamicStateDescriptionProvider stateDescriptionProvider, String deviceId,
|
||||
HomeConnectDirectConfiguration configuration, HomeConnectDirectTranslationProvider translationProvider) {
|
||||
super(thing, applianceProfileService, commandDescriptionProvider, stateDescriptionProvider, deviceId,
|
||||
configuration, translationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeStarted() {
|
||||
addIDosChannels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeFinished() {
|
||||
initializeAllStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
if (CHANNEL_WASHER_I_DOS_1_ACTIVE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_I_DOS_1_ACTIVE_KEY);
|
||||
} else if (CHANNEL_WASHER_I_DOS_2_ACTIVE.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_I_DOS_2_ACTIVE_KEY);
|
||||
} else if (CHANNEL_WASHER_TEMPERATURE.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, WASHER_TEMPERATURE_KEY);
|
||||
} else if (CHANNEL_WASHER_SPIN_SPEED.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, WASHER_SPIN_SPEED_KEY);
|
||||
} else if (CHANNEL_WASHER_RINSE_PLUS.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, WASHER_RINSE_PLUS_KEY);
|
||||
} else if (CHANNEL_WASHER_STAINS.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, WASHER_STAINS_KEY);
|
||||
} else if (CHANNEL_DRYER_DRYING_TARGET.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, DRYER_DRYING_TARGET_KEY);
|
||||
} else if (CHANNEL_DRYER_WRINKLE_GUARD.equals(channelUID.getId()) && command instanceof StringType) {
|
||||
sendEnumOptionIfAllowed(command, DRYER_WRINKLE_GUARD_KEY);
|
||||
} else if (CHANNEL_WASHER_SPEED_PERFECT.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_SPEED_PERFECT_KEY);
|
||||
} else if (CHANNEL_WASHER_WATER_PLUS.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_WATER_PLUS_KEY);
|
||||
} else if (CHANNEL_WASHER_PREWASH.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_PREWASH_KEY);
|
||||
} else if (CHANNEL_WASHER_RINSE_HOLD.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_RINSE_HOLD_KEY);
|
||||
} else if (CHANNEL_WASHER_LESS_IRONING.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_LESS_IRONING_KEY);
|
||||
} else if (CHANNEL_WASHER_SILENT_WASH.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_SILENT_WASH_KEY);
|
||||
} else if (CHANNEL_WASHER_SOAK.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
sendBooleanOptionIfAllowed(command, WASHER_SOAK_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceDescriptionChangeEvent(List<DeviceDescriptionChange> deviceDescriptionChanges) {
|
||||
super.onApplianceDescriptionChangeEvent(deviceDescriptionChanges);
|
||||
|
||||
deviceDescriptionChanges.forEach(deviceDescriptionChange -> {
|
||||
var key = deviceDescriptionChange.key();
|
||||
switch (key) {
|
||||
case LAUNDRY_CARE_PROCESS_PHASE_KEY, DRYER_PROCESS_PHASE_KEY ->
|
||||
updateReadonlyEnumOptionDescriptionIfLinked(CHANNEL_LAUNDRY_CARE_PROCESS_PHASE, key);
|
||||
case WASHER_TEMPERATURE_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_TEMPERATURE, key);
|
||||
case WASHER_SPIN_SPEED_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_SPIN_SPEED, key);
|
||||
case WASHER_RINSE_PLUS_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_RINSE_PLUS, key);
|
||||
case WASHER_STAINS_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_STAINS, key);
|
||||
case DRYER_DRYING_TARGET_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_DRYER_DRYING_TARGET, key);
|
||||
case DRYER_WRINKLE_GUARD_KEY -> updateEnumOptionDescriptionIfLinked(CHANNEL_DRYER_WRINKLE_GUARD, key);
|
||||
case ROOT_OPTION_LIST_KEY, LAUNDRY_CARE_OPTION_LIST_KEY -> {
|
||||
updateReadonlyEnumOptionDescriptionIfLinked(CHANNEL_LAUNDRY_CARE_PROCESS_PHASE, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_TEMPERATURE, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_SPIN_SPEED, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_RINSE_PLUS, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_WASHER_STAINS, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_DRYER_DRYING_TARGET, key);
|
||||
updateEnumOptionDescriptionIfLinked(CHANNEL_DRYER_WRINKLE_GUARD, key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplianceValueEvent(Value value, Resource resource) {
|
||||
super.onApplianceValueEvent(value, resource);
|
||||
|
||||
switch (value.key()) {
|
||||
case LAUNDRY_CARE_PROCESS_PHASE_KEY, DRYER_PROCESS_PHASE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_LAUNDRY_CARE_PROCESS_PHASE, new StringType(value.getValueAsString()));
|
||||
case WASHER_I_DOS_1_FILL_LEVEL_POOR_KEY -> updateStateIfLinked(CHANNEL_WASHER_I_DOS_1_FILL_LEVEL_POOR,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_POOR.equalsIgnoreCase(value.getValueAsString())));
|
||||
case WASHER_I_DOS_2_FILL_LEVEL_POOR_KEY -> updateStateIfLinked(CHANNEL_WASHER_I_DOS_2_FILL_LEVEL_POOR,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_POOR.equalsIgnoreCase(value.getValueAsString())));
|
||||
case WASHER_I_DOS_1_ACTIVE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_I_DOS_1_ACTIVE, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_I_DOS_2_ACTIVE_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_I_DOS_2_ACTIVE, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_TEMPERATURE_KEY -> getLinkedChannel(CHANNEL_WASHER_TEMPERATURE).ifPresent(channel -> {
|
||||
getKeyValueStore().put(WASHER_TEMPERATURE_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case WASHER_SPIN_SPEED_KEY -> getLinkedChannel(CHANNEL_WASHER_SPIN_SPEED).ifPresent(channel -> {
|
||||
getKeyValueStore().put(WASHER_SPIN_SPEED_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case WASHER_RINSE_PLUS_KEY -> getLinkedChannel(CHANNEL_WASHER_RINSE_PLUS).ifPresent(channel -> {
|
||||
getKeyValueStore().put(WASHER_RINSE_PLUS_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case WASHER_STAINS_KEY -> getLinkedChannel(CHANNEL_WASHER_STAINS).ifPresent(channel -> {
|
||||
getKeyValueStore().put(WASHER_STAINS_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case LAUNDRY_LOAD_INFORMATION_KEY, LAUNDRY_LOAD_KEY -> updateStateIfLinked(CHANNEL_LAUNDRY_LOAD_INFORMATION,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), GRAM));
|
||||
case LAUNDRY_LOAD_RECOMMENDATION_KEY -> updateStateIfLinked(CHANNEL_LAUNDRY_LOAD_RECOMMENDATION,
|
||||
() -> new QuantityType<>(value.getValueAsInt(), GRAM));
|
||||
case LAUNDRY_DRUM_CLEAN_REMINDER_KEY -> updateStateIfLinked(CHANNEL_LAUNDRY_DRUM_CLEAN_REMINDER,
|
||||
() -> OnOffType.from(STATE_PRESENT.equalsIgnoreCase(value.getValueAsString())
|
||||
|| STATE_CONFIRMED.equalsIgnoreCase(value.getValueAsString())));
|
||||
case DRYER_DRYING_TARGET_KEY -> getLinkedChannel(CHANNEL_DRYER_DRYING_TARGET).ifPresent(channel -> {
|
||||
getKeyValueStore().put(DRYER_DRYING_TARGET_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case DRYER_WRINKLE_GUARD_KEY -> getLinkedChannel(CHANNEL_DRYER_WRINKLE_GUARD).ifPresent(channel -> {
|
||||
getKeyValueStore().put(DRYER_WRINKLE_GUARD_KEY, value.getValueAsString());
|
||||
updateState(channel.getUID(), new StringType(value.getValueAsString()));
|
||||
});
|
||||
case WASHER_SPEED_PERFECT_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_SPEED_PERFECT, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_WATER_PLUS_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_WATER_PLUS, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_PREWASH_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_PREWASH, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_RINSE_HOLD_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_RINSE_HOLD, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_LESS_IRONING_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_LESS_IRONING, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_SILENT_WASH_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_SILENT_WASH, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
case WASHER_SOAK_KEY ->
|
||||
updateStateIfLinked(CHANNEL_WASHER_SOAK, () -> OnOffType.from(value.getValueAsBoolean()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
initializeState(channelUID.getId());
|
||||
}
|
||||
|
||||
private void initializeAllStates() {
|
||||
Set.of(CHANNEL_LAUNDRY_CARE_PROCESS_PHASE, CHANNEL_WASHER_TEMPERATURE, CHANNEL_WASHER_SPIN_SPEED,
|
||||
CHANNEL_WASHER_RINSE_PLUS, CHANNEL_WASHER_STAINS, CHANNEL_DRYER_DRYING_TARGET,
|
||||
CHANNEL_DRYER_WRINKLE_GUARD, CHANNEL_LAUNDRY_DRUM_CLEAN_REMINDER, CHANNEL_WASHER_SPEED_PERFECT,
|
||||
CHANNEL_LAUNDRY_LOAD_INFORMATION, CHANNEL_LAUNDRY_LOAD_RECOMMENDATION, CHANNEL_WASHER_WATER_PLUS,
|
||||
CHANNEL_WASHER_PREWASH, CHANNEL_WASHER_RINSE_HOLD, CHANNEL_WASHER_LESS_IRONING,
|
||||
CHANNEL_WASHER_SILENT_WASH, CHANNEL_WASHER_SOAK, CHANNEL_WASHER_I_DOS_1_ACTIVE,
|
||||
CHANNEL_WASHER_I_DOS_2_ACTIVE, CHANNEL_WASHER_I_DOS_1_FILL_LEVEL_POOR,
|
||||
CHANNEL_WASHER_I_DOS_2_FILL_LEVEL_POOR).forEach(this::initializeState);
|
||||
}
|
||||
|
||||
private void initializeState(String channelId) {
|
||||
switch (channelId) {
|
||||
case CHANNEL_LAUNDRY_CARE_PROCESS_PHASE ->
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
if (optionKeyExists(deviceDescriptionService, LAUNDRY_CARE_PROCESS_PHASE_KEY)) {
|
||||
updateReadonlyEnumOptionDescriptionIfLinked(channelId, LAUNDRY_CARE_PROCESS_PHASE_KEY);
|
||||
} else if (optionKeyExists(deviceDescriptionService, DRYER_PROCESS_PHASE_KEY)) {
|
||||
updateReadonlyEnumOptionDescriptionIfLinked(channelId, DRYER_PROCESS_PHASE_KEY);
|
||||
}
|
||||
});
|
||||
case CHANNEL_WASHER_TEMPERATURE -> updateEnumOptionDescriptionIfLinked(channelId, WASHER_TEMPERATURE_KEY);
|
||||
case CHANNEL_WASHER_SPIN_SPEED -> updateEnumOptionDescriptionIfLinked(channelId, WASHER_SPIN_SPEED_KEY);
|
||||
case CHANNEL_WASHER_RINSE_PLUS -> updateEnumOptionDescriptionIfLinked(channelId, WASHER_RINSE_PLUS_KEY);
|
||||
case CHANNEL_WASHER_STAINS -> updateEnumOptionDescriptionIfLinked(channelId, WASHER_STAINS_KEY);
|
||||
case CHANNEL_DRYER_DRYING_TARGET -> updateEnumOptionDescriptionIfLinked(channelId, DRYER_DRYING_TARGET_KEY);
|
||||
case CHANNEL_DRYER_WRINKLE_GUARD -> updateEnumOptionDescriptionIfLinked(channelId, DRYER_WRINKLE_GUARD_KEY);
|
||||
case CHANNEL_LAUNDRY_DRUM_CLEAN_REMINDER, CHANNEL_WASHER_SPEED_PERFECT, CHANNEL_WASHER_WATER_PLUS,
|
||||
CHANNEL_WASHER_PREWASH, CHANNEL_WASHER_RINSE_HOLD, CHANNEL_WASHER_LESS_IRONING,
|
||||
CHANNEL_WASHER_SILENT_WASH, CHANNEL_WASHER_SOAK, CHANNEL_WASHER_I_DOS_1_ACTIVE,
|
||||
CHANNEL_WASHER_I_DOS_2_ACTIVE, CHANNEL_WASHER_I_DOS_1_FILL_LEVEL_POOR,
|
||||
CHANNEL_WASHER_I_DOS_2_FILL_LEVEL_POOR ->
|
||||
updateStateIfLinked(channelId, OnOffType.OFF);
|
||||
case CHANNEL_LAUNDRY_LOAD_INFORMATION, CHANNEL_LAUNDRY_LOAD_RECOMMENDATION ->
|
||||
updateStateIfLinked(channelId, () -> new QuantityType<>(0, GRAM));
|
||||
}
|
||||
}
|
||||
|
||||
private void addIDosChannels() {
|
||||
getDeviceDescriptionServiceOptional().ifPresent(deviceDescriptionService -> {
|
||||
var iDosAvailable = deviceDescriptionService.getPrograms(false).stream().anyMatch(program -> program
|
||||
.options().stream().anyMatch(opt -> WASHER_I_DOS_1_ACTIVE_KEY.equals(opt.refKey())));
|
||||
|
||||
if (iDosAvailable) {
|
||||
var thingBuilder = editThing();
|
||||
boolean channelsChanged = false;
|
||||
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_WASHER_I_DOS_1_ACTIVE,
|
||||
CHANNEL_TYPE_I_DOS_ACTIVE, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_I_DOS_ACTIVE, 1));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_WASHER_I_DOS_2_ACTIVE,
|
||||
CHANNEL_TYPE_I_DOS_ACTIVE, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_I_DOS_ACTIVE, 2));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_WASHER_I_DOS_1_FILL_LEVEL_POOR,
|
||||
CHANNEL_TYPE_I_DOS_FILL_LEVEL_POOR, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_I_DOS_FILL_LEVEL_POOR, 1));
|
||||
channelsChanged |= addChannelIfNotExist(thingBuilder, CHANNEL_WASHER_I_DOS_2_FILL_LEVEL_POOR,
|
||||
CHANNEL_TYPE_I_DOS_FILL_LEVEL_POOR, CoreItemFactory.SWITCH,
|
||||
getTranslationProvider().getText(I18N_I_DOS_FILL_LEVEL_POOR, 2));
|
||||
|
||||
if (channelsChanged) {
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean optionKeyExists(DeviceDescriptionService deviceDescriptionService, String optionKey) {
|
||||
return deviceDescriptionService.getOption(optionKey, false, false, false) != null;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler.model;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.common.utils.ConfigurationUtils.createGson;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.change.DeviceDescriptionChange;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Action;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Appliance message model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ApplianceMessage(OffsetDateTime dateTime, long id, MessageType type, Resource resource, int version,
|
||||
long sessionId, long messageId, Action action, @Nullable Integer code, @Nullable JsonArray rawPayload,
|
||||
@Nullable List<Value> values, @Nullable List<DeviceDescriptionChange> descriptionChanges) {
|
||||
|
||||
private static final Gson GSON = createGson();
|
||||
|
||||
public <T> @Nullable List<T> getRawPayloadAsList(Class<T> clazz) {
|
||||
var dataJsonArray = rawPayload();
|
||||
if (dataJsonArray != null) {
|
||||
return GSON.fromJson(dataJsonArray, TypeToken.getParameterized(List.class, clazz).getType());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
|
||||
/**
|
||||
* Dynamic channel model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record DynamicChannel(String channelName, String key, ContentType contentType) {
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Appliance message type.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum MessageType {
|
||||
INCOMING,
|
||||
OUTGOING,
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Action;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
|
||||
/**
|
||||
* Send message request model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record SendMessageRequest(Action action, Resource resource, @Nullable Object data, @Nullable Integer version) {
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.handler.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DataType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescriptionType;
|
||||
|
||||
/**
|
||||
* Appliance value model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Value(int uid, String key, Object value, Object rawValue, DeviceDescriptionType type,
|
||||
@Nullable ContentType contentType, @Nullable DataType dataType, @Nullable Integer enumerationType) {
|
||||
|
||||
public int getValueAsInt() {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Double.valueOf(String.valueOf(value)).intValue();
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public double getValueAsDouble() {
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getValueAsBoolean() {
|
||||
return Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
|
||||
public String getValueAsString() {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.i18n;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.osgi.framework.Bundle;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
|
||||
/**
|
||||
* {@link HomeConnectDirectTranslationProvider} provides i18n message lookup.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectTranslationProvider {
|
||||
|
||||
private final @Nullable Bundle bundle;
|
||||
private final TranslationProvider translationProvider;
|
||||
private final LocaleProvider localeProvider;
|
||||
|
||||
public HomeConnectDirectTranslationProvider(TranslationProvider translationProvider,
|
||||
LocaleProvider localeProvider) {
|
||||
this.bundle = FrameworkUtil.getBundle(this.getClass());
|
||||
this.translationProvider = translationProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
public String getText(String key, @Nullable Object... arguments) {
|
||||
var localBundle = bundle;
|
||||
if (localBundle == null) {
|
||||
return key;
|
||||
}
|
||||
var text = translationProvider.getText(localBundle, key, null, localeProvider.getLocale(), arguments);
|
||||
return Objects.requireNonNullElse(text, key);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
|
||||
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectDynamicCommandDescriptionProvider} is responsible for handling dynamic commands.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicCommandDescriptionProvider.class,
|
||||
HomeConnectDirectDynamicCommandDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider {
|
||||
|
||||
@Activate
|
||||
public HomeConnectDirectDynamicCommandDescriptionProvider(@Reference EventPublisher eventPublisher,
|
||||
@Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
|
||||
@Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.provider;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.events.ThingEventFactory;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HomeConnectDirectDynamicStateDescriptionProvider} is responsible for handling dynamic values.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, HomeConnectDirectDynamicStateDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class HomeConnectDirectDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
|
||||
protected final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragmentMap;
|
||||
|
||||
@Activate
|
||||
public HomeConnectDirectDynamicStateDescriptionProvider(@Reference EventPublisher eventPublisher,
|
||||
@Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
|
||||
@Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
this.stateDescriptionFragmentMap = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
|
||||
@Nullable Locale locale) {
|
||||
if (stateDescriptionFragmentMap.containsKey(channel.getUID())) {
|
||||
return Objects.requireNonNull(stateDescriptionFragmentMap.get(channel.getUID())).toStateDescription();
|
||||
} else {
|
||||
return super.getStateDescription(channel, original, locale);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStateDescriptionFragment(ChannelUID channelUID, StateDescriptionFragment stateDescriptionFragment) {
|
||||
var oldStateDescriptionFragment = stateDescriptionFragmentMap.get(channelUID);
|
||||
var itemChannelLinkRegistry = this.itemChannelLinkRegistry;
|
||||
|
||||
if (!stateDescriptionFragment.equals(oldStateDescriptionFragment)) {
|
||||
stateDescriptionFragmentMap.put(channelUID, stateDescriptionFragment);
|
||||
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
|
||||
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(),
|
||||
stateDescriptionFragment, oldStateDescriptionFragment));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
stateDescriptionFragmentMap.clear();
|
||||
super.deactivate();
|
||||
}
|
||||
}
|
||||
+1712
File diff suppressed because it is too large
Load Diff
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.THING_TYPE_WARMING_DRAWER;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.handler.model.Value;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DataType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescriptionType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ProtocolType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.model.FeatureMapping;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.Resource;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.model.data.ValueData;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Utility methods for device description processing.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DeviceDescriptionUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DeviceDescriptionUtils.class);
|
||||
|
||||
private DeviceDescriptionUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
public static @Nullable List<Value> mapValues(DeviceDescriptionService deviceDescriptionService,
|
||||
FeatureMapping featureMapping, Resource resource, @Nullable List<ValueData> valueDataList,
|
||||
@Nullable ThingTypeUID thingTypeUID) {
|
||||
List<Value> mappedValues = null;
|
||||
if (valueDataList != null) {
|
||||
var values = valueDataList.stream().map(valueData -> {
|
||||
var valueDescription = deviceDescriptionService.getDeviceDescriptionObject(valueData.uid());
|
||||
|
||||
// key
|
||||
var key = featureMapping.mapFeatureIdToKey(valueData.uid());
|
||||
|
||||
// type
|
||||
var type = valueDescription != null ? valueDescription.type() : DeviceDescriptionType.UNKNOWN;
|
||||
|
||||
// contentType
|
||||
ContentType contentType = null;
|
||||
if (valueDescription != null
|
||||
&& valueDescription.object() instanceof ContentTypeProvider contentTypeProvider) {
|
||||
contentType = contentTypeProvider.contentType();
|
||||
}
|
||||
|
||||
// dataType
|
||||
DataType dataType = null;
|
||||
if (valueDescription != null
|
||||
&& valueDescription.object() instanceof DataTypeProvider dataTypeProvider) {
|
||||
dataType = dataTypeProvider.dataType();
|
||||
}
|
||||
|
||||
// value
|
||||
Object value = valueData.value();
|
||||
Integer enumerationTypeId = null;
|
||||
if (DeviceDescriptionType.SELECTED_PROGRAM.equals(type)
|
||||
|| DeviceDescriptionType.ACTIVE_PROGRAM.equals(type)) {
|
||||
var programUid = mapObjectToInteger(valueData.value());
|
||||
if (programUid != null) {
|
||||
var program = deviceDescriptionService.findProgram(programUid);
|
||||
if (program != null) {
|
||||
value = program.key();
|
||||
}
|
||||
}
|
||||
} else if (ContentType.ENUMERATION.equals(contentType)) {
|
||||
if (valueDescription != null
|
||||
&& valueDescription.object() instanceof EnumerationTypeProvider enumerationTypeProvider) {
|
||||
var enumTypeId = enumerationTypeProvider.enumerationType();
|
||||
enumerationTypeId = enumTypeId;
|
||||
var enumValue = mapObjectToInteger(valueData.value());
|
||||
if (enumTypeId != null && enumValue != null) {
|
||||
var enumeration = deviceDescriptionService.findEnumeration(enumTypeId, enumValue);
|
||||
if (enumeration != null) {
|
||||
value = enumeration.valueKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed-point scaling: when protocolType is FLOAT but wire encoding is a raw integer type,
|
||||
// the value is a fixed-point integer that needs to be divided by 10.
|
||||
// Only applied to warming drawers as they encode temperature values as fixed-point integers.
|
||||
if (THING_TYPE_WARMING_DRAWER.equals(thingTypeUID) && contentType != null
|
||||
&& ProtocolType.FLOAT.equals(contentType.protocolType) && dataType != null
|
||||
&& dataType.isRawInteger() && value instanceof Number number) {
|
||||
LOGGER.debug(
|
||||
"Fixed-point scaling applied: key={}, contentType={}, dataType={}, rawValue={}, scaledValue={}",
|
||||
key, contentType, dataType, number, number.doubleValue() / 10.0);
|
||||
value = number.doubleValue() / 10.0;
|
||||
}
|
||||
|
||||
return new Value(valueData.uid(), key, value, valueData.value(), type, contentType, dataType,
|
||||
enumerationTypeId);
|
||||
}).toList();
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
mappedValues = values;
|
||||
}
|
||||
}
|
||||
return mappedValues;
|
||||
}
|
||||
|
||||
public static @Nullable Integer mapObjectToInteger(@Nullable Object object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (object) {
|
||||
case Integer intValue -> intValue;
|
||||
case Long longValue -> longValue.intValue();
|
||||
case Float floatValue when floatValue % 1 == 0 -> floatValue.intValue();
|
||||
case Double doubleValue when doubleValue % 1 == 0 -> doubleValue.intValue();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Access level model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Access {
|
||||
READ_WRITE,
|
||||
READ,
|
||||
READ_STATIC,
|
||||
WRITE_ONLY,
|
||||
NONE
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Active program model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ActiveProgram(int uid, String key, boolean available, Access access, boolean fullOptionSet,
|
||||
@Nullable Boolean validate) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.RangeProvider;
|
||||
|
||||
/**
|
||||
* Command model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*
|
||||
* @param access possible values: WRITE_ONLY, NONE
|
||||
* @param dataType the data type identifier (refDID) from the device description
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Command(int uid, String key, ContentType contentType, @Nullable DataType dataType, boolean available,
|
||||
Access access, @Nullable Integer enumerationType, @Nullable String enumerationTypeKey, @Nullable Number min,
|
||||
@Nullable Number max, @Nullable Number stepSize, boolean passwordProtected, boolean notifyOnChange)
|
||||
implements
|
||||
AccessProvider,
|
||||
AvailableProvider,
|
||||
RangeProvider,
|
||||
KeyProvider,
|
||||
EnumerationTypeProvider,
|
||||
ContentTypeProvider,
|
||||
DataTypeProvider {
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Command list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record CommandList(int uid, String key, boolean available, Access access,
|
||||
DoubleKeyMap<Integer, String, CommandList> commandLists,
|
||||
DoubleKeyMap<Integer, String, Command> commands) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Content type model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ContentType {
|
||||
UNKNOWN(0, "unknown", ProtocolType.STRING),
|
||||
BOOLEAN(0x01, "boolean", ProtocolType.BOOLEAN),
|
||||
INTEGER(0x02, "integer", ProtocolType.INTEGER),
|
||||
ENUMERATION(0x03, "enumeration", ProtocolType.INTEGER),
|
||||
FLOAT(0x04, "float", ProtocolType.FLOAT),
|
||||
STRING(0x05, "string", ProtocolType.STRING),
|
||||
DATE_TIME(0x06, "dateTime", ProtocolType.STRING),
|
||||
TEMPERATURE_CELSIUS(0x07, "temperatureCelsius", ProtocolType.FLOAT),
|
||||
TEMPERATURE_FAHRENHEIT(0x08, "temperatureFahrenheit", ProtocolType.FLOAT),
|
||||
HEX_BINARY(0x0A, "hexBinary", ProtocolType.STRING),
|
||||
TIME_SPAN(0x10, "timeSpan", ProtocolType.INTEGER),
|
||||
PERCENT(0x11, "percent", ProtocolType.FLOAT),
|
||||
DBM(0x12, "dbm", ProtocolType.INTEGER),
|
||||
WEIGHT(0x13, "weight", ProtocolType.INTEGER),
|
||||
LIQUID_VOLUME(0x14, "liquidVolume", ProtocolType.INTEGER),
|
||||
UID_VALUE(0x15, "uidValue", ProtocolType.INTEGER),
|
||||
DATE(0x16, "date", ProtocolType.STRING),
|
||||
TIME(0x17, "time", ProtocolType.STRING),
|
||||
WATER_HARDNESS(0x18, "waterHardness", ProtocolType.INTEGER),
|
||||
POINT_2D(0x19, "point2D", ProtocolType.OBJECT),
|
||||
POSE_2D(0x1A, "pose2D", ProtocolType.OBJECT),
|
||||
LINE_2D(0x1B, "line2D", ProtocolType.OBJECT),
|
||||
RGB(0x1E, "rgb", ProtocolType.STRING),
|
||||
RPM(0x1F, "rpm", ProtocolType.INTEGER),
|
||||
FLOW_RATE(0x20, "flowRate", ProtocolType.INTEGER),
|
||||
LENGTH(0x21, "length", ProtocolType.FLOAT),
|
||||
AREA(0x22, "area", ProtocolType.FLOAT),
|
||||
POWER(0x23, "power", ProtocolType.FLOAT),
|
||||
ENERGY(0x24, "energy", ProtocolType.FLOAT),
|
||||
BIG_INTEGER(0x25, "bigInteger", ProtocolType.INTEGER),
|
||||
IDENTIFIER(0x26, "identifier", ProtocolType.INTEGER),
|
||||
SPEED(0x27, "speed", ProtocolType.FLOAT),
|
||||
PROGRAM_INSTRUCTION(0x28, "programInstruction", ProtocolType.OBJECT),
|
||||
WEIGHT_POUND(0x29, "weightPound", ProtocolType.FLOAT),
|
||||
LOCALE_STRING(0x2A, "localeString", ProtocolType.OBJECT),
|
||||
TEASPOON(0x2B, "teaspoon", ProtocolType.FLOAT),
|
||||
TABLESPOON(0x2C, "tablespoon", ProtocolType.FLOAT),
|
||||
CUP(0x2D, "cup", ProtocolType.FLOAT),
|
||||
PIECE(0x2E, "piece", ProtocolType.FLOAT),
|
||||
BYTE_LENGTH(0x2F, "byteLength", ProtocolType.INTEGER),
|
||||
UUID(0x30, "uuid", ProtocolType.STRING),
|
||||
TIMEZONE(0x31, "timezone", ProtocolType.OBJECT),
|
||||
CSV(0x32, "csv", ProtocolType.STRING),
|
||||
LEAF(0x33, "leaf", ProtocolType.FLOAT),
|
||||
BUNCH(0x34, "bunch", ProtocolType.FLOAT),
|
||||
CASKET(0x35, "casket", ProtocolType.FLOAT),
|
||||
PINCH(0x36, "pinch", ProtocolType.FLOAT),
|
||||
STALK(0x37, "stalk", ProtocolType.FLOAT),
|
||||
STICK(0x38, "stick", ProtocolType.FLOAT),
|
||||
BRANCH(0x39, "branch", ProtocolType.FLOAT),
|
||||
TRAY(0x3A, "tray", ProtocolType.INTEGER),
|
||||
PORTION(0x3B, "portion", ProtocolType.INTEGER),
|
||||
UTC_DATE_TIME(0x3D, "utcDateTime", ProtocolType.STRING),
|
||||
PROGRAM_RUN_SUMMARY(0x3E, "programRunSummary", ProtocolType.OBJECT),
|
||||
PROGRAM_SESSION_SUMMARY(0x3F, "programSessionSummary", ProtocolType.OBJECT),
|
||||
LIQUID_VOLUME_THROUGHPUT(0x40, "liquidVolumeThroughput", ProtocolType.FLOAT),
|
||||
WEIGHT_OUNCES(0x41, "weightOunces", ProtocolType.FLOAT),
|
||||
RECTANGLE_2D(0x42, "rectangle2D", ProtocolType.OBJECT),
|
||||
BOOLEAN_LIST(0x81, "booleanList", ProtocolType.OBJECT),
|
||||
INTEGER_LIST(0x82, "integerList", ProtocolType.OBJECT),
|
||||
ENUMERATION_LIST(0x83, "enumerationList", ProtocolType.OBJECT),
|
||||
FLOAT_LIST(0x84, "floatList", ProtocolType.OBJECT),
|
||||
STRING_LIST(0x85, "stringList", ProtocolType.OBJECT),
|
||||
DATE_TIME_LIST(0x86, "dateTimeList", ProtocolType.OBJECT),
|
||||
TEMPERATURE_CELSIUS_LIST(0x87, "temperatureCelsiusList", ProtocolType.OBJECT),
|
||||
TEMPERATURE_FAHRENHEIT_LIST(0x88, "temperatureFahrenheitList", ProtocolType.OBJECT),
|
||||
HEX_BINARY_LIST(0x8A, "hexBinaryList", ProtocolType.OBJECT),
|
||||
TIME_SPAN_LIST(0x90, "timeSpanList", ProtocolType.OBJECT),
|
||||
PERCENT_LIST(0x91, "percentList", ProtocolType.OBJECT),
|
||||
DBM_LIST(0x92, "dBmList", ProtocolType.OBJECT),
|
||||
WEIGHT_LIST(0x93, "weightList", ProtocolType.OBJECT),
|
||||
LIQUID_VOLUME_LIST(0x94, "liquidVolumeList", ProtocolType.OBJECT),
|
||||
UID_VALUE_LIST(0x95, "uidValueList", ProtocolType.OBJECT),
|
||||
DATE_LIST(0x96, "dateList", ProtocolType.OBJECT),
|
||||
TIME_LIST(0x97, "timeList", ProtocolType.OBJECT),
|
||||
WATER_HARDNESS_LIST(0x98, "waterHardnessList", ProtocolType.OBJECT),
|
||||
POINT_2D_LIST(0x99, "point2DList", ProtocolType.OBJECT),
|
||||
POSE_2D_LIST(0x9A, "pose2DList", ProtocolType.OBJECT),
|
||||
LINE_2D_LIST(0x9B, "line2DList", ProtocolType.OBJECT),
|
||||
PATH(0x9C, "path", ProtocolType.OBJECT),
|
||||
POLYGON(0x9D, "polygon", ProtocolType.OBJECT),
|
||||
RGB_LIST(0x9E, "rgbList", ProtocolType.OBJECT),
|
||||
RPM_LIST(0x9F, "rpmList", ProtocolType.OBJECT),
|
||||
FLOW_RATE_LIST(0xA0, "flowRateList", ProtocolType.OBJECT),
|
||||
LENGTH_LIST(0xA1, "lengthList", ProtocolType.OBJECT),
|
||||
AREA_LIST(0xA2, "areaList", ProtocolType.OBJECT),
|
||||
POWER_LIST(0xA3, "powerList", ProtocolType.OBJECT),
|
||||
ENERGY_LIST(0xA4, "energyList", ProtocolType.OBJECT),
|
||||
BIG_INTEGER_LIST(0xA5, "bigIntegerList", ProtocolType.OBJECT),
|
||||
IDENTIFIER_LIST(0xA6, "identifierList", ProtocolType.OBJECT),
|
||||
SPEED_LIST(0xA7, "speedList", ProtocolType.OBJECT),
|
||||
PROGRAM_INSTRUCTION_LIST(0xA8, "programInstructionList", ProtocolType.OBJECT),
|
||||
WEIGHT_POUND_LIST(0xA9, "weightPoundList", ProtocolType.OBJECT),
|
||||
LOCALE_STRING_LIST(0xAA, "localeStringList", ProtocolType.OBJECT),
|
||||
TEASPOON_LIST(0xAB, "teaspoonList", ProtocolType.OBJECT),
|
||||
TABLESPOON_LIST(0xAC, "tablespoonList", ProtocolType.OBJECT),
|
||||
CUP_LIST(0xAD, "cupList", ProtocolType.OBJECT),
|
||||
PIECE_LIST(0xAE, "pieceList", ProtocolType.OBJECT),
|
||||
BYTE_LENGTH_LIST(0xAF, "byteLengthList", ProtocolType.OBJECT),
|
||||
UUID_LIST(0xB0, "uuidList", ProtocolType.OBJECT),
|
||||
TIMEZONE_LIST(0xB1, "timezoneList", ProtocolType.OBJECT),
|
||||
CSV_LIST(0xB2, "csvList", ProtocolType.OBJECT),
|
||||
LEAF_LIST(0xB3, "leafList", ProtocolType.OBJECT),
|
||||
BUNCH_LIST(0xB4, "bunchList", ProtocolType.OBJECT),
|
||||
CASKET_LIST(0xB5, "casketList", ProtocolType.OBJECT),
|
||||
PINCH_LIST(0xB6, "pinchList", ProtocolType.OBJECT),
|
||||
STALK_LIST(0xB7, "stalkList", ProtocolType.OBJECT),
|
||||
STICK_LIST(0xB8, "stickList", ProtocolType.OBJECT),
|
||||
BRANCH_LIST(0xB9, "branchList", ProtocolType.OBJECT),
|
||||
TRAY_LIST(0xBA, "trayList", ProtocolType.OBJECT),
|
||||
PORTION_LIST(0xBB, "portionList", ProtocolType.OBJECT),
|
||||
UTC_DATE_TIME_LIST(0xBD, "utcDateTimeList", ProtocolType.OBJECT),
|
||||
PROGRAM_RUN_SUMMARY_LIST(0xBE, "programRunSummaryList", ProtocolType.OBJECT),
|
||||
PROGRAM_SESSION_SUMMARY_LIST(0xBF, "programSessionSummaryList", ProtocolType.OBJECT),
|
||||
LIQUID_VOLUME_THROUGHPUT_LIST(0xC0, "liquidVolumeThroughputList", ProtocolType.OBJECT),
|
||||
WEIGHT_OUNCES_LIST(0xC1, "weightOuncesList", ProtocolType.OBJECT),
|
||||
RECTANGLE_2D_LIST(0xC2, "rectangle2DList", ProtocolType.OBJECT);
|
||||
|
||||
public final int id;
|
||||
public final String type;
|
||||
public final ProtocolType protocolType;
|
||||
|
||||
ContentType(int id, String type, ProtocolType protocolType) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.protocolType = protocolType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ContentType.class.getSimpleName() + "[", "]").add("id=" + id)
|
||||
.add("type='" + type + "'").add("protocolType=" + protocolType).toString();
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Data Type Description (DTD) identifiers from the device description.
|
||||
* Based on HC_INT_BSH_DTD.xml.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum DataType {
|
||||
BOOLEAN(0x00, "Boolean"),
|
||||
UINT8(0x80, "UInt8"),
|
||||
UINT16(0x81, "UInt16"),
|
||||
UINT32(0x82, "UInt32"),
|
||||
UINT64(0x83, "UInt64"),
|
||||
INT8(0x84, "Int8"),
|
||||
INT16(0x85, "Int16"),
|
||||
INT32(0x86, "Int32"),
|
||||
INT64(0x87, "Int64"),
|
||||
FLOAT(0x88, "Float"),
|
||||
DOUBLE(0x89, "Double"),
|
||||
DATE_TIME(0x8A, "DateTime"),
|
||||
STRING(0x8B, "String"),
|
||||
BYTE_STRING(0x8C, "ByteString"),
|
||||
GENERIC_JSON_DATA(0x8D, "GenericJsonData"),
|
||||
|
||||
// Conversion types: COM converts between content transfer type and internal representation
|
||||
FLOAT_UINT8(0xA0, "Float_UInt8"),
|
||||
FLOAT_UINT16(0xA1, "Float_UInt16"),
|
||||
FLOAT_UINT32(0xA2, "Float_UInt32"),
|
||||
FLOAT_UINT64(0xA3, "Float_UInt64"),
|
||||
FLOAT_INT8(0xA4, "Float_Int8"),
|
||||
FLOAT_INT16(0xA5, "Float_Int16"),
|
||||
FLOAT_INT32(0xA6, "Float_Int32"),
|
||||
FLOAT_INT64(0xA7, "Float_Int64"),
|
||||
DATE_DATE_TIME(0xA8, "Date_DateTime"),
|
||||
TIME_DATE_TIME(0xA9, "Time_DateTime"),
|
||||
MAC_BYTE_ARRAY6(0xAA, "MAC_ByteArray6"),
|
||||
RGB(0xAB, "RGB"),
|
||||
|
||||
// Composite types
|
||||
POINT2D_UINT16(0x0101, "Point2D_UInt16"),
|
||||
POINT2D_FLOAT(0x0108, "Point2D_Float"),
|
||||
LINE2D_UINT16(0x0121, "Line2D_UInt16"),
|
||||
LINE2D_FLOAT(0x0128, "Line2D_Float"),
|
||||
POSE2D_UINT16(0x0141, "Pose2D_UInt16"),
|
||||
POSE2D_FLOAT(0x0148, "Pose2D_Float");
|
||||
|
||||
public final int id;
|
||||
public final String type;
|
||||
|
||||
DataType(int id, String type) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this data type represents a raw integer wire encoding (UInt8-Int64).
|
||||
* When a contentType has protocolType FLOAT but the wire encoding is a raw integer,
|
||||
* the value is a fixed-point integer that needs scaling.
|
||||
*/
|
||||
public boolean isRawInteger() {
|
||||
return this == UINT8 || this == UINT16 || this == UINT32 || this == UINT64 || this == INT8 || this == INT16
|
||||
|| this == INT32 || this == INT64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", DataType.class.getSimpleName() + "[", "]").add("id=" + id)
|
||||
.add("type='" + type + "'").toString();
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Device description root model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record DeviceDescription(@Nullable StatusList statusList, @Nullable SettingList settingList,
|
||||
@Nullable EventList eventList, @Nullable CommandList commandList, @Nullable OptionList optionList,
|
||||
@Nullable ProgramGroup programGroup, @Nullable ActiveProgram activeProgram,
|
||||
@Nullable SelectedProgram selectedProgram, @Nullable ProtectionPort protectionPort,
|
||||
@Nullable EnumerationTypeList enumerationTypeList) {
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Device description type model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum DeviceDescriptionType {
|
||||
STATUS_LIST,
|
||||
STATUS,
|
||||
SETTING_LIST,
|
||||
SETTING,
|
||||
EVENT_LIST,
|
||||
EVENT,
|
||||
COMMAND_LIST,
|
||||
COMMAND,
|
||||
OPTION_LIST,
|
||||
OPTION,
|
||||
PROGRAM_GROUP,
|
||||
PROGRAM,
|
||||
PROGRAM_OPTION,
|
||||
SELECTED_PROGRAM,
|
||||
PROTECTION_PORT,
|
||||
ACTIVE_PROGRAM,
|
||||
ENUMERATION_TYPE,
|
||||
UNKNOWN
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Enumeration model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Enumeration(int value, String valueKey) {
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
|
||||
/**
|
||||
* Enumeration type model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record EnumerationType(int enId, String enKey, @Nullable Integer subsetOf, @Nullable String subsetOfKey,
|
||||
DoubleKeyMap<Integer, String, Enumeration> enumerations) {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
|
||||
/**
|
||||
* Enumeration type list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record EnumerationTypeList(DoubleKeyMap<Integer, String, EnumerationType> enumerationTypes) {
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Event model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Event(int uid, String key, ContentType contentType, @Nullable DataType dataType, Handling handling,
|
||||
Level level, @Nullable Integer enumerationType, String enumerationTypeKey)
|
||||
implements
|
||||
KeyProvider,
|
||||
ContentTypeProvider,
|
||||
EnumerationTypeProvider,
|
||||
DataTypeProvider {
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Event list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record EventList(int uid, String key, DoubleKeyMap<Integer, String, EventList> eventLists,
|
||||
DoubleKeyMap<Integer, String, Event> events) implements KeyProvider {
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Execution model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Execution {
|
||||
NONE,
|
||||
SELECT_ONLY,
|
||||
START_ONLY,
|
||||
SELECT_AND_START
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Handling model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Handling {
|
||||
NONE,
|
||||
ACKNOWLEDGE,
|
||||
DECISION
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Level model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Level {
|
||||
HINT,
|
||||
INFO,
|
||||
WARNING,
|
||||
ALERT,
|
||||
CRITICAL
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.RangeProvider;
|
||||
|
||||
/**
|
||||
* Option model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*
|
||||
* @param access NONE, READ, READ_WRITE, READ_STATIC
|
||||
* @param dataType the data type identifier (refDID) from the device description
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Option(int uid, String key, ContentType contentType, @Nullable DataType dataType, @Nullable Number min,
|
||||
@Nullable Number max, @Nullable Number stepSize, @Nullable String defaultValue, @Nullable String initValue,
|
||||
@Nullable Integer enumerationType, @Nullable String enumerationTypeKey, boolean available, Access access,
|
||||
boolean notifyOnChange, boolean liveUpdate)
|
||||
implements
|
||||
AccessProvider,
|
||||
AvailableProvider,
|
||||
RangeProvider,
|
||||
KeyProvider,
|
||||
EnumerationTypeProvider,
|
||||
ContentTypeProvider,
|
||||
DataTypeProvider {
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Option list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record OptionList(int uid, String key, boolean available, Access access,
|
||||
DoubleKeyMap<Integer, String, OptionList> optionLists,
|
||||
DoubleKeyMap<Integer, String, Option> options) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Program model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Program(int uid, String key, boolean available, Execution execution,
|
||||
List<ProgramOption> options) implements AvailableProvider, KeyProvider {
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Program group model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ProgramGroup(int uid, String key, boolean available,
|
||||
DoubleKeyMap<Integer, String, ProgramGroup> programGroups,
|
||||
DoubleKeyMap<Integer, String, Program> programs) implements AvailableProvider, KeyProvider {
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.RangeProvider;
|
||||
|
||||
/**
|
||||
* Program option model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ProgramOption(int refUid, String refKey, boolean available, Access access, boolean liveUpdate,
|
||||
@Nullable Number min, @Nullable Number max, @Nullable Number stepSize, String defaultValue,
|
||||
@Nullable Integer enumerationType, @Nullable String enumerationTypeKey)
|
||||
implements
|
||||
AccessProvider,
|
||||
AvailableProvider,
|
||||
RangeProvider,
|
||||
EnumerationTypeProvider {
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Protection port model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ProtectionPort(int uid, String key, boolean available,
|
||||
Access access) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Protocol type model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ProtocolType {
|
||||
BOOLEAN("Boolean"),
|
||||
INTEGER("Integer"),
|
||||
FLOAT("Float"),
|
||||
STRING("String"),
|
||||
OBJECT("Object");
|
||||
|
||||
private final String value;
|
||||
|
||||
ProtocolType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Selected program model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record SelectedProgram(int uid, String key, boolean available, Access access, boolean fullOptionSet,
|
||||
@Nullable Boolean validate) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.RangeProvider;
|
||||
|
||||
/**
|
||||
* Setting model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*
|
||||
* @param dataType the data type identifier (refDID) from the device description
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Setting(int uid, String key, ContentType contentType, @Nullable DataType dataType, @Nullable Number min,
|
||||
@Nullable Number max, @Nullable Number stepSize, boolean available, Access access, @Nullable String initValue,
|
||||
@Nullable Integer enumerationType, @Nullable String enumerationTypeKey, boolean notifyOnChange,
|
||||
boolean passwordProtected)
|
||||
implements
|
||||
AccessProvider,
|
||||
AvailableProvider,
|
||||
RangeProvider,
|
||||
KeyProvider,
|
||||
EnumerationTypeProvider,
|
||||
ContentTypeProvider,
|
||||
DataTypeProvider {
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Setting list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record SettingList(int uid, String key, boolean available, Access access,
|
||||
DoubleKeyMap<Integer, String, SettingList> settingLists,
|
||||
DoubleKeyMap<Integer, String, Setting> settings) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.ContentTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.DataTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.EnumerationTypeProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.RangeProvider;
|
||||
|
||||
/**
|
||||
* Status model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*
|
||||
* @param access possible values: READ, READ_STATIC, NONE
|
||||
* @param dataType the data type identifier (refDID) from the device description
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Status(int uid, String key, ContentType contentType, @Nullable DataType dataType, @Nullable Number min,
|
||||
@Nullable Number max, @Nullable Number stepSize, @Nullable Integer enumerationType,
|
||||
@Nullable String enumerationTypeKey, boolean available, Access access, boolean notifyOnChange,
|
||||
@Nullable String initValue)
|
||||
implements
|
||||
AccessProvider,
|
||||
AvailableProvider,
|
||||
RangeProvider,
|
||||
KeyProvider,
|
||||
EnumerationTypeProvider,
|
||||
ContentTypeProvider,
|
||||
DataTypeProvider {
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AccessProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.AvailableProvider;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.provider.KeyProvider;
|
||||
|
||||
/**
|
||||
* Status list model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record StatusList(int uid, String key, boolean available, Access access,
|
||||
DoubleKeyMap<Integer, String, StatusList> statusLists,
|
||||
DoubleKeyMap<Integer, String, Status> statuses) implements AccessProvider, AvailableProvider, KeyProvider {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.change;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Change model from the device description.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record Change(@Nullable Object from, @Nullable Object to) {
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.change;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescriptionType;
|
||||
|
||||
/**
|
||||
* Device description change model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record DeviceDescriptionChange(int uid, String key, DeviceDescriptionType type, @Nullable Integer parentUid,
|
||||
@Nullable String parentKey, @Nullable DeviceDescriptionType parentType,
|
||||
@Nullable HashMap<String, Change> changes, @Nullable Object object) {
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Access;
|
||||
|
||||
/**
|
||||
* Provider interface for access information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault(value = { DefaultLocation.TYPE_BOUND })
|
||||
public interface AccessProvider {
|
||||
/**
|
||||
* Get the access information.
|
||||
*
|
||||
* @return the access information
|
||||
*/
|
||||
Access access();
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Provider interface for availability information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault(value = { DefaultLocation.TYPE_BOUND })
|
||||
public interface AvailableProvider {
|
||||
/**
|
||||
* Get the availability information.
|
||||
*
|
||||
* @return {@code true} if available, {@code false} otherwise
|
||||
*/
|
||||
boolean available();
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
|
||||
/**
|
||||
* Provider interface for content type information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault(value = { DefaultLocation.TYPE_BOUND })
|
||||
public interface ContentTypeProvider {
|
||||
/**
|
||||
* Get the content type.
|
||||
*
|
||||
* @return the content type
|
||||
*/
|
||||
ContentType contentType();
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DataType;
|
||||
|
||||
/**
|
||||
* Provider interface for data type information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface DataTypeProvider {
|
||||
/**
|
||||
* Get the data type.
|
||||
*
|
||||
* @return the data type, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
DataType dataType();
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provider interface for enumeration type information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface EnumerationTypeProvider {
|
||||
/**
|
||||
* Get the enumeration type.
|
||||
*
|
||||
* @return the enumeration type, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
Integer enumerationType();
|
||||
|
||||
/**
|
||||
* Get the enumeration type key.
|
||||
*
|
||||
* @return the enumeration type key, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
String enumerationTypeKey();
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Provider interface for key information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault(value = { DefaultLocation.TYPE_BOUND })
|
||||
public interface KeyProvider {
|
||||
/**
|
||||
* Get the key.
|
||||
*
|
||||
* @return the key
|
||||
*/
|
||||
String key();
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.model.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provider interface for range information.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface RangeProvider {
|
||||
/**
|
||||
* Get the minimum value.
|
||||
*
|
||||
* @return the minimum value, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
Number min();
|
||||
|
||||
/**
|
||||
* Get the maximum value.
|
||||
*
|
||||
* @return the maximum value, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
Number max();
|
||||
|
||||
/**
|
||||
* Get the step size.
|
||||
*
|
||||
* @return the step size, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
Number stepSize();
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.description.xml.converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.utils.StringUtils;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.xml.converter.AbstractConverter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Access;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ActiveProgram;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Command;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.CommandList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ContentType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DataType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.DeviceDescription;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Enumeration;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.EnumerationType;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.EnumerationTypeList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Event;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.EventList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Execution;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Handling;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Level;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Option;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.OptionList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Program;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ProgramGroup;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ProgramOption;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.ProtectionPort;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.SelectedProgram;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Setting;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.SettingList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.Status;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.description.model.StatusList;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.model.FeatureMapping;
|
||||
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
||||
|
||||
/**
|
||||
* XStream converter for device description XML.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DeviceDescriptionConverter extends AbstractConverter<DeviceDescription, FeatureMapping> {
|
||||
|
||||
@Override
|
||||
public DeviceDescription process(HierarchicalStreamReader reader, @Nullable FeatureMapping featureMapping) {
|
||||
List<StatusList> statusList = new ArrayList<>();
|
||||
List<SettingList> settingList = new ArrayList<>();
|
||||
List<EventList> eventList = new ArrayList<>();
|
||||
List<CommandList> commandList = new ArrayList<>();
|
||||
List<OptionList> optionList = new ArrayList<>();
|
||||
List<ProgramGroup> programGroups = new ArrayList<>();
|
||||
List<ActiveProgram> activeProgramList = new ArrayList<>();
|
||||
List<SelectedProgram> selectedProgramList = new ArrayList<>();
|
||||
List<ProtectionPort> protectionPortList = new ArrayList<>();
|
||||
List<EnumerationTypeList> enumerationTypeList = new ArrayList<>();
|
||||
|
||||
read(reader, featureMapping, statusList, settingList, eventList, commandList, optionList, programGroups,
|
||||
activeProgramList, selectedProgramList, protectionPortList, enumerationTypeList);
|
||||
|
||||
return new DeviceDescription(statusList.stream().findFirst().orElse(null),
|
||||
settingList.stream().findFirst().orElse(null), eventList.stream().findFirst().orElse(null),
|
||||
commandList.stream().findFirst().orElse(null), optionList.stream().findFirst().orElse(null),
|
||||
programGroups.stream().findFirst().orElse(null), activeProgramList.stream().findFirst().orElse(null),
|
||||
selectedProgramList.stream().findFirst().orElse(null),
|
||||
protectionPortList.stream().findFirst().orElse(null),
|
||||
enumerationTypeList.stream().findFirst().orElse(null));
|
||||
}
|
||||
|
||||
private void read(HierarchicalStreamReader reader, @Nullable FeatureMapping featureMapping,
|
||||
List<StatusList> statusList, List<SettingList> settingList, List<EventList> eventList,
|
||||
List<CommandList> commandList, List<OptionList> optionList, List<ProgramGroup> programGroups,
|
||||
List<ActiveProgram> activeProgramList, List<SelectedProgram> selectedProgramList,
|
||||
List<ProtectionPort> protectionPortList, List<EnumerationTypeList> enumerationTypeList) {
|
||||
if (featureMapping == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
|
||||
if ("statusList".equals(nodeName)) {
|
||||
statusList.add(readStatusList(reader, featureMapping));
|
||||
} else if ("settingList".equals(nodeName)) {
|
||||
settingList.add(readSettingList(reader, featureMapping));
|
||||
} else if ("eventList".equals(nodeName)) {
|
||||
eventList.add(readEventList(reader, featureMapping));
|
||||
} else if ("commandList".equals(nodeName)) {
|
||||
commandList.add(readCommandList(reader, featureMapping));
|
||||
} else if ("optionList".equals(nodeName)) {
|
||||
optionList.add(readOptionList(reader, featureMapping));
|
||||
} else if ("programGroup".equals(nodeName)) {
|
||||
programGroups.add(readProgramGroup(reader, featureMapping));
|
||||
} else if ("activeProgram".equals(nodeName)) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = mapBoolean(reader.getAttribute("available"), true);
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
var fullOptionSet = mapBoolean(reader.getAttribute("fullOptionSet"), true);
|
||||
var validateString = reader.getAttribute("validate");
|
||||
Boolean validate = validateString == null ? null : Boolean.parseBoolean(validateString);
|
||||
|
||||
activeProgramList.add(new ActiveProgram(uid, key, available, access, fullOptionSet, validate));
|
||||
} else if ("selectedProgram".equals(nodeName)) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = mapBoolean(reader.getAttribute("available"), true);
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
var fullOptionSet = mapBoolean(reader.getAttribute("fullOptionSet"), true);
|
||||
var validateString = reader.getAttribute("validate");
|
||||
Boolean validate = validateString == null ? null : Boolean.parseBoolean(validateString);
|
||||
|
||||
selectedProgramList.add(new SelectedProgram(uid, key, available, access, fullOptionSet, validate));
|
||||
} else if ("protectionPort".equals(nodeName)) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = mapBoolean(reader.getAttribute("available"), false);
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
|
||||
protectionPortList.add(new ProtectionPort(uid, key, available, access));
|
||||
} else if ("enumerationTypeList".equals(nodeName)) {
|
||||
enumerationTypeList.add(readEnumerationTypeList(reader, featureMapping));
|
||||
}
|
||||
|
||||
reader.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
private EnumerationTypeList readEnumerationTypeList(HierarchicalStreamReader reader,
|
||||
FeatureMapping featureMapping) {
|
||||
DoubleKeyMap<Integer, String, EnumerationType> enumerationTypes = new DoubleKeyMap<>();
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
if ("enumerationType".equals(reader.getNodeName())) {
|
||||
var enumerationType = readEnumerationType(reader, featureMapping);
|
||||
enumerationTypes.put(enumerationType.enId(), enumerationType.enKey(), enumerationType);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
return new EnumerationTypeList(enumerationTypes);
|
||||
}
|
||||
|
||||
private EnumerationType readEnumerationType(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var enId = mapHexId(reader.getAttribute("enid"));
|
||||
var subsetOf = mapHexIdNullable(reader.getAttribute("subsetOf"));
|
||||
var subsetOfKey = featureMapping.mapEnumIdToKeyNullable(subsetOf);
|
||||
String enKey;
|
||||
if (subsetOfKey != null) {
|
||||
enKey = "%s.%d.SubSetOf.%d".formatted(subsetOfKey, enId, subsetOf);
|
||||
} else {
|
||||
enKey = featureMapping.mapEnumIdToKey(enId);
|
||||
}
|
||||
|
||||
DoubleKeyMap<Integer, String, Enumeration> enumerations = new DoubleKeyMap<>();
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
if ("enumeration".equals(reader.getNodeName())) {
|
||||
var value = mapIntegerNullable(reader.getAttribute("value"));
|
||||
if (value != null) {
|
||||
var valueKey = featureMapping.mapEnumValueToKey(Objects.requireNonNullElse(subsetOf, enId), value);
|
||||
var enumeration = new Enumeration(value, valueKey);
|
||||
enumerations.put(enumeration.value(), enumeration.valueKey(), enumeration);
|
||||
}
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
return new EnumerationType(enId, enKey, subsetOf, subsetOfKey, enumerations);
|
||||
}
|
||||
|
||||
private SettingList readSettingList(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
|
||||
DoubleKeyMap<Integer, String, SettingList> subLists = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Setting> settings = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("setting".equals(nodeName)) {
|
||||
var setting = readSetting(reader, featureMapping);
|
||||
settings.put(setting.uid(), setting.key(), setting);
|
||||
} else if ("settingList".equals(nodeName)) {
|
||||
var settingList = readSettingList(reader, featureMapping);
|
||||
subLists.put(settingList.uid(), settingList.key(), settingList);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new SettingList(uid, key, available, access, subLists, settings);
|
||||
}
|
||||
|
||||
private Setting readSetting(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid")); // required
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var contentType = mapContentType(mapHexId(reader.getAttribute("refCID"))); // required
|
||||
var dataType = mapDataType(reader.getAttribute("refDID"));
|
||||
var min = mapNumberNullable(reader.getAttribute("min"));
|
||||
var max = mapNumberNullable(reader.getAttribute("max"));
|
||||
var stepSize = mapNumberNullable(reader.getAttribute("stepSize"));
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available")); // required
|
||||
var access = mapAccess(reader.getAttribute("access")); // required
|
||||
var initValue = reader.getAttribute("initValue");
|
||||
var enumerationType = mapHexIdNullable(reader.getAttribute("enumerationType"));
|
||||
var enumerationKey = featureMapping.mapEnumIdToKeyNullable(enumerationType);
|
||||
var notifyOnChange = mapBoolean(reader.getAttribute("notifyOnChange"), true);
|
||||
var passwordProtected = mapBoolean(reader.getAttribute("passwordProtected"), false);
|
||||
|
||||
return new Setting(uid, key, contentType, dataType, min, max, stepSize, available, access, initValue,
|
||||
enumerationType, enumerationKey, notifyOnChange, passwordProtected);
|
||||
}
|
||||
|
||||
private EventList readEventList(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
|
||||
DoubleKeyMap<Integer, String, EventList> subLists = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Event> events = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("event".equals(nodeName)) {
|
||||
var event = readEvent(reader, featureMapping);
|
||||
events.put(event.uid(), event.key(), event);
|
||||
} else if ("eventList".equals(nodeName)) {
|
||||
var eventList = readEventList(reader, featureMapping);
|
||||
subLists.put(eventList.uid(), eventList.key(), eventList);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new EventList(uid, key, subLists, events);
|
||||
}
|
||||
|
||||
private Event readEvent(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid")); // required
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var contentType = mapContentType(mapHexId(reader.getAttribute("refCID"), 3)); // fixed "03"
|
||||
var dataType = mapDataType(reader.getAttribute("refDID"));
|
||||
var handling = mapHandling(reader.getAttribute("handling")); // required
|
||||
var level = mapLevel(reader.getAttribute("level")); // required
|
||||
var enumerationType = mapHexId(reader.getAttribute("enumerationType"), 1); // fixed "0001"
|
||||
var enumerationKey = featureMapping.mapEnumIdToKey(enumerationType);
|
||||
|
||||
return new Event(uid, key, contentType, dataType, handling, level, enumerationType, enumerationKey);
|
||||
}
|
||||
|
||||
private StatusList readStatusList(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
|
||||
DoubleKeyMap<Integer, String, StatusList> subLists = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Status> statusMap = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("status".equals(nodeName)) {
|
||||
var status = readStatus(reader, featureMapping);
|
||||
statusMap.put(status.uid(), status.key(), status);
|
||||
} else if ("statusList".equals(nodeName)) {
|
||||
var statusList = readStatusList(reader, featureMapping);
|
||||
subLists.put(statusList.uid(), statusList.key(), statusList);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new StatusList(uid, key, available, access, subLists, statusMap);
|
||||
}
|
||||
|
||||
private Status readStatus(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid")); // required
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var contentType = mapContentType(mapHexId(reader.getAttribute("refCID")));
|
||||
var dataType = mapDataType(reader.getAttribute("refDID"));
|
||||
var min = mapNumberNullable(reader.getAttribute("min"));
|
||||
var max = mapNumberNullable(reader.getAttribute("max"));
|
||||
var stepSize = mapNumberNullable(reader.getAttribute("stepSize"));
|
||||
var enumerationType = mapHexIdNullable(reader.getAttribute("enumerationType"));
|
||||
var enumerationKey = featureMapping.mapEnumIdToKeyNullable(enumerationType);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available")); // required
|
||||
var access = mapAccess(reader.getAttribute("access")); // required
|
||||
var notifyOnChange = mapBoolean(reader.getAttribute("notifyOnChange"), true);
|
||||
var initValue = reader.getAttribute("initValue");
|
||||
|
||||
return new Status(uid, key, contentType, dataType, min, max, stepSize, enumerationType, enumerationKey,
|
||||
available, access, notifyOnChange, initValue);
|
||||
}
|
||||
|
||||
private CommandList readCommandList(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
|
||||
DoubleKeyMap<Integer, String, CommandList> commandLists = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Command> commands = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("command".equals(nodeName)) {
|
||||
var command = readCommand(reader, featureMapping);
|
||||
commands.put(command.uid(), command.key(), command);
|
||||
} else if ("commandList".equals(nodeName)) {
|
||||
var commandList = readCommandList(reader, featureMapping);
|
||||
commandLists.put(commandList.uid(), commandList.key(), commandList);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new CommandList(uid, key, available, access, commandLists, commands);
|
||||
}
|
||||
|
||||
private Command readCommand(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid")); // required
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var contentType = mapContentType(mapHexIdNullable(reader.getAttribute("refCID"))); // required
|
||||
var dataType = mapDataType(reader.getAttribute("refDID"));
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available")); // required
|
||||
var access = mapAccess(reader.getAttribute("access")); // required
|
||||
var enumerationType = mapHexIdNullable(reader.getAttribute("enumerationType"));
|
||||
var enumerationKey = featureMapping.mapEnumIdToKeyNullable(enumerationType);
|
||||
var min = mapNumberNullable(reader.getAttribute("min"));
|
||||
var max = mapNumberNullable(reader.getAttribute("max"));
|
||||
var stepSize = mapNumberNullable(reader.getAttribute("stepSize"));
|
||||
var passwordProtected = mapBoolean(reader.getAttribute("passwordProtected"), false);
|
||||
var notifyOnChange = mapBoolean(reader.getAttribute("notifyOnChange"), false); // fixed "false"
|
||||
|
||||
return new Command(uid, key, contentType, dataType, available, access, enumerationType, enumerationKey, min,
|
||||
max, stepSize, passwordProtected, notifyOnChange);
|
||||
}
|
||||
|
||||
private OptionList readOptionList(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
|
||||
DoubleKeyMap<Integer, String, OptionList> optionLists = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Option> options = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("option".equals(nodeName) && !StringUtils.isBlank(reader.getAttribute("uid"))) {
|
||||
var option = readOption(reader, featureMapping);
|
||||
options.put(option.uid(), option.key(), option);
|
||||
} else if ("optionList".equals(nodeName)) {
|
||||
var optionList = readOptionList(reader, featureMapping);
|
||||
optionLists.put(optionList.uid(), optionList.key(), optionList);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new OptionList(uid, key, available, access, optionLists, options);
|
||||
}
|
||||
|
||||
private Option readOption(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid")); // required
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var contentType = mapContentType(mapHexIdNullable(reader.getAttribute("refCID"))); // required
|
||||
var dataType = mapDataType(reader.getAttribute("refDID"));
|
||||
var min = mapNumberNullable(reader.getAttribute("min"));
|
||||
var max = mapNumberNullable(reader.getAttribute("max"));
|
||||
var stepSize = mapNumberNullable(reader.getAttribute("stepSize"));
|
||||
var defaultValue = reader.getAttribute("default");
|
||||
var initValue = reader.getAttribute("initValue");
|
||||
var enumerationType = mapHexIdNullable(reader.getAttribute("enumerationType"));
|
||||
var enumerationKey = featureMapping.mapEnumIdToKeyNullable(enumerationType);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available")); // required
|
||||
var access = mapAccess(reader.getAttribute("access")); // required
|
||||
var notifyOnChange = mapBoolean(reader.getAttribute("notifyOnChange"), true);
|
||||
var liveUpdate = mapBoolean(reader.getAttribute("liveUpdate"), false);
|
||||
|
||||
return new Option(uid, key, contentType, dataType, min, max, stepSize, defaultValue, initValue, enumerationType,
|
||||
enumerationKey, available, access, notifyOnChange, liveUpdate);
|
||||
}
|
||||
|
||||
private ProgramGroup readProgramGroup(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
|
||||
DoubleKeyMap<Integer, String, ProgramGroup> programGroups = new DoubleKeyMap<>();
|
||||
DoubleKeyMap<Integer, String, Program> programs = new DoubleKeyMap<>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("programGroup".equals(nodeName)) {
|
||||
var programGroup = readProgramGroup(reader, featureMapping);
|
||||
programGroups.put(programGroup.uid(), programGroup.key(), programGroup);
|
||||
} else if ("program".equals(nodeName)) {
|
||||
var program = readProgram(reader, featureMapping);
|
||||
programs.put(program.uid(), program.key(), program);
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
return new ProgramGroup(uid, key, available, programGroups, programs);
|
||||
}
|
||||
|
||||
private Program readProgram(HierarchicalStreamReader reader, FeatureMapping featureMapping) {
|
||||
var uid = mapHexId(reader.getAttribute("uid"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var execution = mapExecution(reader.getAttribute("execution"), Execution.SELECT_AND_START);
|
||||
var programOptions = new ArrayList<ProgramOption>();
|
||||
readProgramOptions(reader, programOptions, featureMapping);
|
||||
|
||||
return new Program(uid, key, available, execution, programOptions);
|
||||
}
|
||||
|
||||
private void readProgramOptions(HierarchicalStreamReader reader, List<ProgramOption> optionList,
|
||||
FeatureMapping featureMapping) {
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
if ("option".equals(nodeName)) {
|
||||
var uid = mapHexId(reader.getAttribute("refUID"));
|
||||
var key = featureMapping.mapFeatureIdToKey(uid);
|
||||
var available = Boolean.parseBoolean(reader.getAttribute("available"));
|
||||
var access = mapAccess(reader.getAttribute("access"));
|
||||
var liveUpdate = mapBoolean(reader.getAttribute("liveUpdate"), false);
|
||||
var min = mapNumberNullable(reader.getAttribute("min"));
|
||||
var max = mapNumberNullable(reader.getAttribute("max"));
|
||||
var stepSize = mapNumberNullable(reader.getAttribute("stepSize"));
|
||||
var defaultValue = reader.getAttribute("default");
|
||||
var enumerationType = mapHexIdNullable(reader.getAttribute("enumerationType"));
|
||||
var enumerationKey = featureMapping.mapEnumIdToKeyNullable(enumerationType);
|
||||
|
||||
if (defaultValue == null) {
|
||||
defaultValue = "...";
|
||||
}
|
||||
optionList.add(new ProgramOption(uid, key, available, access, liveUpdate, min, max, stepSize,
|
||||
defaultValue, enumerationType, enumerationKey));
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
private Access mapAccess(@Nullable String access) {
|
||||
if (access == null) {
|
||||
return Access.NONE;
|
||||
}
|
||||
|
||||
return switch (access) {
|
||||
case "read" -> Access.READ;
|
||||
case "readWrite" -> Access.READ_WRITE;
|
||||
case "readStatic" -> Access.READ_STATIC;
|
||||
case "writeOnly" -> Access.WRITE_ONLY;
|
||||
default -> Access.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private Execution mapExecution(@Nullable String execution, Execution defaultExecution) {
|
||||
if (execution == null) {
|
||||
return defaultExecution;
|
||||
}
|
||||
|
||||
return switch (execution) {
|
||||
case "selectOnly" -> Execution.SELECT_ONLY;
|
||||
case "startOnly" -> Execution.START_ONLY;
|
||||
case "selectAndStart" -> Execution.SELECT_AND_START;
|
||||
default -> Execution.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private Handling mapHandling(@Nullable String handling) {
|
||||
if (handling == null) {
|
||||
return Handling.NONE;
|
||||
}
|
||||
|
||||
return switch (handling) {
|
||||
case "acknowledge" -> Handling.ACKNOWLEDGE;
|
||||
case "decision" -> Handling.DECISION;
|
||||
default -> Handling.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private Level mapLevel(@Nullable String level) {
|
||||
if (level == null) {
|
||||
return Level.HINT;
|
||||
}
|
||||
|
||||
return switch (level) {
|
||||
case "info" -> Level.INFO;
|
||||
case "alert" -> Level.ALERT;
|
||||
case "critical" -> Level.CRITICAL;
|
||||
case "warning" -> Level.WARNING;
|
||||
default -> Level.HINT;
|
||||
};
|
||||
}
|
||||
|
||||
private ContentType mapContentType(@Nullable Integer cid) {
|
||||
if (cid == null) {
|
||||
return ContentType.UNKNOWN;
|
||||
}
|
||||
for (ContentType contentType : ContentType.values()) {
|
||||
if (contentType.id == cid) {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
return ContentType.UNKNOWN;
|
||||
}
|
||||
|
||||
private @Nullable DataType mapDataType(@Nullable String hexIdString) {
|
||||
var did = mapHexIdNullable(hexIdString);
|
||||
if (did == null) {
|
||||
return null;
|
||||
}
|
||||
for (DataType dataType : DataType.values()) {
|
||||
if (dataType.id == did) {
|
||||
return dataType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.feature;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.xml.exception.ParseException;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.model.FeatureMapping;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.xml.converter.FeatureMappingConverter;
|
||||
|
||||
import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.XStreamException;
|
||||
import com.thoughtworks.xstream.io.xml.StaxDriver;
|
||||
|
||||
/**
|
||||
* Service for managing feature mappings.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FeatureMappingService {
|
||||
|
||||
private final FeatureMapping featureMapping;
|
||||
|
||||
public FeatureMappingService(Path featureMappingFile) throws ParseException {
|
||||
featureMapping = readFeatureMappingFile(featureMappingFile);
|
||||
}
|
||||
|
||||
public FeatureMappingService(InputStream inputStream) throws ParseException {
|
||||
featureMapping = readFeatureMappingFile(inputStream);
|
||||
}
|
||||
|
||||
public FeatureMapping getFeatureMapping() {
|
||||
return featureMapping;
|
||||
}
|
||||
|
||||
private FeatureMapping readFeatureMappingFile(Path featureMappingFile) throws ParseException {
|
||||
var xstream = createXStream();
|
||||
try {
|
||||
return (FeatureMapping) xstream.fromXML(featureMappingFile.toFile());
|
||||
} catch (XStreamException e) {
|
||||
throw new ParseException("Could not deserialize XML '%s'".formatted(featureMappingFile), e);
|
||||
}
|
||||
}
|
||||
|
||||
private FeatureMapping readFeatureMappingFile(InputStream inputStream) throws ParseException {
|
||||
var xstream = createXStream();
|
||||
try {
|
||||
return (FeatureMapping) xstream.fromXML(inputStream);
|
||||
} catch (XStreamException e) {
|
||||
throw new ParseException("Could not deserialize XML input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
private XStream createXStream() {
|
||||
var xstream = new XStream(new StaxDriver());
|
||||
xstream.allowTypesByWildcard(new String[] { FeatureMappingService.class.getPackageName() + ".**" });
|
||||
xstream.setClassLoader(getClass().getClassLoader());
|
||||
xstream.ignoreUnknownElements();
|
||||
xstream.alias("featureMappingFile", FeatureMapping.class);
|
||||
xstream.registerConverter(new FeatureMappingConverter());
|
||||
return xstream;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.feature.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Enum description model for feature mapping.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record EnumDescription(int enId, String enKey, Map<Integer, String> values) {
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.feature.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
|
||||
/**
|
||||
* Feature mapping model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record FeatureMapping(Map<Integer, String> features, Map<Integer, String> errors,
|
||||
DoubleKeyMap<Integer, String, EnumDescription> enums) {
|
||||
|
||||
public String mapFeatureIdToKey(Integer uid) {
|
||||
return features.getOrDefault(uid, String.valueOf(uid));
|
||||
}
|
||||
|
||||
public String mapErrorIdToKey(Integer eid) {
|
||||
return errors.getOrDefault(eid, String.valueOf(eid));
|
||||
}
|
||||
|
||||
public String mapEnumIdToKey(Integer enid) {
|
||||
EnumDescription desc = enums.getByKey1(enid);
|
||||
return desc != null ? desc.enKey() : String.valueOf(enid);
|
||||
}
|
||||
|
||||
public @Nullable String mapEnumIdToKeyNullable(@Nullable Integer enid) {
|
||||
if (enid == null) {
|
||||
return null;
|
||||
}
|
||||
return mapEnumIdToKey(enid);
|
||||
}
|
||||
|
||||
public String mapEnumValueToKey(Integer enid, Integer enumValue) {
|
||||
EnumDescription desc = enums.getByKey1(enid);
|
||||
if (desc != null) {
|
||||
String value = desc.values().get(enumValue);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return String.valueOf(enumValue);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.feature.xml.converter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.DoubleKeyMap;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.xml.converter.AbstractConverter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.model.EnumDescription;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.feature.model.FeatureMapping;
|
||||
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
||||
|
||||
/**
|
||||
* XStream converter for feature mapping XML.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FeatureMappingConverter extends AbstractConverter<FeatureMapping, Void> {
|
||||
|
||||
@Override
|
||||
public FeatureMapping process(HierarchicalStreamReader reader, @Nullable Void contextObject) {
|
||||
Map<Integer, String> featureMap = new HashMap<>();
|
||||
Map<Integer, String> errorMap = new HashMap<>();
|
||||
DoubleKeyMap<Integer, String, EnumDescription> enums = new DoubleKeyMap<>();
|
||||
|
||||
read(reader, featureMap, errorMap, enums);
|
||||
return new FeatureMapping(featureMap, errorMap, enums);
|
||||
}
|
||||
|
||||
private void read(HierarchicalStreamReader reader, Map<Integer, String> featureMap, Map<Integer, String> errorMap,
|
||||
DoubleKeyMap<Integer, String, EnumDescription> enums) {
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
String nodeName = reader.getNodeName();
|
||||
|
||||
if ("featureDescription".equals(nodeName)) {
|
||||
read(reader, featureMap, errorMap, enums);
|
||||
} else if ("feature".equals(nodeName)) {
|
||||
var uid = mapHexId(reader.getAttribute("refUID"));
|
||||
var value = reader.getValue();
|
||||
featureMap.put(uid, value);
|
||||
} else if ("errorDescription".equals(nodeName)) {
|
||||
read(reader, featureMap, errorMap, enums);
|
||||
} else if ("error".equals(nodeName)) {
|
||||
var id = mapHexId(reader.getAttribute("refEID"));
|
||||
var value = reader.getValue();
|
||||
errorMap.put(id, value);
|
||||
} else if ("enumDescriptionList".equals(nodeName)) {
|
||||
read(reader, featureMap, errorMap, enums);
|
||||
} else if ("enumDescription".equals(nodeName)) {
|
||||
var id = mapHexId(reader.getAttribute("refENID"));
|
||||
var key = reader.getAttribute("enumKey");
|
||||
var values = new HashMap<Integer, String>();
|
||||
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
if ("enumMember".equals(reader.getNodeName())) {
|
||||
var enumValue = mapIntegerNullable(reader.getAttribute("refValue"));
|
||||
var mappingValue = reader.getValue();
|
||||
if (enumValue != null) {
|
||||
values.put(enumValue, mappingValue);
|
||||
}
|
||||
}
|
||||
reader.moveUp();
|
||||
}
|
||||
|
||||
var enumDescription = new EnumDescription(id, key, values);
|
||||
enums.put(id, key, enumDescription);
|
||||
}
|
||||
|
||||
reader.moveUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.BINDING_PROFILES_PATH;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.LOCALE;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.common.utils.StringUtils.endsWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.common.json.adapter.OffsetDateTimeAdapter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.json.adapter.ApplianceProfileAdapter;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.model.ApplianceProfile;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
|
||||
/**
|
||||
*
|
||||
* Home Connect Direct appliance profile service.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ApplianceProfileService.class, scope = ServiceScope.SINGLETON, immediate = true)
|
||||
public class ApplianceProfileService {
|
||||
|
||||
private static final String JSON_EXTENSION = ".json";
|
||||
private static final String XML_EXTENSION = ".xml";
|
||||
|
||||
private final Logger logger;
|
||||
private final Gson gson;
|
||||
private final String profileDirectory;
|
||||
|
||||
@Activate
|
||||
public ApplianceProfileService() {
|
||||
this(BINDING_PROFILES_PATH);
|
||||
}
|
||||
|
||||
public ApplianceProfileService(String profileDirectory) {
|
||||
this.logger = LoggerFactory.getLogger(ApplianceProfileService.class);
|
||||
this.gson = new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter())
|
||||
.registerTypeAdapter(ApplianceProfile.class, new ApplianceProfileAdapter())
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
|
||||
this.profileDirectory = profileDirectory;
|
||||
|
||||
createProfileDirectory();
|
||||
}
|
||||
|
||||
public List<ApplianceProfile> getProfiles() {
|
||||
var profiles = new ArrayList<ApplianceProfile>();
|
||||
try {
|
||||
File directory = new File(profileDirectory);
|
||||
File[] jsonFiles = directory.listFiles((dir, name) -> name.toLowerCase(LOCALE).endsWith(JSON_EXTENSION));
|
||||
|
||||
if (jsonFiles != null) {
|
||||
for (File jsonFile : jsonFiles) {
|
||||
try (var reader = new FileReader(jsonFile, StandardCharsets.UTF_8)) {
|
||||
profiles.add(gson.fromJson(reader, ApplianceProfile.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException | JsonParseException | IOException e) {
|
||||
logger.error("Could not read profile files! error={}", e.getMessage());
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public @Nullable ApplianceProfile getProfile(String haId) {
|
||||
return getProfiles().stream().filter(applianceProfile -> Objects.equals(applianceProfile.haId(), haId))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public void deleteProfile(String haId) {
|
||||
try {
|
||||
File directory = new File(profileDirectory);
|
||||
File[] jsonFiles = directory.listFiles((dir, name) -> name.toLowerCase(LOCALE).endsWith(JSON_EXTENSION));
|
||||
|
||||
if (jsonFiles != null) {
|
||||
for (File jsonFile : jsonFiles) {
|
||||
try (var reader = new FileReader(jsonFile, StandardCharsets.UTF_8)) {
|
||||
var profile = gson.fromJson(reader, ApplianceProfile.class);
|
||||
if (Objects.equals(profile.haId(), haId)) {
|
||||
Files.delete(safePath(profileDirectory, profile.featureMappingFileName()));
|
||||
Files.delete(safePath(profileDirectory, profile.deviceDescriptionFileName()));
|
||||
Files.delete(safePath(profileDirectory, jsonFile.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException | IOException | JsonParseException e) {
|
||||
logger.error("Could not delete profile files! error={}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ApplianceProfile> uploadProfileZip(InputStream inputStream) {
|
||||
Path profilePath = null;
|
||||
List<Path> extractedFiles = new ArrayList<>();
|
||||
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory()
|
||||
&& (endsWith(entry.getName(), JSON_EXTENSION) || endsWith(entry.getName(), XML_EXTENSION))) {
|
||||
var path = safePath(profileDirectory, entry.getName());
|
||||
Files.copy(zipInputStream, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
extractedFiles.add(path);
|
||||
|
||||
if (endsWith(entry.getName(), JSON_EXTENSION)) {
|
||||
profilePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profilePath != null) {
|
||||
try (FileReader reader = new FileReader(profilePath.toFile(), StandardCharsets.UTF_8)) {
|
||||
return Optional.of(gson.fromJson(reader, ApplianceProfile.class));
|
||||
}
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
logger.debug("Could not save profile! error={}", e.getMessage());
|
||||
}
|
||||
|
||||
// Rollback extracted files on failure
|
||||
for (Path file : extractedFiles) {
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Could not delete extracted file! path={}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void downloadProfileZip(String haId, OutputStream outputStream) {
|
||||
var profile = getProfile(haId);
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var profileJsonContent = gson.toJson(profile);
|
||||
try (ZipOutputStream zos = new ZipOutputStream(outputStream, StandardCharsets.UTF_8);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(zos, StandardCharsets.UTF_8)) {
|
||||
// json
|
||||
ZipEntry zipEntry = new ZipEntry(haId + JSON_EXTENSION);
|
||||
zos.putNextEntry(zipEntry);
|
||||
writer.write(profileJsonContent);
|
||||
writer.flush();
|
||||
zos.closeEntry();
|
||||
|
||||
// original XMLs
|
||||
for (Path path : List.of(safePath(profileDirectory, profile.deviceDescriptionFileName()),
|
||||
safePath(profileDirectory, profile.featureMappingFileName()))) {
|
||||
if (Files.exists(path)) {
|
||||
ZipEntry fileEntry = new ZipEntry(path.getFileName().toString());
|
||||
zos.putNextEntry(fileEntry);
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
} else {
|
||||
logger.warn("Profile file {} does not exist!", profile.deviceDescriptionFileName());
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// ignore exception
|
||||
}
|
||||
}
|
||||
|
||||
private Path safePath(String directory, String fileName) throws IOException {
|
||||
Path resolved = Path.of(directory, fileName).normalize();
|
||||
if (!resolved.startsWith(Path.of(directory).normalize())) {
|
||||
throw new IOException("Path traversal attempt detected: " + fileName);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void createProfileDirectory() {
|
||||
try {
|
||||
Files.createDirectories(Paths.get(profileDirectory));
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not create profile directory! directory={}", profileDirectory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile.json.adapter;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.ZONE_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.model.AesCredentials;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.model.ApplianceProfile;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.model.Credentials;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.profile.model.TlsCredentials;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* Gson adapter for appliance profile serialization and deserialization.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ApplianceProfileAdapter extends TypeAdapter<ApplianceProfile> {
|
||||
|
||||
@Override
|
||||
public void write(@Nullable JsonWriter out, @Nullable ApplianceProfile value) throws IOException {
|
||||
if (out == null || value == null) {
|
||||
return;
|
||||
}
|
||||
out.beginObject();
|
||||
out.name("haId").value(value.haId());
|
||||
out.name("type").value(value.type());
|
||||
out.name("serialNumber").value(value.serialNumber());
|
||||
out.name("brand").value(value.brand());
|
||||
out.name("vib").value(value.vib());
|
||||
out.name("mac").value(value.mac());
|
||||
|
||||
Credentials credentials = value.credentials();
|
||||
if (credentials instanceof AesCredentials(String key, String iv)) {
|
||||
out.name("connectionType").value("AES");
|
||||
out.name("key").value(key);
|
||||
out.name("iv").value(iv);
|
||||
} else if (credentials instanceof TlsCredentials(String key)) {
|
||||
out.name("connectionType").value("TLS");
|
||||
out.name("key").value(key);
|
||||
}
|
||||
|
||||
out.name("featureMappingFileName").value(value.featureMappingFileName());
|
||||
out.name("deviceDescriptionFileName").value(value.deviceDescriptionFileName());
|
||||
out.name("created").value(value.created().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ApplianceProfile read(@Nullable JsonReader in) throws IOException {
|
||||
if (in == null || in.peek() == JsonToken.NULL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String haId = "";
|
||||
String type = "";
|
||||
String serialNumber = "";
|
||||
String brand = "";
|
||||
String vib = "";
|
||||
String mac = "";
|
||||
String connectionType = "AES";
|
||||
String key = "";
|
||||
String iv = "";
|
||||
String featureMappingFileName = "";
|
||||
String deviceDescriptionFileName = "";
|
||||
OffsetDateTime created = OffsetDateTime.now(ZONE_ID);
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
switch (in.nextName()) {
|
||||
case "haId" -> haId = in.nextString();
|
||||
case "type" -> type = in.nextString();
|
||||
case "serialNumber" -> serialNumber = in.nextString();
|
||||
case "brand" -> brand = in.nextString();
|
||||
case "vib" -> vib = in.nextString();
|
||||
case "mac" -> mac = in.nextString();
|
||||
case "connectionType" -> connectionType = in.nextString();
|
||||
case "key" -> key = in.nextString();
|
||||
case "iv" -> iv = in.nextString();
|
||||
case "featureMappingFileName" -> featureMappingFileName = in.nextString();
|
||||
case "deviceDescriptionFileName" -> deviceDescriptionFileName = in.nextString();
|
||||
case "created" -> {
|
||||
String createdStr = in.nextString();
|
||||
created = OffsetDateTime.parse(createdStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
}
|
||||
default -> in.skipValue();
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
Credentials credentials = "TLS".equals(connectionType) ? new TlsCredentials(key) : new AesCredentials(key, iv);
|
||||
|
||||
return new ApplianceProfile(haId, type, serialNumber, brand, vib, mac, credentials, featureMappingFileName,
|
||||
deviceDescriptionFileName, created);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* AES credentials model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record AesCredentials(String key, String iv) implements Credentials {
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Appliance profile model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record ApplianceProfile(String haId, String type, String serialNumber, String brand, String vib, String mac,
|
||||
Credentials credentials, String featureMappingFileName, String deviceDescriptionFileName,
|
||||
OffsetDateTime created) {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Credentials base model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public sealed interface Credentials permits AesCredentials, TlsCredentials {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.profile.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* TLS credentials model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record TlsCredentials(String key) implements Credentials {
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import static org.eclipse.jetty.websocket.api.extensions.Frame.Type.PING;
|
||||
import static org.eclipse.jetty.websocket.api.extensions.Frame.Type.PONG;
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
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.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.api.extensions.Frame;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.exception.WebSocketClientServiceException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract base class for WebSocket client services.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@WebSocket
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractWebSocketClientService implements WebSocketClientService {
|
||||
|
||||
private final Logger logger;
|
||||
private final WebSocketHandler webSocketHandler;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final URI applianceUri;
|
||||
private final Thing thing;
|
||||
|
||||
private @Nullable Instant lastMessageReceived;
|
||||
private @Nullable Session session;
|
||||
private @Nullable ScheduledFuture<?> pingFuture;
|
||||
private @Nullable ScheduledFuture<?> staleConnectionFuture;
|
||||
private @Nullable WebSocketClient webSocketClient;
|
||||
|
||||
public AbstractWebSocketClientService(Thing thing, URI applianceUri, WebSocketHandler webSocketHandler,
|
||||
ScheduledExecutorService scheduler) {
|
||||
logger = LoggerFactory.getLogger(AbstractWebSocketClientService.class);
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
this.scheduler = scheduler;
|
||||
this.applianceUri = applianceUri;
|
||||
this.thing = thing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws WebSocketClientServiceException {
|
||||
logger.debug("Connecting to {} ({}).", applianceUri, getThingUID());
|
||||
try {
|
||||
var webSocketClient = getWebSocketClient();
|
||||
if (webSocketClient != null) {
|
||||
webSocketClient.start();
|
||||
webSocketClient.connect(this, applianceUri).get();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not connect to {} ({}). error={}", applianceUri, getThingUID(), e.getMessage());
|
||||
throw new WebSocketClientServiceException("Could not connect to " + applianceUri, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopConnectionChecks();
|
||||
var webSocketClient = getWebSocketClient();
|
||||
if (webSocketClient != null) {
|
||||
try {
|
||||
logger.debug("Stop web socket client ({}).", applianceUri);
|
||||
webSocketClient.stop();
|
||||
} catch (Exception ignored) {
|
||||
// best-effort stop during cleanup
|
||||
}
|
||||
|
||||
logger.debug("Destroy web socket client ({}).", applianceUri);
|
||||
webSocketClient.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(Session session) {
|
||||
setSession(session);
|
||||
getWebSocketHandler().onWebSocketConnect();
|
||||
updateLastMessageReceived();
|
||||
startConnectionChecks(session);
|
||||
}
|
||||
|
||||
@OnWebSocketFrame
|
||||
public void onFrame(Session session, Frame frame) {
|
||||
if (PONG.equals(frame.getType())) {
|
||||
logger.trace("<< PONG ({})", thing.getUID());
|
||||
} else if (PING.equals(frame.getType())) {
|
||||
logger.trace("<< PING ({})", thing.getUID());
|
||||
sendPong(session);
|
||||
}
|
||||
|
||||
updateLastMessageReceived();
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onClose(Session session, int statusCode, String reason) {
|
||||
logger.debug("Closed websocket connection. status={}, reason={} thingUID={}", statusCode, reason,
|
||||
thing.getUID());
|
||||
stopConnectionChecks();
|
||||
getWebSocketHandler().onWebSocketClose();
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(@Nullable Session session, Throwable error) throws Exception {
|
||||
stopConnectionChecks();
|
||||
getWebSocketHandler().onWebSocketError(error);
|
||||
|
||||
if (session == null || !session.isOpen()) {
|
||||
getWebSocketHandler().onWebSocketClose();
|
||||
}
|
||||
}
|
||||
|
||||
protected WebSocketHandler getWebSocketHandler() {
|
||||
return webSocketHandler;
|
||||
}
|
||||
|
||||
protected @Nullable Instant getLastMessageReceived() {
|
||||
return lastMessageReceived;
|
||||
}
|
||||
|
||||
protected void updateLastMessageReceived() {
|
||||
lastMessageReceived = Instant.now();
|
||||
}
|
||||
|
||||
protected @Nullable Session getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
protected void setSession(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
protected ThingUID getThingUID() {
|
||||
return thing.getUID();
|
||||
}
|
||||
|
||||
protected @Nullable WebSocketClient getWebSocketClient() {
|
||||
return webSocketClient;
|
||||
}
|
||||
|
||||
protected void setWebSocketClient(WebSocketClient webSocketClient) {
|
||||
this.webSocketClient = webSocketClient;
|
||||
}
|
||||
|
||||
protected void sendPong(@Nullable Session session) {
|
||||
if (session != null && session.isOpen()) {
|
||||
try {
|
||||
session.getRemote().sendPong(ByteBuffer.allocate(0));
|
||||
logger.trace(">> PONG ({})", thing.getUID());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send PONG! error={} thingUID={}", e.getMessage(), thing.getUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendPing(@Nullable Session session) {
|
||||
if (session != null && session.isOpen()) {
|
||||
try {
|
||||
session.getRemote().sendPing(ByteBuffer.allocate(0));
|
||||
logger.trace(">> PING ({})", thing.getUID());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send PING! error={} thingUID={}", e.getMessage(), thing.getUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized void startConnectionChecks(Session session) {
|
||||
pingFuture = scheduler.scheduleWithFixedDelay(() -> sendPing(session), WS_PING_INITIAL_DELAY.toSeconds(),
|
||||
WS_PING_INTERVAL.toSeconds(), TimeUnit.SECONDS);
|
||||
|
||||
staleConnectionFuture = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (session.isOpen()) {
|
||||
var lastMessage = getLastMessageReceived();
|
||||
var now = Instant.now();
|
||||
if (lastMessage != null && now.isAfter(lastMessage.plus(WS_INACTIVITY_TIMEOUT))) {
|
||||
logger.debug("Last message received {} seconds ago. -> reconnect. (thingUID={})",
|
||||
Duration.between(lastMessage, now).toSeconds(), thing.getUID());
|
||||
try {
|
||||
session.disconnect();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not disconnect from session! error={}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, WS_INACTIVITY_CHECK_INITIAL_DELAY.toSeconds(), WS_INACTIVITY_CHECK_INTERVAL.toSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected synchronized void stopConnectionChecks() {
|
||||
ScheduledFuture<?> pingFuture = this.pingFuture;
|
||||
if (pingFuture != null) {
|
||||
pingFuture.cancel(true);
|
||||
}
|
||||
|
||||
ScheduledFuture<?> staleConnectionFuture = this.staleConnectionFuture;
|
||||
if (staleConnectionFuture != null) {
|
||||
staleConnectionFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
import org.conscrypt.PSKKeyManager;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* PSK key manager implementation using Conscrypt.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ConscryptPskKeyManager implements PSKKeyManager {
|
||||
|
||||
private static final String PSK_ALGORITHM = "PSK";
|
||||
|
||||
private final String identityHint;
|
||||
private final byte[] key;
|
||||
|
||||
public ConscryptPskKeyManager(String identityHint, byte[] key) {
|
||||
this.identityHint = identityHint;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String chooseServerKeyIdentityHint(@Nullable Socket socket) {
|
||||
return identityHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String chooseServerKeyIdentityHint(@Nullable SSLEngine sslEngine) {
|
||||
return identityHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String chooseClientKeyIdentity(@Nullable String s, @Nullable Socket socket) {
|
||||
return identityHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String chooseClientKeyIdentity(@Nullable String s, @Nullable SSLEngine sslEngine) {
|
||||
return identityHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SecretKey getKey(@Nullable String s, @Nullable String s1, @Nullable Socket socket) {
|
||||
return new SecretKeySpec(key, PSK_ALGORITHM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SecretKey getKey(@Nullable String s, @Nullable String s1, @Nullable SSLEngine sslEngine) {
|
||||
return new SecretKeySpec(key, PSK_ALGORITHM);
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.exception.WebSocketClientServiceException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* WebSocket client service using AES encryption.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@WebSocket
|
||||
@NonNullByDefault
|
||||
public class WebSocketAesClientService extends AbstractWebSocketClientService {
|
||||
|
||||
private static final String HMAC_SHA_256 = "HmacSHA256";
|
||||
private static final String AES_CBC_NO_PADDING = "AES/CBC/NoPadding";
|
||||
private static final String AES = "AES";
|
||||
private static final String ENC = "ENC";
|
||||
private static final String MAC = "MAC";
|
||||
|
||||
private final Cipher aesEncrypt;
|
||||
private final Cipher aesDecrypt;
|
||||
private final byte[] iv;
|
||||
private final byte[] macKey;
|
||||
private final Logger logger;
|
||||
|
||||
private byte[] lastRxHmac;
|
||||
private byte[] lastTxHmac;
|
||||
|
||||
public WebSocketAesClientService(Thing thing, URI uri, String base64EncodedKey,
|
||||
String base64EncodedInitializationVector, WebSocketHandler webSocketHandler,
|
||||
ScheduledExecutorService scheduler) throws WebSocketClientServiceException {
|
||||
super(thing, uri, webSocketHandler, scheduler);
|
||||
|
||||
try {
|
||||
logger = LoggerFactory.getLogger(WebSocketAesClientService.class);
|
||||
|
||||
byte[] key = Base64.getUrlDecoder().decode(base64EncodedKey);
|
||||
iv = Base64.getUrlDecoder().decode(base64EncodedInitializationVector);
|
||||
|
||||
// init AES
|
||||
byte[] encryptionKey = hmac(key, ENC.getBytes(StandardCharsets.UTF_8));
|
||||
SecretKeySpec keySpec = new SecretKeySpec(encryptionKey, AES);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
aesEncrypt = Cipher.getInstance(AES_CBC_NO_PADDING);
|
||||
aesEncrypt.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||
aesDecrypt = Cipher.getInstance(AES_CBC_NO_PADDING);
|
||||
aesDecrypt.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
|
||||
// init HMAC
|
||||
macKey = hmac(key, MAC.getBytes(StandardCharsets.UTF_8));
|
||||
lastRxHmac = new byte[16];
|
||||
lastTxHmac = new byte[16];
|
||||
|
||||
// websocket
|
||||
setWebSocketClient(new WebSocketClient(new HttpClient()));
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
|
||||
throw new WebSocketClientServiceException(e.getMessage(), e);
|
||||
} catch (InvalidKeyException e) {
|
||||
var message = e.getMessage();
|
||||
try {
|
||||
if (javax.crypto.Cipher.getMaxAllowedKeyLength(AES) < 256) {
|
||||
message = "The current cryptographic policy is set to 'limited', which restricts the use of stronger encryption algorithms and key lengths. "
|
||||
+ "To resolve this issue, ensure that the 'crypto.policy' property is set to 'unlimited' in the 'java.security' file located at: "
|
||||
+ "'<JAVA_HOME>/conf/security/java.security'. The unlimited policy is supported natively in your Java version.";
|
||||
}
|
||||
} catch (NoSuchAlgorithmException ignored) {
|
||||
// AES algorithm check failed, use original exception message
|
||||
}
|
||||
throw new WebSocketClientServiceException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
try {
|
||||
var encryptedMessage = encrypt(message);
|
||||
var session = getSession();
|
||||
|
||||
if (session != null && session.isOpen()) {
|
||||
logger.debug(">> {} ({})", message, getThingUID());
|
||||
logger.trace(">> {} ({})", HexUtils.bytesToHex(encryptedMessage), getWebSocketHandler());
|
||||
ByteBuffer buffer = ByteBuffer.wrap(encryptedMessage);
|
||||
session.getRemote().sendBytes(buffer);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send message! error={} thingUID={}", e.getMessage(), getThingUID());
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onBinaryMessage(Session session, InputStream inputStream)
|
||||
throws IOException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
|
||||
try (inputStream) {
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] message = output.toByteArray();
|
||||
|
||||
logger.trace("<< {} ({})", HexUtils.bytesToHex(message), getThingUID());
|
||||
var decryptedMessage = decrypt(message);
|
||||
var stringMessage = new String(decryptedMessage, StandardCharsets.UTF_8);
|
||||
logger.debug("<< {} ({})", stringMessage, getThingUID());
|
||||
getWebSocketHandler().onWebSocketMessage(stringMessage, this);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] hmac(byte[] key, byte[] msg) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA_256);
|
||||
SecretKeySpec secretKey = new SecretKeySpec(key, HMAC_SHA_256);
|
||||
mac.init(secretKey);
|
||||
return mac.doFinal(msg);
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] buf) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (buf.length < 32) {
|
||||
logger.error("Can not decrypt invalid message! Short message? {}", HexUtils.bytesToHex(buf));
|
||||
}
|
||||
|
||||
if (buf.length % 16 != 0) {
|
||||
logger.error("Unaligned message? Probably bad padding: {}", HexUtils.bytesToHex(buf));
|
||||
}
|
||||
|
||||
// Split the message into the encrypted message and the first 16 bytes of the HMAC
|
||||
byte[] encryptedMessage = Arrays.copyOfRange(buf, 0, buf.length - 16);
|
||||
byte[] applianceHmac = Arrays.copyOfRange(buf, buf.length - 16, buf.length);
|
||||
|
||||
// Compute the expected HMAC on the encrypted message
|
||||
byte[] directionAndLastHmac = concatenateByteArrays(new byte[] { 0x43 }, this.lastRxHmac);
|
||||
byte[] ourHmac = createHmacMessage(directionAndLastHmac, encryptedMessage);
|
||||
|
||||
if (!Arrays.equals(applianceHmac, ourHmac)) {
|
||||
logger.error("HMAC failure! appliance={} ourHmac={}, msgLength={}", HexUtils.bytesToHex(applianceHmac),
|
||||
HexUtils.bytesToHex(ourHmac), buf.length);
|
||||
}
|
||||
|
||||
this.lastRxHmac = applianceHmac;
|
||||
|
||||
// Decrypt the message with CBC, so the last message block is mixed in
|
||||
byte[] msg;
|
||||
msg = aesDecrypt.update(encryptedMessage);
|
||||
|
||||
// Check for padding and trim it off the end
|
||||
int padLen = msg[msg.length - 1] & 0xFF; // Convert to unsigned integer
|
||||
if (msg.length < padLen) {
|
||||
logger.error("Padding error! {}", HexUtils.bytesToHex(msg));
|
||||
}
|
||||
logger.trace("padding length={}", padLen);
|
||||
|
||||
return Arrays.copyOfRange(msg, 0, msg.length - padLen);
|
||||
}
|
||||
|
||||
public byte[] encrypt(String clearMsg) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Convert the UTF-8 string into a byte array
|
||||
byte[] clearMsgBytes = clearMsg.getBytes(StandardCharsets.UTF_8);
|
||||
logger.trace("encrypt: clearMsg={}", HexUtils.bytesToHex(clearMsgBytes));
|
||||
|
||||
// Pad the buffer, adding an extra block if necessary
|
||||
int padLen = 16 - (clearMsgBytes.length % 16);
|
||||
if (padLen == 1) {
|
||||
padLen += 16;
|
||||
}
|
||||
logger.trace("encrypt: padLen={}", padLen);
|
||||
byte[] pad = new byte[padLen];
|
||||
pad[0] = 0x00;
|
||||
|
||||
// Generate random bytes for the padding
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] randomBytes = new byte[padLen - 2];
|
||||
random.nextBytes(randomBytes);
|
||||
System.arraycopy(randomBytes, 0, pad, 1, padLen - 2);
|
||||
|
||||
pad[padLen - 1] = (byte) padLen;
|
||||
|
||||
// Combine clear message and padding
|
||||
byte[] paddedMsg = concatenateByteArrays(clearMsgBytes, pad);
|
||||
|
||||
// Encrypt the padded message with CBC, so there is chained state from the last cipher block sent
|
||||
byte[] encMsg;
|
||||
encMsg = aesEncrypt.update(paddedMsg);
|
||||
|
||||
// Compute the HMAC of the encrypted message, chaining the HMAC of the previous message plus direction 'E'
|
||||
byte[] directionAndLastHmac = concatenateByteArrays(new byte[] { 0x45 }, this.lastTxHmac);
|
||||
this.lastTxHmac = createHmacMessage(directionAndLastHmac, encMsg);
|
||||
|
||||
// Append the new HMAC to the message
|
||||
return concatenateByteArrays(encMsg, this.lastTxHmac);
|
||||
}
|
||||
|
||||
// HMAC an inbound or outbound message, chaining the last HMAC
|
||||
private byte[] createHmacMessage(byte[] direction, byte[] encMsg)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
byte[] hmacMsg = concatenateByteArrays(iv, direction, encMsg);
|
||||
byte[] fullHmac = hmac(macKey, hmacMsg);
|
||||
return Arrays.copyOfRange(fullHmac, 0, 16);
|
||||
}
|
||||
|
||||
private byte[] concatenateByteArrays(byte[]... arrays) {
|
||||
int totalLength = 0;
|
||||
for (byte[] array : arrays) {
|
||||
totalLength += array.length;
|
||||
}
|
||||
byte[] result = new byte[totalLength];
|
||||
int currentIndex = 0;
|
||||
for (byte[] array : arrays) {
|
||||
System.arraycopy(array, 0, result, currentIndex, array.length);
|
||||
currentIndex += array.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.exception.WebSocketClientServiceException;
|
||||
|
||||
/**
|
||||
* WebSocket client service interface.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface WebSocketClientService {
|
||||
/**
|
||||
* Establish the WebSocket connection to the appliance.
|
||||
*
|
||||
* @throws WebSocketClientServiceException if the connection could not be established
|
||||
*/
|
||||
void connect() throws WebSocketClientServiceException;
|
||||
|
||||
/**
|
||||
* Close the connection and release all associated resources.
|
||||
*/
|
||||
void dispose();
|
||||
|
||||
/**
|
||||
* Send a message to the appliance.
|
||||
*
|
||||
* @param message the raw message to send
|
||||
*/
|
||||
void send(String message);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Handler for incoming WebSocket messages.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface WebSocketHandler {
|
||||
/**
|
||||
* Called when the WebSocket connection has been established.
|
||||
*/
|
||||
void onWebSocketConnect();
|
||||
|
||||
/**
|
||||
* Called when a message has been received from the appliance.
|
||||
*
|
||||
* @param rawMessage the raw message received
|
||||
* @param websocketClientService the client service the message was received on
|
||||
*/
|
||||
void onWebSocketMessage(String rawMessage, WebSocketClientService websocketClientService);
|
||||
|
||||
/**
|
||||
* Called when the WebSocket connection has been closed.
|
||||
*/
|
||||
void onWebSocketClose();
|
||||
|
||||
/**
|
||||
* Called when an error occurred on the WebSocket connection.
|
||||
*
|
||||
* @param throwable the error that occurred
|
||||
*/
|
||||
void onWebSocketError(Throwable throwable);
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket;
|
||||
|
||||
import static org.openhab.binding.homeconnectdirect.internal.HomeConnectDirectBindingConstants.CONSCRYPT_PROVIDER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.GZIPContentDecoder;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.homeconnectdirect.internal.service.websocket.exception.WebSocketClientServiceException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* WebSocket client service using TLS with Conscrypt.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@WebSocket
|
||||
@NonNullByDefault
|
||||
public class WebSocketTlsConscryptClientService extends AbstractWebSocketClientService {
|
||||
|
||||
private static final String PSK_IDENTITY = "HCCOM_Local_App";
|
||||
private static final String TLSV_1_2 = "TLSv1.2";
|
||||
private static final String TLS_ECDHE_PSK_WITH_CHACHA_20_POLY_1305_SHA_256 = "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256";
|
||||
private static final String TLS = "TLS";
|
||||
private static final Provider PROVIDER = Conscrypt.newProviderBuilder().setName(CONSCRYPT_PROVIDER).build();
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
public WebSocketTlsConscryptClientService(Thing thing, URI uri, String base64EncodedKey,
|
||||
WebSocketHandler webSocketHandler, ScheduledExecutorService scheduler)
|
||||
throws WebSocketClientServiceException {
|
||||
super(thing, uri, webSocketHandler, scheduler);
|
||||
try {
|
||||
logger = LoggerFactory.getLogger(WebSocketTlsConscryptClientService.class);
|
||||
byte[] key = Base64.getUrlDecoder().decode(base64EncodedKey);
|
||||
|
||||
// http client setup
|
||||
SSLContext sslContext = SSLContext.getInstance(TLS, PROVIDER);
|
||||
sslContext.init(new KeyManager[] { new ConscryptPskKeyManager(PSK_IDENTITY, key) }, new TrustManager[0],
|
||||
new SecureRandom());
|
||||
|
||||
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
sslContextFactory.setIncludeCipherSuites(TLS_ECDHE_PSK_WITH_CHACHA_20_POLY_1305_SHA_256);
|
||||
sslContextFactory.setIncludeProtocols(TLSV_1_2);
|
||||
|
||||
var httpClient = new HttpClient(sslContextFactory);
|
||||
httpClient.getContentDecoderFactories().add(new GZIPContentDecoder.Factory());
|
||||
|
||||
// websocket
|
||||
setWebSocketClient(new WebSocketClient(httpClient));
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new WebSocketClientServiceException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
try {
|
||||
var session = getSession();
|
||||
if (session != null && session.isOpen()) {
|
||||
logger.debug(">> {} ({})", message, getThingUID());
|
||||
session.getRemote().sendString(message);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to send message! error={} thingUID={}", e.getMessage(), getThingUID());
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onMessage(Session session, String message) {
|
||||
logger.debug("<< {} ({})", message, getThingUID());
|
||||
getWebSocketHandler().onWebSocketMessage(message, this);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Exception thrown by WebSocket client services.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebSocketClientServiceException extends Exception {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -8417710695707997749L;
|
||||
|
||||
public WebSocketClientServiceException(@Nullable String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebSocketClientServiceException(@Nullable String message, @Nullable Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* WebSocket access model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Access {
|
||||
READ,
|
||||
@SerializedName("READWRITE")
|
||||
READ_WRITE,
|
||||
@SerializedName("READSTATIC")
|
||||
READ_STATIC,
|
||||
@SerializedName("WRITEONLY")
|
||||
WRITE_ONLY,
|
||||
NONE
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* WebSocket action model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Action {
|
||||
GET,
|
||||
POST,
|
||||
RESPONSE,
|
||||
NOTIFY
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.homeconnectdirect.internal.service.websocket.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* WebSocket description change data model.
|
||||
*
|
||||
* @author Jonas Brüstel - Initial contribution
|
||||
*/
|
||||
@Deprecated
|
||||
public record DescriptionChangeData(int uid, @SerializedName("parentUID") int parentUid, Boolean available,
|
||||
Access access, Integer min, Integer max, Integer stepSize, @SerializedName("default") Object defaultValue,
|
||||
Integer enumType
|
||||
|
||||
) {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user