[nest] Add support for Smart Device Management (SDM) API (#8947)

* [nest] Add support for Smart Device Management (SDM) API

* Reworks WWN implementation so that the thing types have a wwn_ prefix and the classes have a WWN prefix and reside in a 'wwn' package
* Adds an SDM implementation which is also based on: https://github.com/bhigg-code/openhab-addons/tree/2.5.x/bundles/org.openhab.binding.nestdeviceaccess
* Adds unit tests for (de)serialization of the SDM and Pub/Sub API requests and responses
* Updates the binding documentation for the changes and additions

Fixes #8664

Also-by: Brian Higginbotham <brianhigginbothamtx@gmail.com>
Signed-off-by: Wouter Born <github@maindrain.net>

* Fix and improve documentation

Signed-off-by: Wouter Born <github@maindrain.net>

* Always use UTF8 when decoding SDM events

Signed-off-by: Wouter Born <github@maindrain.net>
pull/10882/head^2
Wouter Born 2021-06-20 19:59:46 +02:00 committed by GitHub
parent ec7c3a528f
commit 6296eba14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
160 changed files with 7668 additions and 1186 deletions

View File

@ -1,66 +1,243 @@
# Nest Binding
The Nest binding integrates devices by [Nest](https://nest.com) using the [Nest API](https://developers.nest.com/documentation/cloud/get-started) (REST).
The Nest binding integrates devices by [Nest](https://store.google.com/us/category/connected_home?) using the [Smart Device Management](https://developers.google.com/nest/device-access/api) (SDM) API and the Works with Nest (WWN) API.
Because the Nest API runs on Nest's servers a connection with the Internet is required for sending and receiving information.
The binding uses HTTPS to connect to the Nest API using ports 443 and 9553. Make sure outbound connections to these ports are not blocked by a firewall.
To be able to use the SDM API it is required to first [register](https://developers.google.com/nest/device-access/registration) and pay a US$5 non-refundable registration fee.
> Note: This binding can only be used with Nest devices if you have an existing Nest developer account signed up for the Works with Nest (WWN) program.
New integrations using the WWN program are no longer accepted because WWN is being retired.
To keep using this binding do **NOT** migrate your Nest Account to a Google Account.
For more information see [What's happening at Nest?](https://nest.com/whats-happening/).
It is also possible to use the older WWN API with this binding.
For this you need to have the account details of a previously registered WWN API account.
Another requirement is that you have not yet migrated your Nest account to a Google account (which is irreversible).
It is no longer possible to register new WWN API accounts because the WWN API runs in maintenance mode.
See also [What's happening at Nest?](https://nest.com/whats-happening/).
Because the SDM and WWN APIs run on servers in the cloud, a connection with the Internet is required for sending and receiving information.
The binding uses HTTPS to connect to the APIs using port 443.
When using the WWN API, the binding also connects to servers on port 9553.
So make sure outbound connections to these ports are not blocked by a firewall.
## Supported Things
The table below lists the Nest binding thing types:
| Things | Description | Thing Type |
|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
| Nest Account | An account for using the Nest REST API | account |
| Nest Cam (Indoor, IQ, Outdoor), Dropcam | A Nest Cam registered with your account | camera |
| Nest Protect | The smoke detector/Nest Protect for the account | smoke_detector |
| Structure | The Nest structure defines the house the account has setup on Nest. You will only have more than one structure if you have more than one house | structure |
| Nest Thermostat (E) | A Thermostat to control the various aspects of the house's HVAC system | thermostat |
| Things | Description | SDM Thing Type | WWN Thing Type |
|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------|--------------------|
| Nest Account (SDM, WWN) | An account for using the Nest (SDM/WWN) REST API | sdm_account | wwn_account |
| Nest Cam (Indoor, IQ, Outdoor), Dropcam | A Nest Cam registered with your account | sdm_camera | wwn_camera |
| Nest Hello Doorbell | A Nest Doorbell registered with your account | sdm_doorbell | wwn_camera |
| Nest Hub (Max) | A Nest Display registered with your account | sdm_display | wwn_camera |
| Nest Protect | The smoke detector/Nest Protect for the account | | wwn_smoke_detector |
| Nest Thermostat (E) | A Thermostat to control the various aspects of the house's HVAC system | sdm_thermostat | wwn_thermostat |
| Structure | The Nest structure defines the house the account has setup on Nest. You will only have more than one structure if you have more than one house | | wwn_structure |
## Authorization
The SDM API currently does not support Nest Protect devices.
There are no structure Things when using the SDM API, because the SDM API does not support setting the Home/Away status like the WWN API does.
The Nest API uses OAuth for authorization.
Therefore the binding needs some authorization parameters before it can access your Nest account via the Nest API.
To use one of the Nest APIs, add the corresponding Account Thing using the UI and configure the required parameters.
After configuring an Account Thing, you can use it to discover the connected devices which are then added the Inbox.
To get these authorization parameters you first need to sign up as a [Nest Developer](https://developer.nest.com) and [register a new Product](https://developer.nest.com/products/new) (free and instant).
## SDM Account Configuration
While registering a new Product (on the Product Details page) make sure to:
### Google Account Requirement
* Leave both "OAuth Redirect URI" fields empty to enable PIN-based authorization.
* Grant all the permissions you intend to use. When in doubt, enable the permission because the binding needs to be reauthorized when permissions change at a later time.
To be able to use the SDM API it is required that you use a Google Account with your Nest devices.
If you still use the WWN API, you can no longer use the WWN API after migrating to a Google Account.
So if you have not yet migrated your account, check that all the functionality you require is provided by the SDM API and SDM Things in the binding.
Most notably, there is no support for the Nest Protect in the SDM API and you cannot change your Home/Away status.
To migrate to a Google account, follow the migration steps in the [Nest accounts FAQ](https://support.google.com/googlenest/answer/9297676?co=GENIE.Platform%3DiOS&hl=en&oco=0#accountmigration&accountmigration1&#accountmigration2&#accountmigration3&zippy=%2Chow-do-i-migrate-my-account)
After creating the Product, your browser shows the Product Overview page.
This page contains the **Product ID** and **Product Secret** authorization parameters that are used by the binding.
Take note of both parameters or keep this page open in a browser tab.
Now copy and paste the "Authorization URL" in a new browser tab.
Accept the permissions and you will be presented the **Pincode** authorization parameter that is also used by the binding.
### SDM Configuration Parameters
You can return to the Product Overview page at a later time by opening the [Products](https://console.developers.nest.com/products) page and selecting your Product.
These parameters configure which SDM project is accessed using the SDM API and configure the OAuth 2.0 client details used for accessing the project.
First a SDM project needs to be created and configured:
1. Register for device access by clicking the "Go to Device Access Console" button and follow the instructions on the [Device Access Registration](https://developers.google.com/nest/device-access/registration) page.
1. Create a new SDM project on the [Projects](https://console.nest.google.com/device-access/project-list) page
1. Give your project a name so it is easily recognizable
1. "Skip" entering the OAuth client ID for now
1. If you want to download camera images using the binding, it is required to "Enable" events.
Enabling events also allows for faster thermostat state updates.
The binding only uses events when the Pub/Sub configuration parameters of the Nest SDM Account Thing are also configured.
1. After clicking the "Create project" button, the SDM project details of the created project show
1. Copy and save the **Project ID** at the top of the page (e.g. `585de72e-968c-435c-b16a-31d1d3f76833`) somewhere
Now an OAuth 2.0 client is created and configured for using the SDM API by the binding:
1. Configure the "Publishing status" of your Google Cloud Platform to "Production" ([APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent)) so the OAuth 2.0 tokens do not expire after 2 weeks
1. Create a new client on the "Credentials" page ([APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials)):
1. Click the "Create Credentials" button at the top of the page
1. Choose "OAuth client ID"
1. As "Application type" choose "TVs and Limited Input devices"
1. Give it a name so you can remember what it is used for (e.g. `Nest Binding SDM`)
1. Click "Create" to create the client
1. Copy and save the generated **Client ID** (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`) and **Client Secret** (e.g. `726kcU-d1W4RXxEJA79oZ0oG`) somewhere
1. Configure the SDM project to use the created client:
1. Go the the SDM [Projects](https://console.nest.google.com/device-access/project-list) page
1. Click on your SDM Project to show its details
1. Scroll to "Project Info > OAuth client ID" and open the options menu (3 stacked dots) at the end of the line
1. Select the "Edit" option
1. Copy/paste the saved OAuth 2.0 Client ID here (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`)
1. Click the "Save" button at the end of the line to update the project
Finally, an SDM Account Thing can be created to access the SDM project using the SDM API with the created client:
1. Create a new "Nest SDM Account" Thing in openHAB
1. Copy/paste the saved SDM **Project ID** to SDM group parameter in the SDM Account Thing configuration parameters (e.g. `585de72e-968c-435c-b16a-31d1d3f76833`)
1. Copy/paste the saved OAuth 2.0 **Client ID** to SDM group parameter (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`)
1. Copy/paste the saved OAuth 2.0 **Client Secret** to SDM group parameter (e.g. `726kcU-d1W4RXxEJA79oZ0oG`)
1. Create an authorization code for the binding:
1. Replace the **Project ID** and **Client ID** in the URL below with your SDM Project ID and SDM OAuth 2.0 Client ID and open the URL in a new browser tab:
`https://nestservices.google.com/partnerconnections/{{ProjectID}}/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&access_type=offline&prompt=consent&client_id={{ClientID}}&response_type=code&scope=https://www.googleapis.com/auth/sdm.service`
For the example values used so far this is:
`https://nestservices.google.com/partnerconnections/585de72e-968c-435c-b16a-31d1d3f76833/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&access_type=offline&prompt=consent&client_id=1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com&response_type=code&scope=https://www.googleapis.com/auth/sdm.service`
1. Enable all the permissions you want to use with the binding and click "Next" to continue
1. Login using your Google account when prompted
1. On the "Google hasn't verified this app" page, click on "Advanced"
1. Then click on "Go to ... (advanced)"
1. Now "Allow" the SDM permissions and confirm your choices again by clicking "Allow"
1. Next the "Sign in" page will show the **Authorization Code**
1. Copy/paste the **Authorization Code** to the SDM group parameter in the openHAB Nest SDM Account Thing configuration
1. All required SDM Account Thing configuration parameters have now been entered so create it by clicking "Create Thing".
The SDM Account Thing should now be ONLINE and have as status description "Using periodic refresh".
It should also be possible to use the configured account to discover your Nest devices via the Inbox.
You can monitor the SDM API using the Google Cloud Platform Console via [API & Services > Smart Device Management API](https://console.cloud.google.com/apis/api/smartdevicemanagement.googleapis.com/overview).
If you've made it this far, it should be easy to edit the SDM Account Thing again and update it so it can also use SDM Pub/Sub events. :-)
### Pub/Sub Configuration Parameters
After configuring the SDM configuration parameters, a SDM Account Thing can be updated so it can listen to SDM events using Pub/Sub.
This is required if you want to download camera images using the binding or to get faster thermostat state updates.
Enable Pub/Sub events in your SDM project:
1. Open your SDM project details using the [Projects](https://console.nest.google.com/device-access/project-list) page
1. Scroll to "Project Info > Pub/Sub topic" and check if it is set to "Enabled"
1. If it is set to "Disabled", enable events:
1. Open the options menu (3 stacked dots) at the end of the line
1. Select the "Edit" option
1. Check the "Enable events" option
1. Click the "Save" button at the end of the line to update the project
Lookup your Google Cloud Platform (GCP) Project ID:
1. Open the [IAM & Admin > Settings](https://console.cloud.google.com/iam-admin/settings)
1. Copy and save the GCP **Project ID** (e.g. `openhab-12345`)
Next an OAuth 2.0 client is created which is used to create a Pub/Sub subscription for listening to SDM events by the binding:
1. Open the "Credentials" page ([APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials)):
1. Click the "Create Credentials" button at the top of the page
1. Choose "OAuth client ID"
1. As "Application type" choose "TVs and Limited Input devices"
1. Give it a name so you can remember what it is used for (e.g. `Nest Binding Pub/Sub`)
1. Click "Create" to create the client
1. Copy and save the generated **Client ID** (e.g. `1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com`) and **Client Secret** (e.g. `1-k78-XcHhp_gdZF-I6JaIHp`) somewhere
Finally, the existing SDM Account Thing can be updated so it can subscribe to SDM events:
1. Open the configuration details of your existing "Nest SDM Account" Thing in openHAB
1. Copy/paste the saved GCP **Project ID** to Pub/Sub group parameter (e.g. `openhab-123`)
1. Enter a name in **Subscription ID** that uniquely identifies the Pub/Sub subscription used by the binding
> Must be 3-255 characters, start with a letter, and contain only the following characters: letters, numbers, dashes (-), periods (.), underscores (_), tildes (~), percents (%) or plus signs (+). Cannot start with goog.
1. Copy/paste the saved OAuth 2.0 **Client ID** to Pub/Sub group parameter (e.g. `1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com`)
1. Copy/paste the saved OAuth 2.0 **Client Secret** to Pub/Sub group parameter (e.g. `1-k78-XcHhp_gdZF-I6JaIHp`)
1. Create an authorization code for the binding:
1. Replace the **Client ID** in the URL below with your Pub/Sub OAuth 2.0 Client ID and open the URL in a new browser tab:
`https://accounts.google.com/o/oauth2/auth?client_id={{ClientID}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/pubsub`
For the example client this is:
`https://accounts.google.com/o/oauth2/auth?client_id=1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/pubsub`
1. Login using your Google account when prompted
1. On the "Google hasn't verified this app" page, click on "Advanced"
1. Then click on "Go to ... (advanced)"
1. Now "Allow" the Pub/Sub permissions and confirm your choices again by clicking "Allow"
1. Next the "Sign in" page will show the **Authorization Code**
1. Copy/paste the **Authorization Code** to the Pub/Sub group parameter in the openHAB Nest SDM Account Thing configuration
1. All required Pub/Sub Account Thing configuration parameters have now been entered so click "Save" to update the SDM Account Thing configuration.
The SDM Account Thing should now be ONLINE and have as status description "Using periodic refresh and Pub/Sub".
The created subscription can also be monitored using the Google Cloud Platform Console via [Pub/Sub > Subscriptions](https://console.cloud.google.com/cloudpubsub/subscription/list).
## SDM Device Configuration
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|---------|---------------------------------------------------------------------------|
| deviceId | X | | Identifies the device in the SDM API |
| refreshInterval | | 300 | This is refresh interval in seconds to update the Nest device information |
Decreasing the `refreshInterval` may cause issues when you have a lot of devices connected because it may cause API rate limits to be exceeded.
You may want to decrease the `refreshInterval` for a Thermostat if Pub/Sub events have not been configured to provide state updating.
## WWN Account Configuration
To configure the binding to use the WWN API, add a new "Nest WWN Account" Thing in the UI and enter the **Product ID**, **Product Secret** and **Access Token** of an existing WWN account as configuration parameters.
It is no longer possible to register new WWN accounts with Nest because the WWN API runs in maintenance mode.
## Discovery
The binding will discover all Nest Things from your account when you add and configure a "Nest Account" Thing.
See the Authorization paragraph above for details on how to obtain the Product ID, Product Secret and Pincode configuration parameters.
Once the binding has successfully authorized with the Nest API, it obtains an Access Token using the Pincode.
The configured Pincode is cleared because it can only be used once.
The obtained Access Token is saved as an advanced configuration parameter of the "Nest Account".
You can reuse an Access Token for authorization but not the Pincode.
A new Pincode can again be generated via the "Authorization URL" (see Authorization paragraph).
The binding will discover all Nest Things from your account when you add and configure a Nest SDM or WWN Account Thing.
## Channels
### Account Channels
### SDM/WWN Account Channels
The account Thing Type does not have any channels.
The account Thing Types do not have any channels.
### Camera Channels
### SDM Camera/Display/Doorbell Channels
The state of these channels is based on Pub/Sub events sent by the SDM API.
So make sure the Pub/Sub account details are properly configured in the `sdm_account`.
| Channel Type ID | Item Type | Description | Read Write |
|----------------------------------|-----------|-----------------------------------------------------|:----------:|
| chime_event#image | Image | Static image based on a chime event | R |
| chime_event#timestamp | DateTime | The last time that the door chime was pressed | R |
| live_stream#current_token | String | Live stream current token value | R |
| live_stream#expiration_timestamp | DateTime | Live stream token expiration time | R |
| live_stream#extension_token | String | Live stream token extension value | R |
| live_stream#url | String | The RTSP video stream URL for the most recent event | R |
| motion_event#image | Image | Static image based on a motion event | R |
| motion_event#timestamp | DateTime | The last time that motion was detected | R |
| person_event#image | Image | Static image based on a person event | R |
| person_event#timestamp | DateTime | The last time that a person was detected | R |
| sound_event#image | Image | Static image based on a sound event | R |
| sound_event#timestamp | DateTime | The last time that a sound was detected | R |
The `chime_event` group channels only exist for doorbell Things.
Each image channel has the `imageWidth` and `imageHeight` configuration parameters that can be used for configuring the image size in pixels.
The maximum camera resolution is listed as `maxImageResolution` property in the Thing properties.
### SDM Thermostat Channels
| Channel Type ID | Item Type | Description | Read Write |
|---------------------|----------------------|------------------------------------------------------------------------|:----------:|
| ambient_humidity | Number:Dimensionless | Lists the current ambient humidity percentage from the thermostat | R |
| ambient_temperature | Number:Temperature | Lists the current ambient temperature from the thermostat | R |
| current_eco_mode | String | Lists the current eco mode from the thermostat (OFF, MANUAL_ECO) | R/W |
| current_mode | String | Lists the current mode from the thermostat (OFF, HEAT, COOL, HEATCOOL) | R/W |
| fan_timer_mode | Switch | Lists the current fan timer mode | R/W |
| fan_timer_timeout | DateTime | Timestamp at which timer mode turns OFF | R/W |
| hvac_status | String | Provides the thermostat HVAC Status (OFF, HEATING, COOLING) | R |
| maximum_temperature | Number:Temperature | Lists the maximum temperature setting from the thermostat | R/W |
| minimum_temperature | Number:Temperature | Lists the target temperature setting from the thermostat | R/W |
| target_temperature | Number:Temperature | Lists the target temperature setting from the thermostat | R/W |
| temperature_cool | Number:Temperature | Lists the heat temperature Setting from the thermostat | R |
| temperature_heat | Number:Temperature | Lists the heat temperature setting from the thermostat | R |
The `fan_timer_mode` channel has a `fanTimerDuration` configuration parameter that can be used for configuring how long the fan is ON before it is switched OFF (1s to 43200s).
Similarly, when a DateTime command is sent to the `fan_timer_timeout` channel, the fan timer is switched ON and runs until the timestamp in the command (min now+1s, max now+43200s).
### WWN Camera Channels
**Camera group channels**
@ -96,7 +273,7 @@ Information about the last camera event (requires Nest Aware subscription).
| urls_expire_time | DateTime | Timestamp when the camera event URLs expire | R |
| web_url | String | The web URL for the camera event, allows you to see the camera event in a web page | R |
### Smoke Detector Channels
### WWN Smoke Detector Channels
| Channel Type ID | Item Type | Description | Read Write |
|-----------------------|-----------|-----------------------------------------------------------------------------------|:----------:|
@ -108,7 +285,7 @@ Information about the last camera event (requires Nest Aware subscription).
| smoke_alarm_state | String | The smoke alarm state of the Nest Protect (OK, EMERGENCY, WARNING) | R |
| ui_color_state | String | The current color of the ring on the smoke detector (GRAY, GREEN, YELLOW, RED) | R |
### Structure Channels
### WWN Structure Channels
| Channel Type ID | Item Type | Description | Read Write |
|------------------------------|-----------|--------------------------------------------------------------------------------------------------------|:----------:|
@ -124,7 +301,7 @@ Information about the last camera event (requires Nest Aware subscription).
| smoke_alarm_state | String | Smoke alarm state (OK, EMERGENCY, WARNING) | R |
| time_zone | String | The time zone for the structure ([IANA time zone format](https://www.iana.org/time-zones)) | R |
### Thermostat Channels
### WWN Thermostat Channels
| Channel Type ID | Item Type | Description | Read Write |
|-----------------------------|----------------------|----------------------------------------------------------------------------------------|:----------:|
@ -165,94 +342,144 @@ The Nest API applies the following rounding:
You can use the discovery functionality of the binding to obtain the deviceId and structureId values for defining Nest things in files.
Another way to get the deviceId and structureId values is by querying the Nest API yourself. First [obtain an Access Token](https://developers.nest.com/documentation/cloud/sample-code-auth) (or use the Access Token obtained by the binding).
Then use it with one of the [API Read Examples](https://developers.nest.com/documentation/cloud/how-to-read-data).
### demo.things:
### sdm-demo.things
```
Bridge nest:account:demo_account [ productId="8fdf9885-ca07-4252-1aa3-f3d5ca9589e0", productSecret="QITLR3iyUlWaj9dbvCxsCKp4f", accessToken="c.6rse1xtRk2UANErcY0XazaqPHgbvSSB6owOrbZrZ6IXrmqhsr9QTmcfaiLX1l0ULvlI5xLp01xmKeiojHqozLQbNM8yfITj1LSdK28zsUft1aKKH2mDlOeoqZKBdVIsxyZk4orH0AvKEZ5aY" ] {
camera fish_cam [ deviceId="qw0NNE8ruxA9AGJkTaFH3KeUiJaONWKiH9Gh3RwwhHClonIexTtufQ" ]
smoke_detector hallway_smoke [ deviceId="Tzvibaa3lLKnHpvpi9OQeCI_z5rfkBAV" ]
structure home [ structureId="20wKjydArmMV3kOluTA7JRcZg8HKBzTR-G_2nRXuIN1Bd6laGLOJQw" ]
thermostat living_thermostat [ deviceId="ZqAKzSv6TO6PjBnOCXf9LSI_z5rfkBAV" ]
Bridge nest:sdm_account:demo_sdm_account [ sdmProjectId="585de72e-968c-435c-b16a-31d1d3f76833", sdmClientId="1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com", sdmClientSecret="726kcU-d1W4RXxEJA79oZ0oG", sdmAuthorizationCode="xkkY3qYtfZCzaXCcPxpOELUW8EhgiSMD3n9jmzJ3m0yerkQpVRdj5vqWRjMSIG", pubsubProjectId="openhab-12345", pubsubSubscriptionId="nest-sdm-events", pubsubClientId="1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com", pubsubClientSecret="1-k78-XcHhp_gdZF-I6JaIHp", pubsubAuthorizationCode="tASfQq7gn6sfbUSbwRufbMI0BYDzh1d7MBG2G7vdZpbhjmZfwDp5MkeaX0iMxn" ] {
Thing sdm_camera fish_cam [ deviceId="AVPHwTQCAhersqmQ3IXwyqSX-XyuVZXoiNSNPeHdIMKgYpYZolNP4S9LS5QDF2LeuM3BQcpBh_fOEZYxkeH6eoQdWEELqi" ] {
Channels:
Image : motion_event#image [ imageHeight=1080 ]
Image : person_event#image [ imageWidth=1920 ]
Image : sound_event#image [ imageHeight=1080 ]
}
Thing sdm_doorbell front_door [ deviceId="AVPHws4JWeIzZlru3DSxXoKnIgPntKpzax7a1Zwms8H0-HaRet2pTdTCPOTBZ74YDzYqq7w6XpEPwOTkBXtf4KCJ4nq9hq" ] {
Channels:
Image : chime_event#image [ imageWidth=1920 ]
}
Thing sdm_display kitchen_hub [ deviceId="AVPHw64dWG5CcAJdDNzBbHWgu91l4v8WA4CsJqgtrvMS3QrbDnurB0_WzZEwpcWaw8Y9rLEQXW0avEwCjTd40Gmia6ussU" ]
Thing sdm_thermostat living_thermostat [ deviceId="AVPHwQum_bx9LmiRfv6jv5qPcKho0vHx2HqqMUvXP3TD-TTDCJebbzkegpRMozU5t7GSeTQIzxdH2LYDsZO8RClcGj7CCT", refreshInterval=180 ] {
Channels:
Image : fan_timer_mode [ fanTimerDuration=7200 ]
}
}
```
### demo.items:
### sdm-demo.items
```
/* Camera */
String Cam_App_URL "App URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#app_url" }
Switch Cam_Audio_Input_Enabled "Audio Input Enabled" { channel="nest:camera:demo_account:fish_cam:camera#audio_input_enabled" }
DateTime Cam_Last_Online_Change "Last Online Change [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:camera#last_online_change" }
String Cam_Snapshot_URL "Snapshot URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#snapshot_url" }
Switch Cam_Streaming "Streaming" { channel="nest:camera:demo_account:fish_cam:camera#streaming" }
Switch Cam_Public_Share_Enabled "Public Share Enabled" { channel="nest:camera:demo_account:fish_cam:camera#public_share_enabled" }
String Cam_Public_Share_URL "Public Share URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#public_share_url" }
Switch Cam_Video_History_Enabled "Video History Enabled" { channel="nest:camera:demo_account:fish_cam:camera#video_history_enabled" }
String Cam_Web_URL "Web URL [%s]" { channel="nest:camera:demo_account:fish_cam:camera#web_url" }
String Cam_LE_Activity_Zones "Last Event Activity Zones [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#activity_zones" }
String Cam_LE_Animated_Image_URL "Last Event Animated Image URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#animated_image_url" }
String Cam_LE_App_URL "Last Event App URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#app_url" }
DateTime Cam_LE_End_Time "Last Event End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#end_time" }
Switch Cam_LE_Has_Motion "Last Event Has Motion" { channel="nest:camera:demo_account:fish_cam:last_event#has_motion" }
Switch Cam_LE_Has_Person "Last Event Has Person" { channel="nest:camera:demo_account:fish_cam:last_event#has_person" }
Switch Cam_LE_Has_Sound "Last Event Has Sound" { channel="nest:camera:demo_account:fish_cam:last_event#has_sound" }
String Cam_LE_Image_URL "Last Event Image URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#image_url" }
DateTime Cam_LE_Start_Time "Last Event Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#start_time" }
DateTime Cam_LE_URLs_Expire_Time "Last Event URLs Expire Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:camera:demo_account:fish_cam:last_event#urls_expire_time" }
String Cam_LE_Web_URL "Last Event Web URL [%s]" { channel="nest:camera:demo_account:fish_cam:last_event#web_url" }
/* SDM Doorbell */
Image Doorbell_Chime_Image "Chime Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:chime_event#image" }
DateTime Doorbell_Chime_Timestamp "Chime Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:chime_event#timestamp" }
String Doorbell_Stream_Token "Stream Token [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#current_token" }
DateTime Doorbell_Stream_Timestamp "Stream Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#expiration_timestamp" }
String Doorbell_Stream_Ext_Token "Stream Extension Token [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#extension_token" }
String Doorbell_Stream_URL "Stream Extension URL [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#url" }
Image Doorbell_Motion_Image "Motion Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:motion_event#image" }
DateTime Doorbell_Motion_Timestamp "Motion Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:motion_event#timestamp" }
Image Doorbell_Person_Image "Person Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:person_event#image" }
DateTime Doorbell_Person_Timestamp "Person Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:person_event#timestamp" }
Image Doorbell_Sound_Image "Sound Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:sound_event#image" }
DateTime Doorbell_Sound_Timestamp "Sound Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:sound_event#timestamp" }
/* Smoke Detector */
String Smoke_CO_Alarm "CO Alarm [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:co_alarm_state" }
Switch Smoke_Battery_Low "Battery Low" { channel="nest:smoke_detector:demo_account:hallway_smoke:low_battery" }
Switch Smoke_Manual_Test "Manual Test" { channel="nest:smoke_detector:demo_account:hallway_smoke:manual_test_active" }
DateTime Smoke_Last_Connection "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:smoke_detector:demo_account:hallway_smoke:last_connection" }
DateTime Smoke_Last_Manual_Test "Last Manual Test [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:smoke_detector:demo_account:hallway_smoke:last_manual_test_time" }
String Smoke_Smoke_Alarm "Smoke Alarm [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:smoke_alarm_state" }
String Smoke_UI_Color "UI Color [%s]" { channel="nest:smoke_detector:demo_account:hallway_smoke:ui_color_state" }
/* SDM Thermostat */
Number:Dimensionless Thermostat_Amb_Humidity "Ambient Humidity [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:ambient_humidity" }
Number:Temperature Thermostat_Amb_Temperature "Ambient Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:ambient_temperature" }
String Thermostat_Current_Eco_Mode "Current Eco Mode [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:current_eco_mode" }
String Thermostat_Current_Mode "Current Mode [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:current_mode" }
Switch Thermostat_Fan_Timer_Mode "Fan Timer Mode" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:fan_timer_mode" }
DateTime Thermostat_Fan_Timer_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:fan_timer_timeout" }
String Thermostat_HVAC_Status "HVAC Status [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:hvac_status" }
Number:Temperature Thermostat_Max_Temperature "Max Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:maximum_temperature" }
Number:Temperature Thermostat_Min_Temperature "Min Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:minimum_temperature" }
Number:Temperature Thermostat_Target_temperature "Target Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:target_temperature" }
Number:Temperature Thermostat_Temperature_Cool "Temperature Cool [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:temperature_cool" }
Number:Temperature Thermostat_Temperature_Heat "Temperature Heat [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:temperature_heat" }
```
/* Thermostat */
Switch Thermostat_Can_Cool "Can Cool" { channel="nest:thermostat:demo_account:living_thermostat:can_cool" }
Switch Thermostat_Can_Heat "Can Heat" { channel="nest:thermostat:demo_account:living_thermostat:can_heat" }
Number:Temperature Therm_EMaxSP "Eco Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:eco_max_set_point" }
Number:Temperature Therm_EMinSP "Eco Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:eco_min_set_point" }
Switch Thermostat_FT_Active "Fan Timer Active" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_active" }
Number:Time Thermostat_FT_Duration "Fan Timer Duration [%d %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_duration" }
DateTime Thermostat_FT_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:thermostat:demo_account:living_thermostat:fan_timer_timeout" }
Switch Thermostat_Has_Fan "Has Fan" { channel="nest:thermostat:demo_account:living_thermostat:has_fan" }
Switch Thermostat_Has_Leaf "Has Leaf" { channel="nest:thermostat:demo_account:living_thermostat:has_leaf" }
Number:Dimensionless Therm_Hum "Humidity [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:humidity" }
DateTime Thermostat_Last_Conn "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:thermostat:demo_account:living_thermostat:last_connection" }
Switch Thermostat_Locked "Locked" { channel="nest:thermostat:demo_account:living_thermostat:locked" }
Number:Temperature Therm_LMaxSP "Locked Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:locked_max_set_point" }
Number:Temperature Therm_LMinSP "Locked Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:locked_min_set_point" }
Number:Temperature Therm_Max_SP "Max Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:max_set_point" }
Number:Temperature Therm_Min_SP "Min Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:min_set_point" }
String Thermostat_Mode "Mode [%s]" { channel="nest:thermostat:demo_account:living_thermostat:mode" }
String Thermostat_Previous_Mode "Previous Mode [%s]" { channel="nest:thermostat:demo_account:living_thermostat:previous_mode" }
String Thermostat_State "State [%s]" { channel="nest:thermostat:demo_account:living_thermostat:state" }
Number:Temperature Thermostat_SP "Set Point [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:set_point" }
Switch Thermostat_Sunlight_CA "Sunlight Correction Active" { channel="nest:thermostat:demo_account:living_thermostat:sunlight_correction_active" }
Switch Thermostat_Sunlight_CE "Sunlight Correction Enabled" { channel="nest:thermostat:demo_account:living_thermostat:sunlight_correction_enabled" }
Number:Temperature Therm_Temp "Temperature [%.1f %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:temperature" }
Number:Time Therm_Time_To_Target "Time To Target [%d %unit%]" { channel="nest:thermostat:demo_account:living_thermostat:time_to_target" }
Switch Thermostat_Using_Em_Heat "Using Emergency Heat" { channel="nest:thermostat:demo_account:living_thermostat:using_emergency_heat" }
### wwn-demo.things
/* Structure */
String Home_Away "Away [%s]" { channel="nest:structure:demo_account:home:away" }
String Home_Country_Code "Country Code [%s]" { channel="nest:structure:demo_account:home:country_code" }
String Home_CO_Alarm_State "CO Alarm State [%s]" { channel="nest:structure:demo_account:home:co_alarm_state" }
DateTime Home_ETA "ETA [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:eta_begin" }
DateTime Home_PP_End_Time "PP End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:peak_period_end_time" }
DateTime Home_PP_Start_Time "PP Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:structure:demo_account:home:peak_period_start_time" }
String Home_Postal_Code "Postal Code [%s]" { channel="nest:structure:demo_account:home:postal_code" }
Switch Home_Rush_Hour_Rewards "Rush Hour Rewards" { channel="nest:structure:demo_account:home:rush_hour_rewards_enrollment" }
String Home_Security_State "Security State [%s]" { channel="nest:structure:demo_account:home:security_state" }
String Home_Smoke_Alarm_State "Smoke Alarm State [%s]" { channel="nest:structure:demo_account:home:smoke_alarm_state" }
String Home_Time_Zone "Time Zone [%s]" { channel="nest:structure:demo_account:home:time_zone" }
```
Bridge nest:wwn_account:demo_wwn_account [ productId="8fdf9885-ca07-4252-1aa3-f3d5ca9589e0", productSecret="QITLR3iyUlWaj9dbvCxsCKp4f", accessToken="c.6rse1xtRk2UANErcY0XazaqPHgbvSSB6owOrbZrZ6IXrmqhsr9QTmcfaiLX1l0ULvlI5xLp01xmKeiojHqozLQbNM8yfITj1LSdK28zsUft1aKKH2mDlOeoqZKBdVIsxyZk4orH0AvKEZ5aY" ] {
Thing wwn_camera fish_cam [ deviceId="qw0NNE8ruxA9AGJkTaFH3KeUiJaONWKiH9Gh3RwwhHClonIexTtufQ" ]
Thing wwn_smoke_detector hallway_smoke [ deviceId="Tzvibaa3lLKnHpvpi9OQeCI_z5rfkBAV" ]
Thing wwn_structure home [ structureId="20wKjydArmMV3kOluTA7JRcZg8HKBzTR-G_2nRXuIN1Bd6laGLOJQw" ]
Thing wwn_thermostat living_thermostat [ deviceId="ZqAKzSv6TO6PjBnOCXf9LSI_z5rfkBAV" ]
}
```
### wwn-demo.items
```
/* WWN Camera */
String Cam_App_URL "App URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#app_url" }
Switch Cam_Audio_Input_Enabled "Audio Input Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#audio_input_enabled" }
DateTime Cam_Last_Online_Change "Last Online Change [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#last_online_change" }
String Cam_Snapshot_URL "Snapshot URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#snapshot_url" }
Switch Cam_Streaming "Streaming" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#streaming" }
Switch Cam_Public_Share_Enabled "Public Share Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#public_share_enabled" }
String Cam_Public_Share_URL "Public Share URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#public_share_url" }
Switch Cam_Video_History_Enabled "Video History Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#video_history_enabled" }
String Cam_Web_URL "Web URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#web_url" }
String Cam_LE_Activity_Zones "Last Event Activity Zones [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#activity_zones" }
String Cam_LE_Animated_Image_URL "Last Event Animated Image URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#animated_image_url" }
String Cam_LE_App_URL "Last Event App URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#app_url" }
DateTime Cam_LE_End_Time "Last Event End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#end_time" }
Switch Cam_LE_Has_Motion "Last Event Has Motion" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_motion" }
Switch Cam_LE_Has_Person "Last Event Has Person" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_person" }
Switch Cam_LE_Has_Sound "Last Event Has Sound" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_sound" }
String Cam_LE_Image_URL "Last Event Image URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#image_url" }
DateTime Cam_LE_Start_Time "Last Event Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#start_time" }
DateTime Cam_LE_URLs_Expire_Time "Last Event URLs Expire Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#urls_expire_time" }
String Cam_LE_Web_URL "Last Event Web URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#web_url" }
/* WWN Smoke Detector */
String Smoke_CO_Alarm "CO Alarm [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:co_alarm_state" }
Switch Smoke_Battery_Low "Battery Low" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:low_battery" }
Switch Smoke_Manual_Test "Manual Test" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:manual_test_active" }
DateTime Smoke_Last_Connection "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:last_connection" }
DateTime Smoke_Last_Manual_Test "Last Manual Test [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:last_manual_test_time" }
String Smoke_Smoke_Alarm "Smoke Alarm [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:smoke_alarm_state" }
String Smoke_UI_Color "UI Color [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:ui_color_state" }
/* WWN Thermostat */
Switch Thermostat_Can_Cool "Can Cool" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:can_cool" }
Switch Thermostat_Can_Heat "Can Heat" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:can_heat" }
Number:Temperature Therm_EMaxSP "Eco Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:eco_max_set_point" }
Number:Temperature Therm_EMinSP "Eco Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:eco_min_set_point" }
Switch Thermostat_FT_Active "Fan Timer Active" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_active" }
Number:Time Thermostat_FT_Duration "Fan Timer Duration [%d %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_duration" }
DateTime Thermostat_FT_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_timeout" }
Switch Thermostat_Has_Fan "Has Fan" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:has_fan" }
Switch Thermostat_Has_Leaf "Has Leaf" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:has_leaf" }
Number:Dimensionless Therm_Hum "Humidity [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:humidity" }
DateTime Thermostat_Last_Conn "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:last_connection" }
Switch Thermostat_Locked "Locked" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked" }
Number:Temperature Therm_LMaxSP "Locked Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked_max_set_point" }
Number:Temperature Therm_LMinSP "Locked Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked_min_set_point" }
Number:Temperature Therm_Max_SP "Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:max_set_point" }
Number:Temperature Therm_Min_SP "Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:min_set_point" }
String Thermostat_Mode "Mode [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:mode" }
String Thermostat_Previous_Mode "Previous Mode [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:previous_mode" }
String Thermostat_State "State [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:state" }
Number:Temperature Thermostat_SP "Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:set_point" }
Switch Thermostat_Sunlight_CA "Sunlight Correction Active" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:sunlight_correction_active" }
Switch Thermostat_Sunlight_CE "Sunlight Correction Enabled" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:sunlight_correction_enabled" }
Number:Temperature Therm_Temp "Temperature [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:temperature" }
Number:Time Therm_Time_To_Target "Time To Target [%d %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:time_to_target" }
Switch Thermostat_Using_Em_Heat "Using Emergency Heat" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:using_emergency_heat" }
/* WWN Structure */
String Home_Away "Away [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:away" }
String Home_Country_Code "Country Code [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:country_code" }
String Home_CO_Alarm_State "CO Alarm State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:co_alarm_state" }
DateTime Home_ETA "ETA [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:eta_begin" }
DateTime Home_PP_End_Time "PP End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:peak_period_end_time" }
DateTime Home_PP_Start_Time "PP Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:peak_period_start_time" }
String Home_Postal_Code "Postal Code [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:postal_code" }
Switch Home_Rush_Hour_Rewards "Rush Hour Rewards" { channel="nest:wwn_structure:demo_wwn_account:home:rush_hour_rewards_enrollment" }
String Home_Security_State "Security State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:security_state" }
String Home_Smoke_Alarm_State "Smoke Alarm State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:smoke_alarm_state" }
String Home_Time_Zone "Time Zone [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:time_zone" }
```
## Attribution

View File

@ -1,135 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.discovery.NestDiscoveryService;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.handler.NestCameraHandler;
import org.openhab.binding.nest.internal.handler.NestSmokeDetectorHandler;
import org.openhab.binding.nest.internal.handler.NestStructureHandler;
import org.openhab.binding.nest.internal.handler.NestThermostatHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link NestHandlerFactory} is responsible for creating things and thing
* handlers. It also sets up the discovery service to track things from the bridge
* when the bridge is created.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest")
public class NestHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_THERMOSTAT,
THING_TYPE_CAMERA, THING_TYPE_BRIDGE, THING_TYPE_STRUCTURE, THING_TYPE_SMOKE_DETECTOR).collect(toSet());
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final Map<ThingUID, ServiceRegistration<?>> discoveryService = new HashMap<>();
@Activate
public NestHandlerFactory(@Reference ClientBuilder clientBuilder,
@Reference SseEventSourceFactory eventSourceFactory) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
}
/**
* The things this factory supports creating.
*/
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Creates a handler for the specific thing. THis also creates the discovery service
* when the bridge is created.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_THERMOSTAT.equals(thingTypeUID)) {
return new NestThermostatHandler(thing);
}
if (THING_TYPE_CAMERA.equals(thingTypeUID)) {
return new NestCameraHandler(thing);
}
if (THING_TYPE_STRUCTURE.equals(thingTypeUID)) {
return new NestStructureHandler(thing);
}
if (THING_TYPE_SMOKE_DETECTOR.equals(thingTypeUID)) {
return new NestSmokeDetectorHandler(thing);
}
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
NestBridgeHandler handler = new NestBridgeHandler((Bridge) thing, clientBuilder, eventSourceFactory);
NestDiscoveryService service = new NestDiscoveryService(handler);
service.activate();
// Register the discovery service.
discoveryService.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), service, new Hashtable<>()));
return handler;
}
return null;
}
/**
* Removes the handler for the specific thing. This also handles disabling the discovery
* service when the bridge is removed.
*/
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NestBridgeHandler) {
ServiceRegistration<?> reg = discoveryService.get(thingHandler.getThing().getUID());
if (reg != null) {
// Unregister the discovery service.
NestDiscoveryService service = (NestDiscoveryService) bundleContext.getService(reg.getReference());
service.deactivate();
reg.unregister();
discoveryService.remove(thingHandler.getThing().getUID());
}
}
super.removeHandler(thingHandler);
}
}

View File

@ -1,171 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.discovery;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.config.NestStructureConfiguration;
import org.openhab.binding.nest.internal.data.BaseNestDevice;
import org.openhab.binding.nest.internal.data.Camera;
import org.openhab.binding.nest.internal.data.SmokeDetector;
import org.openhab.binding.nest.internal.data.Structure;
import org.openhab.binding.nest.internal.data.Thermostat;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service connects to the Nest bridge and creates the correct discovery results for Nest devices
* as they are found through the API.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add representation properties
*/
@NonNullByDefault
public class NestDiscoveryService extends AbstractDiscoveryService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
.of(THING_TYPE_CAMERA, THING_TYPE_THERMOSTAT, THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE)
.collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(NestDiscoveryService.class);
private final DiscoveryDataListener<Camera> cameraDiscoveryDataListener = new DiscoveryDataListener<>(Camera.class,
THING_TYPE_CAMERA, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<SmokeDetector> smokeDetectorDiscoveryDataListener = new DiscoveryDataListener<>(
SmokeDetector.class, THING_TYPE_SMOKE_DETECTOR, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<Structure> structureDiscoveryDataListener = new DiscoveryDataListener<>(
Structure.class, THING_TYPE_STRUCTURE, this::addStructureDiscoveryResult);
private final DiscoveryDataListener<Thermostat> thermostatDiscoveryDataListener = new DiscoveryDataListener<>(
Thermostat.class, THING_TYPE_THERMOSTAT, this::addDeviceDiscoveryResult);
@SuppressWarnings("rawtypes")
private final List<DiscoveryDataListener> discoveryDataListeners = Stream.of(cameraDiscoveryDataListener,
smokeDetectorDiscoveryDataListener, structureDiscoveryDataListener, thermostatDiscoveryDataListener)
.collect(Collectors.toList());
private final NestBridgeHandler bridge;
private static class DiscoveryDataListener<T> implements NestThingDataListener<T> {
private Class<T> dataClass;
private ThingTypeUID thingTypeUID;
private BiConsumer<T, ThingTypeUID> onDiscovered;
private DiscoveryDataListener(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
this.dataClass = dataClass;
this.thingTypeUID = thingTypeUID;
this.onDiscovered = onDiscovered;
}
@Override
public void onNewData(T data) {
onDiscovered.accept(data, thingTypeUID);
}
@Override
public void onUpdatedData(T oldData, T data) {
}
@Override
public void onMissingData(String nestId) {
}
}
public NestDiscoveryService(NestBridgeHandler bridge) {
super(SUPPORTED_THING_TYPES, 60, true);
this.bridge = bridge;
}
@SuppressWarnings("unchecked")
public void activate() {
discoveryDataListeners.forEach(l -> bridge.addThingDataListener(l.dataClass, l));
addDiscoveryResultsFromLastUpdates();
}
@Override
@SuppressWarnings("unchecked")
public void deactivate() {
discoveryDataListeners.forEach(l -> bridge.removeThingDataListener(l.dataClass, l));
}
@Override
protected void startScan() {
addDiscoveryResultsFromLastUpdates();
}
@SuppressWarnings("unchecked")
private void addDiscoveryResultsFromLastUpdates() {
discoveryDataListeners
.forEach(l -> addDiscoveryResultsFromLastUpdates(l.dataClass, l.thingTypeUID, l.onDiscovered));
}
private <T> void addDiscoveryResultsFromLastUpdates(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
List<T> lastUpdates = bridge.getLastUpdates(dataClass);
lastUpdates.forEach(lastUpdate -> onDiscovered.accept(lastUpdate, thingTypeUID));
}
private void addDeviceDiscoveryResult(BaseNestDevice device, ThingTypeUID typeUID) {
ThingUID bridgeUID = bridge.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, device.getDeviceId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = new HashMap<>();
properties.put(NestDeviceConfiguration.DEVICE_ID, device.getDeviceId());
properties.put(PROPERTY_FIRMWARE_VERSION, device.getSoftwareVersion());
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(thingUID)
.withThingType(typeUID)
.withLabel(device.getNameLong())
.withBridge(bridgeUID)
.withProperties(properties)
.withRepresentationProperty(NestDeviceConfiguration.DEVICE_ID)
.build()
);
// @formatter:on
}
public void addStructureDiscoveryResult(Structure structure, ThingTypeUID typeUID) {
ThingUID bridgeUID = bridge.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, structure.getStructureId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = new HashMap<>();
properties.put(NestStructureConfiguration.STRUCTURE_ID, structure.getStructureId());
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(thingUID)
.withThingType(THING_TYPE_STRUCTURE)
.withLabel(structure.getName())
.withBridge(bridgeUID)
.withProperties(properties)
.withRepresentationProperty(NestStructureConfiguration.STRUCTURE_ID)
.build()
);
// @formatter:on
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm;
import static java.util.Map.entry;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.sdm.dto.SDMDeviceType;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SDMBindingConstants} class defines common constants, which are used for the SDM implementation in the
* binding.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMBindingConstants {
private static final String BINDING_ID = "nest";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "sdm_account");
public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "sdm_camera");
public static final ThingTypeUID THING_TYPE_DISPLAY = new ThingTypeUID(BINDING_ID, "sdm_display");
public static final ThingTypeUID THING_TYPE_DOORBELL = new ThingTypeUID(BINDING_ID, "sdm_doorbell");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "sdm_thermostat");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_CAMERA,
THING_TYPE_DISPLAY, THING_TYPE_DOORBELL, THING_TYPE_THERMOSTAT);
// Maps SDM device types to Thing Types UIDs
public static final Map<SDMDeviceType, ThingTypeUID> SDM_THING_TYPE_MAPPING = Map.ofEntries(
entry(SDMDeviceType.CAMERA, THING_TYPE_CAMERA), //
entry(SDMDeviceType.DISPLAY, THING_TYPE_DISPLAY), //
entry(SDMDeviceType.DOORBELL, THING_TYPE_DOORBELL), //
entry(SDMDeviceType.THERMOSTAT, THING_TYPE_THERMOSTAT));
// List of all Channel ids
public static final String CHANNEL_CHIME_EVENT_IMAGE = "chime_event#image";
public static final String CHANNEL_CHIME_EVENT_TIMESTAMP = "chime_event#timestamp";
public static final String CHANNEL_LIVE_STREAM_URL = "live_stream#url";
public static final String CHANNEL_LIVE_STREAM_CURRENT_TOKEN = "live_stream#current_token";
public static final String CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP = "live_stream#expiration_timestamp";
public static final String CHANNEL_LIVE_STREAM_EXTENSION_TOKEN = "live_stream#extension_token";
public static final String CHANNEL_MOTION_EVENT_IMAGE = "motion_event#image";
public static final String CHANNEL_MOTION_EVENT_TIMESTAMP = "motion_event#timestamp";
public static final String CHANNEL_PERSON_EVENT_IMAGE = "person_event#image";
public static final String CHANNEL_PERSON_EVENT_TIMESTAMP = "person_event#timestamp";
public static final String CHANNEL_SOUND_EVENT_IMAGE = "sound_event#image";
public static final String CHANNEL_SOUND_EVENT_TIMESTAMP = "sound_event#timestamp";
public static final String CHANNEL_AMBIENT_HUMIDITY = "ambient_humidity";
public static final String CHANNEL_AMBIENT_TEMPERATURE = "ambient_temperature";
public static final String CHANNEL_CURRENT_ECO_MODE = "current_eco_mode";
public static final String CHANNEL_CURRENT_MODE = "current_mode";
public static final String CHANNEL_FAN_TIMER_MODE = "fan_timer_mode";
public static final String CHANNEL_FAN_TIMER_TIMEOUT = "fan_timer_timeout";
public static final String CHANNEL_HVAC_STATUS = "hvac_status";
public static final String CHANNEL_MAXIMUM_TEMPERATURE = "maximum_temperature";
public static final String CHANNEL_MINIMUM_TEMPERATURE = "minimum_temperature";
public static final String CHANNEL_TARGET_TEMPERATURE = "target_temperature";
// List of all configuration property IDs
public static final String CONFIG_PROPERTY_FAN_TIMER_DURATION = "fanTimerDuration";
public static final String CONFIG_PROPERTY_IMAGE_HEIGHT = "imageHeight";
public static final String CONFIG_PROPERTY_IMAGE_WIDTH = "imageWidth";
// List of all property IDs
public static final String PROPERTY_AUDIO_CODECS = "audioCodecs";
public static final String PROPERTY_CUSTOM_NAME = "customName";
public static final String PROPERTY_MAX_IMAGE_RESOLUTION = "maxImageResolution";
public static final String PROPERTY_MAX_VIDEO_RESOLUTION = "maxVideoResolution";
public static final String PROPERTY_SUPPORTED_PROTOCOLS = "supportedProtocols";
public static final String PROPERTY_ROOM = "room";
public static final String PROPERTY_TEMPERATURE_SCALE = "temperatureScale";
public static final String PROPERTY_VIDEO_CODECS = "videoCodecs";
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm;
import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.sdm.handler.SDMAccountHandler;
import org.openhab.binding.nest.internal.sdm.handler.SDMCameraHandler;
import org.openhab.binding.nest.internal.sdm.handler.SDMThermostatHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link SDMThingHandlerFactory} is responsible for creating SDM thing handlers.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest")
@NonNullByDefault
public class SDMThingHandlerFactory extends BaseThingHandlerFactory {
private HttpClientFactory httpClientFactory;
private OAuthFactory oAuthFactory;
private final TimeZoneProvider timeZoneProvider;
@Activate
public SDMThingHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory, final @Reference TimeZoneProvider timeZoneProvider) {
this.httpClientFactory = httpClientFactory;
this.oAuthFactory = oAuthFactory;
this.timeZoneProvider = timeZoneProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) {
return new SDMAccountHandler((Bridge) thing, httpClientFactory, oAuthFactory);
} else if (thingTypeUID.equals(THING_TYPE_CAMERA)) {
return new SDMCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(THING_TYPE_DISPLAY)) {
return new SDMCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(THING_TYPE_DOORBELL)) {
return new SDMCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(THING_TYPE_THERMOSTAT)) {
return new SDMThermostatHandler(thing, timeZoneProvider);
}
return null;
}
}

View File

@ -0,0 +1,284 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.api;
import static org.eclipse.jetty.http.HttpHeader.*;
import static org.eclipse.jetty.http.HttpMethod.POST;
import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubAcknowledgeRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubCreateRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullResponse;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingPubSubDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAccessTokenException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAuthorizationCodeException;
import org.openhab.binding.nest.internal.sdm.listener.PubSubSubscriptionListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubAPI} implements a subset of the Pub/Sub REST API which allows for subscribing to SDM events.
*
* @author Wouter Born - Initial contribution
*
* @see https://cloud.google.com/pubsub/docs/reference/rest
* @see https://developers.google.com/nest/device-access/api/events
*/
@NonNullByDefault
public class PubSubAPI {
private class Subscriber implements Runnable {
private final String subscriptionId;
Subscriber(String subscriptionId) {
this.subscriptionId = subscriptionId;
}
@Override
public void run() {
if (!subscriptionListeners.containsKey(subscriptionId)) {
logger.debug("Stop receiving subscription '{}' messages since there are no listeners", subscriptionId);
return;
}
try {
String messages = pullSubscriptionMessages(subscriptionId);
PubSubPullResponse pullResponse = GSON.fromJson(messages, PubSubPullResponse.class);
if (pullResponse != null && pullResponse.receivedMessages != null) {
logger.debug("Subscription '{}' has {} new message(s)", subscriptionId,
pullResponse.receivedMessages.size());
forEachListener((listener) -> pullResponse.receivedMessages
.forEach((message) -> listener.onMessage(message.message)));
List<String> ackIds = pullResponse.receivedMessages.stream().map(message -> message.ackId)
.collect(Collectors.toList());
acknowledgeSubscriptionMessages(subscriptionId, ackIds);
} else {
forEachListener((listener) -> listener.onNoNewMessages());
}
scheduler.submit(this);
} catch (FailedSendingPubSubDataException e) {
logger.debug("Expected exception while pulling message for '{}' subscription", subscriptionId, e);
Throwable cause = e.getCause();
if (!(cause instanceof InterruptedException)) {
forEachListener((listener) -> listener.onError(e));
scheduler.schedule(this, RETRY_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
}
} catch (InvalidPubSubAccessTokenException e) {
logger.warn("Cannot pull messages for '{}' subscription (access token invalid)", subscriptionId, e);
forEachListener((listener) -> listener.onError(e));
} catch (Exception e) {
logger.warn("Unexpected exception while pulling message for '{}' subscription", subscriptionId, e);
forEachListener((listener) -> listener.onError(e));
scheduler.schedule(this, RETRY_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
}
}
private void forEachListener(Consumer<PubSubSubscriptionListener> consumer) {
Set<PubSubSubscriptionListener> listeners = subscriptionListeners.get(subscriptionId);
if (listeners != null) {
listeners.forEach(consumer::accept);
} else {
logger.debug("Subscription '{}' has no listeners", subscriptionId);
}
}
}
private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token";
private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
private static final String PUBSUB_HANDLE_FORMAT = "%s.pubsub";
private static final String PUBSUB_SCOPE = "https://www.googleapis.com/auth/pubsub";
private static final String PUBSUB_URL_PREFIX = "https://pubsub.googleapis.com/v1/";
private static final int PUBSUB_PULL_MAX_MESSAGES = 10;
private static final String APPLICATION_JSON = "application/json";
private static final String BEARER = "Bearer ";
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private static final Duration RETRY_TIMEOUT = Duration.ofSeconds(30);
private final Logger logger = LoggerFactory.getLogger(PubSubAPI.class);
private final HttpClient httpClient;
private final OAuthClientService oAuthService;
private final String projectId;
private final ScheduledThreadPoolExecutor scheduler;
private final Map<String, Set<PubSubSubscriptionListener>> subscriptionListeners = new HashMap<>();
public PubSubAPI(HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory, String ownerId, String projectId,
String clientId, String clientSecret) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.projectId = projectId;
this.oAuthService = oAuthFactory.createOAuthClientService(String.format(PUBSUB_HANDLE_FORMAT, ownerId),
TOKEN_URL, AUTH_URL, clientId, clientSecret, PUBSUB_SCOPE, false);
scheduler = new ScheduledThreadPoolExecutor(3, new NamedThreadFactory(ownerId, true));
}
public void dispose() {
subscriptionListeners.clear();
scheduler.shutdownNow();
}
public void authorizeClient(String authorizationCode) throws InvalidPubSubAuthorizationCodeException, IOException {
try {
oAuthService.getAccessTokenResponseByAuthorizationCode(authorizationCode, REDIRECT_URI);
} catch (OAuthException | OAuthResponseException e) {
throw new InvalidPubSubAuthorizationCodeException(
"Failed to authorize Pub/Sub client. Check the authorization code or generate a new one.", e);
}
}
public void checkAccessTokenValidity() throws InvalidPubSubAccessTokenException, IOException {
getAuthorizationHeader();
}
private String acknowledgeSubscriptionMessages(String subscriptionId, List<String> ackIds)
throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException {
logger.debug("Acknowleding {} message(s) for '{}' subscription", ackIds.size(), subscriptionId);
String url = getSubscriptionUrl(subscriptionId) + ":acknowledge";
String requestContent = GSON.toJson(new PubSubAcknowledgeRequest(ackIds));
return postJson(url, requestContent);
}
public void addSubscriptionListener(String subscriptionId, PubSubSubscriptionListener listener) {
synchronized (subscriptionListeners) {
Set<PubSubSubscriptionListener> listeners = subscriptionListeners.get(subscriptionId);
if (listeners == null) {
listeners = new HashSet<>();
subscriptionListeners.put(subscriptionId, listeners);
}
listeners.add(listener);
if (listeners.size() == 1) {
scheduler.submit(new Subscriber(subscriptionId));
}
}
}
public void removeSubscriptionListener(String subscriptionId, PubSubSubscriptionListener listener) {
synchronized (subscriptionListeners) {
Set<PubSubSubscriptionListener> listeners = subscriptionListeners.get(subscriptionId);
if (listeners != null) {
listeners.remove(listener);
if (listeners.isEmpty()) {
subscriptionListeners.remove(subscriptionId);
scheduler.getQueue().removeIf((runnable) -> runnable instanceof Subscriber
&& ((Subscriber) runnable).subscriptionId.equals(subscriptionId));
}
}
}
}
public void createSubscription(String subscriptionId, String topicName)
throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException {
logger.debug("Creating '{}' subscription", subscriptionId);
String url = getSubscriptionUrl(subscriptionId);
String requestContent = GSON.toJson(new PubSubCreateRequest(topicName, true));
putJson(url, requestContent);
}
private String getAuthorizationHeader() throws InvalidPubSubAccessTokenException, IOException {
try {
AccessTokenResponse response = oAuthService.getAccessTokenResponse();
if (response == null || response.getAccessToken() == null || response.getAccessToken().isEmpty()) {
throw new InvalidPubSubAccessTokenException(
"No Pub/Sub access token. Client may not have been authorized.");
}
return BEARER + response.getAccessToken();
} catch (OAuthException | OAuthResponseException e) {
throw new InvalidPubSubAccessTokenException(
"Error fetching Pub/Sub access token. Check the authorization code or generate a new one.", e);
}
}
private String getSubscriptionUrl(String subscriptionId) {
return PUBSUB_URL_PREFIX + "projects/" + projectId + "/subscriptions/" + subscriptionId;
}
private String postJson(String url, String requestContent)
throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException {
try {
logger.debug("Posting JSON to: {}", url);
String response = httpClient.newRequest(url) //
.method(POST) //
.header(ACCEPT, APPLICATION_JSON) //
.header(AUTHORIZATION, getAuthorizationHeader()) //
.content(new StringContentProvider(requestContent), APPLICATION_JSON) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send() //
.getContentAsString();
logger.debug("Response: {}", response);
return response;
} catch (ExecutionException | InterruptedException | IOException | TimeoutException e) {
throw new FailedSendingPubSubDataException("Failed to send JSON POST request", e);
}
}
private String pullSubscriptionMessages(String subscriptionId)
throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException {
logger.debug("Pulling messages for '{}' subscription", subscriptionId);
String url = getSubscriptionUrl(subscriptionId) + ":pull";
String requestContent = GSON.toJson(new PubSubPullRequest(PUBSUB_PULL_MAX_MESSAGES));
return postJson(url, requestContent);
}
private String putJson(String url, String requestContent)
throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException {
try {
logger.debug("Putting JSON to: {}", url);
String response = httpClient.newRequest(url) //
.method(HttpMethod.PUT) //
.header(ACCEPT, APPLICATION_JSON) //
.header(AUTHORIZATION, getAuthorizationHeader()) //
.content(new StringContentProvider(requestContent), APPLICATION_JSON) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send() //
.getContentAsString();
logger.debug("Response: {}", response);
return response;
} catch (ExecutionException | InterruptedException | IOException | TimeoutException e) {
throw new FailedSendingPubSubDataException("Failed to send JSON PUT request", e);
}
}
}

View File

@ -0,0 +1,340 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.api;
import static org.eclipse.jetty.http.HttpHeader.*;
import static org.eclipse.jetty.http.HttpMethod.*;
import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMDevice;
import org.openhab.binding.nest.internal.sdm.dto.SDMError;
import org.openhab.binding.nest.internal.sdm.dto.SDMError.SDMErrorDetails;
import org.openhab.binding.nest.internal.sdm.dto.SDMListDevicesResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMListRoomsResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMListStructuresResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMRoom;
import org.openhab.binding.nest.internal.sdm.dto.SDMStructure;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAuthorizationCodeException;
import org.openhab.binding.nest.internal.sdm.listener.SDMAPIRequestListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMAPI} implements the SDM REST API which allows for querying Nest device, structure and room information
* as well as executing device commands.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest
*/
@NonNullByDefault
public class SDMAPI {
private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token";
private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
private static final String SDM_HANDLE_FORMAT = "%s.sdm";
private static final String SDM_SCOPE = "https://www.googleapis.com/auth/sdm.service";
private static final String SDM_URL_PREFIX = "https://smartdevicemanagement.googleapis.com/v1/enterprises/";
private static final String APPLICATION_JSON = "application/json";
private static final String BEARER = "Bearer ";
private static final String IMAGE_JPEG = "image/jpeg";
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private final Logger logger = LoggerFactory.getLogger(SDMAPI.class);
private final HttpClient httpClient;
private final OAuthClientService oAuthService;
private final String projectId;
private final Set<SDMAPIRequestListener> requestListeners = ConcurrentHashMap.newKeySet();
public SDMAPI(HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory, String ownerId, String projectId,
String clientId, String clientSecret) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.oAuthService = oAuthFactory.createOAuthClientService(String.format(SDM_HANDLE_FORMAT, ownerId), TOKEN_URL,
AUTH_URL, clientId, clientSecret, SDM_SCOPE, false);
this.projectId = projectId;
}
public void dispose() {
requestListeners.clear();
}
public void authorizeClient(String authorizationCode) throws InvalidSDMAuthorizationCodeException, IOException {
try {
oAuthService.getAccessTokenResponseByAuthorizationCode(authorizationCode, REDIRECT_URI);
} catch (OAuthException | OAuthResponseException e) {
throw new InvalidSDMAuthorizationCodeException(
"Failed to authorize SDM client. Check the authorization code or generate a new one.", e);
}
}
public void checkAccessTokenValidity() throws InvalidSDMAccessTokenException, IOException {
getAuthorizationHeader();
}
public void addRequestListener(SDMAPIRequestListener listener) {
requestListeners.add(listener);
}
public void removeRequestListener(SDMAPIRequestListener listener) {
requestListeners.remove(listener);
}
public <T extends SDMCommandResponse> @Nullable T executeDeviceCommand(String deviceId,
SDMCommandRequest<T> request) throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Executing device command for: {}", deviceId);
String requestContent = GSON.toJson(request);
String responseContent = postJson(getDeviceUrl(deviceId) + ":executeCommand", requestContent);
return GSON.fromJson(responseContent, request.getResponseClass());
}
private String getAuthorizationHeader() throws InvalidSDMAccessTokenException, IOException {
try {
AccessTokenResponse response = oAuthService.getAccessTokenResponse();
if (response == null || response.getAccessToken() == null || response.getAccessToken().isEmpty()) {
throw new InvalidSDMAccessTokenException("No SDM access token. Client may not have been authorized.");
}
return BEARER + response.getAccessToken();
} catch (OAuthException | OAuthResponseException e) {
throw new InvalidSDMAccessTokenException(
"Error fetching SDM access token. Check the authorization code or generate a new one.", e);
}
}
public byte[] getCameraImage(String url, String token, @Nullable BigDecimal imageWidth,
@Nullable BigDecimal imageHeight) throws FailedSendingSDMDataException {
try {
logger.debug("Getting camera image from: {}", url);
Request request = httpClient.newRequest(url) //
.method(GET) //
.header(ACCEPT, IMAGE_JPEG) //
.header(AUTHORIZATION, token) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
if (imageWidth != null) {
request = request.param("width", Long.toString(imageWidth.longValue()));
} else if (imageHeight != null) {
request = request.param("height", Long.toString(imageHeight.longValue()));
}
ContentResponse contentResponse = request.send();
logResponseErrors(contentResponse);
logger.debug("Retrieved camera image from: {}", url);
requestListeners.forEach(listener -> listener.onSuccess());
return contentResponse.getContent();
} catch (ExecutionException | InterruptedException | TimeoutException e) {
logger.debug("Failed to get camera image", e);
FailedSendingSDMDataException exception = new FailedSendingSDMDataException("Failed to get camera image",
e);
requestListeners.forEach(listener -> listener.onError(exception));
throw exception;
}
}
public @Nullable SDMDevice getDevice(String deviceId)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Getting device: {}", deviceId);
return GSON.fromJson(getJson(getDeviceUrl(deviceId)), SDMDevice.class);
}
public @Nullable SDMStructure getStructure(String structureId)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Getting structure: {}", structureId);
return GSON.fromJson(getJson(getStructureUrl(structureId)), SDMStructure.class);
}
public @Nullable SDMRoom getRoom(String structureId, String roomId)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Getting structure {} room: {}", structureId, roomId);
return GSON.fromJson(getJson(getRoomUrl(structureId, roomId)), SDMRoom.class);
}
private String getProjectUrl() {
return SDM_URL_PREFIX + projectId;
}
private String getDevicesUrl() {
return getProjectUrl() + "/devices";
}
private String getDevicesUrl(String pageToken) {
return getDevicesUrl() + "?pageToken=" + pageToken;
}
private String getDeviceUrl(String deviceId) {
return getDevicesUrl() + "/" + deviceId;
}
private String getStructuresUrl() {
return getProjectUrl() + "/structures";
}
private String getStructuresUrl(String pageToken) {
return getStructuresUrl() + "?pageToken=" + pageToken;
}
private String getStructureUrl(String structureId) {
return getStructuresUrl() + "/" + structureId;
}
private String getRoomsUrl(String structureId) {
return getStructureUrl(structureId) + "/rooms";
}
private String getRoomsUrl(String structureId, String pageToken) {
return getRoomsUrl(structureId) + "?pageToken=" + pageToken;
}
private String getRoomUrl(String structureId, String roomId) {
return getRoomsUrl(structureId) + "/" + roomId;
}
public List<SDMDevice> listDevices() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Listing devices");
SDMListDevicesResponse response = GSON.fromJson(getJson(getDevicesUrl()), SDMListDevicesResponse.class);
List<SDMDevice> result = response == null ? List.of() : response.devices;
while (response != null && !response.nextPageToken.isEmpty()) {
response = GSON.fromJson(getJson(getDevicesUrl(response.nextPageToken)), SDMListDevicesResponse.class);
if (response != null) {
result.addAll(response.devices);
}
}
return result;
}
public List<SDMStructure> listStructures() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Listing structures");
SDMListStructuresResponse response = GSON.fromJson(getJson(getStructuresUrl()),
SDMListStructuresResponse.class);
List<SDMStructure> result = response == null ? List.of() : response.structures;
while (response != null && !response.nextPageToken.isEmpty()) {
response = GSON.fromJson(getJson(getStructuresUrl(response.nextPageToken)),
SDMListStructuresResponse.class);
if (response != null) {
result.addAll(response.structures);
}
}
return result;
}
public List<SDMRoom> listRooms(String structureId)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("Listing rooms for structure: {}", structureId);
SDMListRoomsResponse response = GSON.fromJson(getJson(getRoomsUrl(structureId)), SDMListRoomsResponse.class);
List<SDMRoom> result = response == null ? List.of() : response.rooms;
while (response != null && !response.nextPageToken.isEmpty()) {
response = GSON.fromJson(getJson(getRoomsUrl(structureId, response.nextPageToken)),
SDMListRoomsResponse.class);
if (response != null) {
result.addAll(response.rooms);
}
}
return result;
}
private void logResponseErrors(ContentResponse contentResponse) {
if (contentResponse.getStatus() >= 400) {
logger.debug("SDM API error: {}", contentResponse.getContentAsString());
SDMError error = GSON.fromJson(contentResponse.getContentAsString(), SDMError.class);
SDMErrorDetails details = error == null ? null : error.error;
if (details != null && !details.message.isBlank()) {
logger.warn("SDM API error: {}", details.message);
} else {
logger.warn("SDM API error: {} (HTTP {})", contentResponse.getReason(), contentResponse.getStatus());
}
}
}
private String getJson(String url) throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
try {
logger.debug("Getting JSON from: {}", url);
ContentResponse contentResponse = httpClient.newRequest(url) //
.method(GET) //
.header(ACCEPT, APPLICATION_JSON) //
.header(AUTHORIZATION, getAuthorizationHeader()) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send();
logResponseErrors(contentResponse);
String response = contentResponse.getContentAsString();
logger.debug("Response: {}", response);
requestListeners.forEach(listener -> listener.onSuccess());
return response;
} catch (ExecutionException | InterruptedException | IOException | TimeoutException e) {
logger.debug("Failed to send JSON GET request", e);
FailedSendingSDMDataException exception = new FailedSendingSDMDataException(
"Failed to send JSON GET request", e);
requestListeners.forEach(listener -> listener.onError(exception));
throw exception;
}
}
private String postJson(String url, String requestContent)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
try {
logger.debug("Posting JSON to: {}", url);
ContentResponse contentResponse = httpClient.newRequest(url) //
.method(POST) //
.header(ACCEPT, APPLICATION_JSON) //
.header(AUTHORIZATION, getAuthorizationHeader()) //
.content(new StringContentProvider(requestContent), APPLICATION_JSON) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send();
logResponseErrors(contentResponse);
String response = contentResponse.getContentAsString();
logger.debug("Response: {}", response);
requestListeners.forEach(listener -> listener.onSuccess());
return response;
} catch (ExecutionException | InterruptedException | IOException | TimeoutException e) {
logger.debug("Failed to send JSON POST request", e);
FailedSendingSDMDataException exception = new FailedSendingSDMDataException(
"Failed to send JSON POST request", e);
requestListeners.forEach(listener -> listener.onError(exception));
throw exception;
}
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.config;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SDMAccountConfiguration} contains the configuration parameter values for the SDM and Pub/Sub APIs.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMAccountConfiguration {
public static final String PUBSUB_AUTHORIZATION_CODE = "pubsubAuthorizationCode";
public String pubsubAuthorizationCode = "";
public static final String PUBSUB_CLIENT_ID = "pubsubClientId";
public String pubsubClientId = "";
public static final String PUBSUB_CLIENT_SECRET = "pubsubClientSecret";
public String pubsubClientSecret = "";
public static final String PUBSUB_PROJECT_ID = "pubsubProjectId";
public String pubsubProjectId = "";
public static final String PUBSUB_SUBSCRIPTION_ID = "pubsubSubscriptionId";
public String pubsubSubscriptionId = "";
public static final String SDM_AUTHORIZATION_CODE = "sdmAuthorizationCode";
public String sdmAuthorizationCode = "";
public static final String SDM_CLIENT_ID = "sdmClientId";
public String sdmClientId = "";
public static final String SDM_CLIENT_SECRET = "sdmClientSecret";
public String sdmClientSecret = "";
public static final String SDM_PRODUCT_ID = "sdmProductId";
public String sdmProjectId = "";
public boolean usePubSub() {
return Stream.of(pubsubProjectId, pubsubSubscriptionId, pubsubClientId, pubsubClientSecret)
.noneMatch(String::isBlank);
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SDMDeviceConfiguration} contains the configuration parameter values for a SDM device.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMDeviceConfiguration {
public static final String DEVICE_ID = "deviceId";
public String deviceId = "";
public static final String REFRESH_INTERVAL = "refreshInterval";
public int refreshInterval = 300;
}

View File

@ -0,0 +1,146 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.discovery;
import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.sdm.config.SDMDeviceConfiguration;
import org.openhab.binding.nest.internal.sdm.dto.SDMDevice;
import org.openhab.binding.nest.internal.sdm.dto.SDMDeviceType;
import org.openhab.binding.nest.internal.sdm.dto.SDMParentRelation;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.binding.nest.internal.sdm.handler.SDMAccountHandler;
import org.openhab.binding.nest.internal.sdm.handler.SDMBaseHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMDiscoveryService} is discovers devices using the SDM API list devices method.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/list
*/
@NonNullByDefault
public class SDMDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(SDMDiscoveryService.class);
private @NonNullByDefault({}) SDMAccountHandler accountHandler;
private @Nullable Future<?> discoveryJob;
public SDMDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, 30, false);
}
protected void activate(ComponentContext context) {
}
@Override
public void deactivate() {
cancelDiscoveryJob();
super.deactivate();
}
@Override
public @Nullable ThingHandler getThingHandler() {
return accountHandler;
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof SDMAccountHandler) {
accountHandler = (SDMAccountHandler) handler;
}
}
@Override
protected void startScan() {
cancelDiscoveryJob();
discoveryJob = scheduler.submit(this::discoverDevices);
}
@Override
protected synchronized void stopScan() {
cancelDiscoveryJob();
super.stopScan();
}
private void cancelDiscoveryJob() {
Future<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob != null) {
localDiscoveryJob.cancel(true);
}
}
private void discoverDevices() {
ThingUID bridgeUID = accountHandler.getThing().getUID();
logger.debug("Starting discovery scan for {}", bridgeUID);
try {
accountHandler.getAPI().listDevices().forEach(device -> addDeviceDiscoveryResult(bridgeUID, device));
} catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
logger.debug("Exception during discovery scan for {}", bridgeUID, e);
}
logger.debug("Finished discovery scan for {}", bridgeUID);
}
private void addDeviceDiscoveryResult(ThingUID bridgeUID, SDMDevice device) {
SDMDeviceType type = device.type;
ThingTypeUID thingTypeUID = type == null ? null : SDM_THING_TYPE_MAPPING.get(type);
if (type == null || thingTypeUID == null) {
logger.debug("Ignoring unsupported device type: {}", type);
return;
}
String deviceId = device.name.deviceId;
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceId);
thingDiscovered(DiscoveryResultBuilder.create(thingUID) //
.withThingType(thingTypeUID) //
.withLabel(getDeviceLabel(device, type)) //
.withBridge(bridgeUID) //
.withProperty(SDMDeviceConfiguration.DEVICE_ID, deviceId) //
.withProperties(new HashMap<>(SDMBaseHandler.getDeviceProperties(device))) //
.withRepresentationProperty(SDMDeviceConfiguration.DEVICE_ID) //
.build() //
);
}
private String getDeviceLabel(SDMDevice device, SDMDeviceType type) {
String label = device.traits.deviceInfo.customName;
if (!label.isBlank()) {
return label;
}
List<SDMParentRelation> parentRelations = device.parentRelations;
String displayName = !parentRelations.isEmpty() ? parentRelations.get(0).displayName : "";
String typeLabel = type.toLabel();
return displayName.isBlank() ? String.format("Nest %s", typeLabel)
: String.format("Nest %s %s", displayName, typeLabel);
}
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.time.ZonedDateTime;
import java.util.List;
/**
* The {@link PubSubRequestsResponses} provides classes used for mapping Pub/Sub REST API requests and responses.
* Only the subset of requests/responses and fields that are used by the binding are implemented.
*
* @author Wouter Born - Initial contribution
*
* @see https://cloud.google.com/pubsub/docs/reference/rest
*/
public class PubSubRequestsResponses {
// Method: projects.subscriptions.acknowledge
/**
* Acknowledges the messages associated with the ackIds in the AcknowledgeRequest. The Pub/Sub system can remove the
* relevant messages from the subscription.
*
* Acknowledging a message whose ack deadline has expired may succeed, but such a message may be redelivered later.
* Acknowledging a message more than once will not result in an error.
*/
public static class PubSubAcknowledgeRequest {
public List<String> ackIds;
public PubSubAcknowledgeRequest(List<String> ackIds) {
this.ackIds = ackIds;
}
}
// Method: projects.subscriptions.create
/**
* Creates a subscription to a given topic. See the resource name rules. If the subscription already exists, returns
* ALREADY_EXISTS. If the corresponding topic doesn't exist, returns NOT_FOUND.
*
* If the name is not provided in the request, the server will assign a random name for this subscription on the
* same project as the topic, conforming to the resource name format. The generated name is populated in the
* returned Subscription object. Note that for REST API requests, you must specify a name in the request.
*/
public static class PubSubCreateRequest {
public String topic;
public boolean enableMessageOrdering;
/**
* @param topic The name of the topic from which this subscription is receiving messages. Format is
* <code>projects/{project}/topics/{topic}</code>.
* @param enableMessageOrdering If true, messages published with the same orderingKey in the message will be
* delivered to the subscribers in the order in which they are received by the Pub/Sub system.
* Otherwise, they may be delivered in any order.
*/
public PubSubCreateRequest(String topic, boolean enableMessageOrdering) {
this.topic = topic;
this.enableMessageOrdering = enableMessageOrdering;
}
}
// Method: projects.subscriptions.pull
/**
* Pulls messages from the server. The server may return UNAVAILABLE if there are too many concurrent pull requests
* pending for the given subscription.
*
* A {@link PubSubPullResponse} is returned when successful.
*/
public static class PubSubPullRequest {
public int maxMessages;
/**
* @param maxMessages The maximum number of messages to return for this request. Must be a positive integer. The
* Pub/Sub system may return fewer than the number specified.
*/
public PubSubPullRequest(int maxMessages) {
this.maxMessages = maxMessages;
}
}
/**
* A message that is published by publishers and consumed by subscribers.
*/
public static class PubSubMessage {
/**
* The message data field. A base64-encoded string.
*/
public String data;
/**
* ID of this message, assigned by the server when the message is published. Guaranteed to be unique within the
* topic. This value may be read by a subscriber that receives a PubsubMessage via a
* <code>subscriptions.pull</code> call or a push delivery. It must not be populated by the publisher in a
* topics.publish call.
*/
public String messageId;
/**
* The time at which the message was published, populated by the server when it receives the topics.publish
* call. It must not be populated by the publisher in a topics publish call.
*
* A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits.
* Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z".
*/
public ZonedDateTime publishTime;
}
/**
* A message and its corresponding acknowledgment ID.
*/
public static class PubSubReceivedMessage {
/**
* This ID can be used to acknowledge the received message.
*/
public String ackId;
/**
* The message.
*/
public PubSubMessage message;
}
/**
* Response to a {@link PubSubPullRequest}.
*/
public class PubSubPullResponse {
/**
* Received Pub/Sub messages. The list will be empty if there are no more messages available in the backlog. For
* JSON, the response can be entirely empty. The Pub/Sub system may return fewer than the maxMessages requested
* even if there are more messages available in the backlog.
*/
public List<PubSubReceivedMessage> receivedMessages;
}
}

View File

@ -0,0 +1,318 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import static java.util.Map.entry;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode;
/**
* The {@link SDMCommands} provides classes used for mapping all SDM REST API device command requests and responses.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/executeCommand
*/
public class SDMCommands {
/**
* Command request parent.
*/
public abstract static class SDMCommandRequest<T extends SDMCommandResponse> {
private final String command;
private final Map<String, Object> params = new LinkedHashMap<>();
@SafeVarargs
private SDMCommandRequest(String command, Entry<String, Object>... params) {
this.command = command;
for (Entry<String, Object> param : params) {
this.params.put(param.getKey(), param.getValue());
}
}
public String getCommand() {
return command;
}
public Map<String, Object> getParams() {
return params;
}
@SuppressWarnings("unchecked")
public Class<T> getResponseClass() {
return (Class<T>) SDMCommandResponse.class;
}
}
/**
* Command response parent. This class is also used for responses without additional data.
*/
public static class SDMCommandResponse {
}
// CameraEventImage trait commands
/**
* Generates a download URL for the image related to a camera event.
*/
public static class SDMGenerateCameraImageRequest extends SDMCommandRequest<SDMGenerateCameraImageResponse> {
/**
* Event images expire 30 seconds after the event is published. Make sure to download the image prior to
* expiration.
*/
public static final Duration EVENT_IMAGE_VALIDITY = Duration.ofSeconds(30);
/**
* @param eventId ID of the camera event to request a related image for.
*/
public SDMGenerateCameraImageRequest(String eventId) {
super("sdm.devices.commands.CameraEventImage.GenerateImage", entry("eventId", eventId));
}
@Override
public Class<SDMGenerateCameraImageResponse> getResponseClass() {
return SDMGenerateCameraImageResponse.class;
}
}
public static class SDMGenerateCameraImageResults {
/**
* The URL to download the camera image from.
*/
public String url;
/**
* Token to use in the HTTP Authorization header when downloading the camera image.
*/
public String token;
}
public static class SDMGenerateCameraImageResponse extends SDMCommandResponse {
public SDMGenerateCameraImageResults results;
}
// CameraLiveStream trait commands
/**
* Request a token to access a camera RTSP live stream URL.
*/
public static class SDMGenerateCameraRtspStreamRequest
extends SDMCommandRequest<SDMGenerateCameraRtspStreamResponse> {
public SDMGenerateCameraRtspStreamRequest() {
super("sdm.devices.commands.CameraLiveStream.GenerateRtspStream");
}
@Override
public Class<SDMGenerateCameraRtspStreamResponse> getResponseClass() {
return SDMGenerateCameraRtspStreamResponse.class;
}
}
/**
* Camera RTSP live stream URLs.
*/
public static class SDMCameraRtspStreamUrls {
public String rtspUrl;
}
public static class SDMGenerateCameraRtspStreamResults {
/**
* Camera RTSP live stream URLs.
*/
public SDMCameraRtspStreamUrls streamUrls;
/**
* Token to use to extend the {@link #streamToken} for an RTSP live stream.
*/
public String streamExtensionToken;
/**
* Token to use to access an RTSP live stream.
*/
public String streamToken;
/**
* Time at which both {@link #streamExtensionToken} and {@link #streamToken} expire.
*/
public ZonedDateTime expiresAt;
}
public static class SDMGenerateCameraRtspStreamResponse extends SDMCommandResponse {
public SDMGenerateCameraRtspStreamResults results;
}
/**
* Request a new RTSP live stream URL access token to replace a valid RTSP access token before it expires. This is
* also used to replace a valid RTSP token from a previous ExtendRtspStream command request.
*/
public static class SDMExtendCameraRtspStreamRequest extends SDMCommandRequest<SDMExtendCameraRtspStreamResponse> {
/**
* @param streamExtensionToken Token to use to request an extension to the RTSP streaming token.
*/
public SDMExtendCameraRtspStreamRequest(String streamExtensionToken) {
super("sdm.devices.commands.CameraLiveStream.ExtendRtspStream",
entry("streamExtensionToken", streamExtensionToken));
}
@Override
public Class<SDMExtendCameraRtspStreamResponse> getResponseClass() {
return SDMExtendCameraRtspStreamResponse.class;
}
}
public static class SDMExtendCameraRtspStreamResults {
/**
* Token to use to view an existing RTSP live stream and to request an extension to the streaming token.
*/
public String streamExtensionToken;
/**
* New token to use to access an existing RTSP live stream.
*/
public String streamToken;
/**
* Time at which both {@link #streamExtensionToken} and {@link #streamToken} expire.
*/
public ZonedDateTime expiresAt;
}
public static class SDMExtendCameraRtspStreamResponse extends SDMCommandResponse {
public SDMExtendCameraRtspStreamResults results;
}
/**
* Invalidates a valid RTSP access token and stops the RTSP live stream tied to that access token.
*/
public static class SDMStopCameraRtspStreamRequest extends SDMCommandRequest<SDMCommandResponse> {
/**
* @param streamExtensionToken Token to use to invalidate an existing RTSP live stream.
*/
public SDMStopCameraRtspStreamRequest(String streamExtensionToken) {
super("sdm.devices.commands.CameraLiveStream.StopRtspStream",
entry("streamExtensionToken", streamExtensionToken));
}
}
// Fan trait commands
/**
* Change the fan timer.
*/
public static class SDMSetFanTimerRequest extends SDMCommandRequest<SDMCommandResponse> {
public SDMSetFanTimerRequest(SDMFanTimerMode timerMode) {
super("sdm.devices.commands.Fan.SetTimer", entry("timerMode", timerMode.name()));
}
/**
* @param duration Specifies the length of time in seconds that the timer is set to run.
* Range: "1s" to "43200s"
* Default: "900s"
*/
public SDMSetFanTimerRequest(SDMFanTimerMode timerMode, Duration duration) {
super("sdm.devices.commands.Fan.SetTimer", entry("timerMode", timerMode.name()),
entry("duration", String.valueOf(duration.toSeconds()) + "s"));
}
}
// ThermostatEco trait commands
/**
* Change the thermostat Eco mode.
*
* To change the thermostat mode to HEAT, COOL, or HEATCOOL, use the {@link SDMSetThermostatModeRequest}.
* <br>
* <br>
* This command impacts other traits, based on the current status of, or changes to, the Eco mode:
* <ul>
* <li>If Eco mode is OFF, the thermostat mode will default to the last standard mode (HEAT, COOL, HEATCOOL, or OFF)
* that was active.</li>
* <li>If Eco mode is MANUAL_ECO:
* <ul>
* <li>Commands for the ThermostatTemperatureSetpoint trait are rejected.</li>
* <li>Temperature setpoints are not returned by the ThermostatTemperatureSetpoint trait.</li>
* </ul>
* </li>
* </ul>
*
* Some thermostat models do not support changing the Eco mode when the thermostat mode is OFF, according to the
* ThermostatMode trait. The thermostat mode must be changed to HEAT, COOL, or HEATCOOL prior to changing the Eco
* mode.
*/
public static class SDMSetThermostatEcoModeRequest extends SDMCommandRequest<SDMCommandResponse> {
public SDMSetThermostatEcoModeRequest(SDMThermostatEcoMode mode) {
super("sdm.devices.commands.ThermostatEco.SetMode", entry("mode", mode.name()));
}
}
// ThermostatMode trait commands
/**
* Change the thermostat mode.
*/
public static class SDMSetThermostatModeRequest extends SDMCommandRequest<SDMCommandResponse> {
public SDMSetThermostatModeRequest(SDMThermostatMode mode) {
super("sdm.devices.commands.ThermostatMode.SetMode", entry("mode", mode.name()));
}
}
// ThermostatTemperatureSetpoint trait commands
/**
* Sets the target temperature when the thermostat is in COOL mode.
*/
public static class SDMSetThermostatCoolSetpointRequest extends SDMCommandRequest<SDMCommandResponse> {
/**
* @param temperature the target temperature in degrees Celsius
*/
public SDMSetThermostatCoolSetpointRequest(BigDecimal temperature) {
super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool", entry("coolCelsius", temperature));
}
}
/**
* Sets the target temperature when the thermostat is in HEAT mode.
*/
public static class SDMSetThermostatHeatSetpointRequest extends SDMCommandRequest<SDMCommandResponse> {
/**
* @param temperature the target temperature in degrees Celsius
*/
public SDMSetThermostatHeatSetpointRequest(BigDecimal temperature) {
super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat", entry("heatCelsius", temperature));
}
}
/**
* Sets the minimum and maximum temperatures when the thermostat is in HEATCOOL mode.
*/
public static class SDMSetThermostatRangeSetpointRequest extends SDMCommandRequest<SDMCommandResponse> {
/**
* @param minTemperature the minimum target temperature in degrees Celsius
* @param maxTemperature the maximum target temperature in degrees Celsius
*/
public SDMSetThermostatRangeSetpointRequest(BigDecimal minTemperature, BigDecimal maxTemperature) {
super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange", entry("heatCelsius", minTemperature),
entry("coolCelsius", maxTemperature));
}
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* An instance of enterprise managed device in the property.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMDevice {
/**
* The resource name of the device.
*/
public SDMResourceName name = SDMResourceName.NAMELESS;
/**
* Type of the device for general display purposes.
*/
public @Nullable SDMDeviceType type;
/**
* Device traits.
*/
public SDMTraits traits = new SDMTraits();
/**
* Assignee details of the device.
*/
public List<SDMParentRelation> parentRelations = List.of();
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import com.google.gson.annotations.SerializedName;
/**
* Type of the SDM device.
*
* @author Wouter Born - Initial contribution
*/
public enum SDMDeviceType {
@SerializedName("sdm.devices.types.CAMERA")
CAMERA,
@SerializedName("sdm.devices.types.DISPLAY")
DISPLAY,
@SerializedName("sdm.devices.types.DOORBELL")
DOORBELL,
@SerializedName("sdm.devices.types.THERMOSTAT")
THERMOSTAT;
public String toLabel() {
return name().charAt(0) + name().toLowerCase().substring(1);
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
/**
* An error response of the SDM API.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/errors/api
*/
public class SDMError {
public static class SDMErrorDetails {
public int code;
public String message;
public String status;
}
public SDMErrorDetails error;
}

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.annotations.SerializedName;
/**
* The {@link SDMEvent} is used for mapping the SDM event data received from the SDM API in messages pulled from a
* Pub/Sub topic.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/api/events
*/
public class SDMEvent {
/**
* An object that details information about the relation update.
*/
public static class SDMRelationUpdate {
public SDMRelationUpdateType type;
/**
* The resource that the object now has a relation with.
*/
public SDMResourceName subject;
/**
* The resource that triggered the event.
*/
public SDMResourceName object;
}
public enum SDMRelationUpdateType {
CREATED,
DELETED,
UPDATED
}
/**
* An object that details information about the resource update.
*/
public static class SDMResourceUpdate {
public SDMResourceName name;
public SDMTraits traits;
public SDMResourceUpdateEvents events;
}
public static class SDMDeviceEvent {
public String eventId;
public String eventSessionId;
}
public static class SDMResourceUpdateEvents extends SDMTraits {
@SerializedName("sdm.devices.events.CameraMotion.Motion")
public SDMDeviceEvent cameraMotionEvent;
@SerializedName("sdm.devices.events.CameraPerson.Person")
public SDMDeviceEvent cameraPersonEvent;
@SerializedName("sdm.devices.events.CameraSound.Sound")
public SDMDeviceEvent cameraSoundEvent;
@SerializedName("sdm.devices.events.DoorbellChime.Chime")
public SDMDeviceEvent doorbellChimeEvent;
public <T> Stream<SDMDeviceEvent> eventStream() {
return Stream.of(cameraMotionEvent, cameraPersonEvent, cameraSoundEvent, doorbellChimeEvent)
.filter(Objects::nonNull);
}
public List<SDMDeviceEvent> eventList() {
return eventStream().collect(Collectors.toList());
}
public Set<SDMDeviceEvent> eventSet() {
return eventStream().collect(Collectors.toSet());
}
}
/**
* The unique identifier for the event.
*/
public String eventId;
/**
* An object that details information about the relation update.
*/
public SDMRelationUpdate relationUpdate;
/**
* An object that indicates resources that might have similar updates to this event.
* The resource of the event itself (from the resourceUpdate object) will always be present in this object.
*/
public List<SDMResourceName> resourceGroup;
/**
* An object that details information about the resource update.
*/
public SDMResourceUpdate resourceUpdate;
/**
* The time when the event occurred.
*/
public ZonedDateTime timestamp;
/**
* A unique, obfuscated identifier that represents the user.
*/
public String userId;
}

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.lang.reflect.Type;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* The {@link SDMGson} class provides a {@link Gson} instance configured for (de)serializing all SDM and Pub/Sub data
* from/to JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMGson {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(SDMResourceName.class, new SDMResourceNameConverter()) //
.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeConverter()) //
.create();
private static class SDMResourceNameConverter
implements JsonSerializer<SDMResourceName>, JsonDeserializer<SDMResourceName> {
@Override
public JsonElement serialize(SDMResourceName src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}
@Override
public @Nullable SDMResourceName deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return new SDMResourceName(json.getAsString());
}
}
private static class ZonedDateTimeConverter
implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
@Override
public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(FORMATTER.format(src));
}
@Override
public @Nullable ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return ZonedDateTime.parse(json.getAsString());
}
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for uniquely identifiable SDM objects (device, structure).
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface SDMIdentifiable {
/**
* Returns the identifier that uniquely identifies the SDM object (deviceId or structureId).
*/
String getId();
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Lists devices managed by the enterprise.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/list
*/
@NonNullByDefault
public class SDMListDevicesResponse {
/**
* The list of devices.
*/
public List<SDMDevice> devices = List.of();
/**
* The pagination token to retrieve the next page of results.
*/
public String nextPageToken = "";
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Lists rooms managed by the enterprise.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.structures.rooms/list
*/
@NonNullByDefault
public class SDMListRoomsResponse {
/**
* The list of rooms.
*/
public List<SDMRoom> rooms = List.of();
/**
* The pagination token to retrieve the next page of results.
*/
public String nextPageToken = "";
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Lists structures managed by the enterprise.
*
* @author Wouter Born - Initial contribution
*
* @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.structures/list
*/
@NonNullByDefault
public class SDMListStructuresResponse {
/**
* The list of structures.
*/
public List<SDMStructure> structures = List.of();
/**
* The pagination token to retrieve the next page of results.
*/
public String nextPageToken = "";
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
/**
* Represents device relationships, for instance, structure/room to which the device is assigned to.
*
* @author Wouter Born - Initial contribution
*/
public class SDMParentRelation {
/**
* The name of the relation.
*/
public SDMResourceName parent;
/**
* The custom name of the relation.
*/
public String displayName;
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A resource name uniquely identifies a structure, room or device.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMResourceName {
public enum SDMResourceNameType {
DEVICE,
ROOM,
STRUCTURE,
UNKNOWN
}
private static final Pattern PATTERN = Pattern
.compile("^enterprises/([^/]+)(/devices/([^/]+)|/structures/([^/]+)(/rooms/([^/]+))?)$");
public static final SDMResourceName NAMELESS = new SDMResourceName("");
public final String name;
public final String projectId;
public final String deviceId;
public final String structureId;
public final String roomId;
public final SDMResourceNameType type;
public SDMResourceName(String name) {
this.name = name;
Matcher matcher = PATTERN.matcher(name);
if (matcher.matches()) {
projectId = matcher.group(1);
deviceId = matcher.group(3) == null ? "" : matcher.group(3);
structureId = matcher.group(4) == null ? "" : matcher.group(4);
roomId = matcher.group(6) == null ? "" : matcher.group(6);
if (!deviceId.isEmpty()) {
type = SDMResourceNameType.DEVICE;
} else if (!roomId.isEmpty()) {
type = SDMResourceNameType.ROOM;
} else if (!structureId.isEmpty()) {
type = SDMResourceNameType.STRUCTURE;
} else {
type = SDMResourceNameType.UNKNOWN;
}
} else {
projectId = "";
deviceId = "";
structureId = "";
roomId = "";
type = SDMResourceNameType.UNKNOWN;
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
return prime * result + name.hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return name.equals(((SDMResourceName) obj).name);
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
/**
* An instance of enterprise managed room in a structure.
*
* @author Wouter Born - Initial contribution
*/
public class SDMRoom {
/**
* The resource name of the room.
*/
public SDMResourceName name = SDMResourceName.NAMELESS;
/**
* Room traits.
*/
public SDMTraits traits = new SDMTraits();
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
/**
* An instance of an enterprise managed structure.
*
* @author Wouter Born - Initial contribution
*/
public class SDMStructure {
/**
* The resource name of the structure.
*/
public SDMResourceName name = SDMResourceName.NAMELESS;
/**
* Structure traits.
*/
public SDMTraits traits = new SDMTraits();
}

View File

@ -0,0 +1,441 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.annotations.SerializedName;
/**
* The common SDM traits that are used in the {@link SDMDevice} and {@link SDMEvent} types.
*
* @author Wouter Born - Initial contribution
*/
public class SDMTraits {
/**
* This trait belongs to any device that supports generation of images from events.
*/
public static class SDMCameraEventImageTrait extends SDMCameraTrait {
}
/**
* This trait belongs to any device that supports taking images.
*/
public static class SDMCameraImageTrait extends SDMCameraTrait {
/**
* Maximum image resolution that is supported.
*/
public SDMResolution maxImageResolution;
}
/**
* This trait belongs to any device that supports live streaming.
*/
public static class SDMCameraLiveStreamTrait extends SDMCameraTrait {
/**
* Maximum resolution of the video live stream.
*/
public SDMResolution maxVideoResolution;
/**
* Video codecs supported for the live stream.
*/
public List<String> videoCodecs;
/**
* Audio codecs supported for the live stream.
*/
public List<String> audioCodecs;
/**
* Protocols supported for the live stream.
*/
public List<String> supportedProtocols;
}
/**
* This trait belongs to any device that supports motion detection events.
*/
public static class SDMCameraMotionTrait extends SDMCameraTrait {
}
/**
* This trait belongs to any device that supports person detection events.
*/
public static class SDMCameraPersonTrait extends SDMCameraTrait {
}
/**
* This trait belongs to any device that supports sound detection events.
*/
public static class SDMCameraSoundTrait extends SDMCameraTrait {
}
public static class SDMCameraTrait extends SDMTrait {
}
public enum SDMConnectivityStatus {
OFFLINE,
ONLINE
}
/**
* This trait belongs to any device that has connectivity information.
*/
public static class SDMConnectivityTrait extends SDMDeviceTrait {
/**
* Device connectivity status.
*/
public SDMConnectivityStatus status;
}
/**
* This trait belongs to any device for device-related information.
*/
public static class SDMDeviceInfoTrait extends SDMDeviceTrait {
/**
* Custom name of the device. Corresponds to the Label value for a device in the Nest App.
*/
public String customName;
}
/**
* This trait belongs to any device for device-related settings information.
*/
public static class SDMDeviceSettingsTrait extends SDMDeviceTrait {
/**
* Format of the degrees displayed on a Google Nest Thermostat.
*/
public SDMTemperatureScale temperatureScale;
}
public static class SDMDeviceTrait extends SDMTrait {
}
/**
* This trait belongs to any device that supports a doorbell chime and related press events.
*/
public static class SDMDoorbellChimeTrait extends SDMDoorbellTrait {
}
public static class SDMDoorbellTrait extends SDMTrait {
}
public enum SDMThermostatEcoMode {
MANUAL_ECO,
OFF
}
/**
* This trait belongs to any device that has the system ability to control the fan.
*/
public static class SDMFanTrait extends SDMDeviceTrait {
/**
* Current timer mode.
*/
public SDMFanTimerMode timerMode;
/**
* Timestamp, in RFC 3339 format, at which timer mode will turn to OFF.
*/
public ZonedDateTime timerTimeout;
}
/**
* This trait belongs to any device that has a sensor to measure humidity.
*/
public static class SDMHumidityTrait extends SDMDeviceTrait {
/**
* Percent humidity, measured at the device.
*/
public BigDecimal ambientHumidityPercent;
}
public enum SDMHvacStatus {
OFF,
HEATING,
COOLING
}
public static class SDMResolution {
/**
* Maximum image resolution width.
*/
public int width;
/**
* Maximum image resolution height.
*/
public int height;
}
/**
* This trait belongs to any room for room-related information.
*/
public static class SDMRoomInfoTrait extends SDMStructureTrait {
/**
* Custom name of the room. Corresponds to the name in the Google Home App.
*/
public String customName;
}
/**
* This trait belongs to any structure for structure-related information.
*/
public static class SDMStructureInfoTrait extends SDMStructureTrait {
/**
* Custom name of the structure. Corresponds to the name in the Google Home App.
*/
public String customName;
}
public static class SDMStructureTrait extends SDMTrait {
}
public enum SDMTemperatureScale {
CELSIUS,
FAHRENHEIT;
}
/**
* This trait belongs to any device that has a sensor to measure temperature.
*/
public static class SDMTemperatureTrait extends SDMDeviceTrait {
/**
* Temperature in degrees Celsius, measured at the device.
*/
public BigDecimal ambientTemperatureCelsius;
}
/**
* This trait belongs to device types of THERMOSTAT that support ECO modes.
*/
public static class SDMThermostatEcoTrait extends SDMThermostatTrait {
/**
* List of supported Eco modes.
*/
public List<SDMThermostatEcoMode> availableModes;
/**
* The current Eco mode of the thermostat.
*/
public SDMThermostatEcoMode mode;
/**
* Lowest temperature in Celsius at which the thermostat begins heating in Eco mode.
*/
public BigDecimal heatCelsius;
/**
* Highest temperature in Celsius at which the thermostat begins cooling in Eco mode.
*/
public BigDecimal coolCelsius;
}
/**
* This trait belongs to device types of THERMOSTAT that can report HVAC details.
*/
public static class SDMThermostatHvacTrait extends SDMThermostatTrait {
/**
* Current HVAC status of the thermostat.
*/
public SDMHvacStatus status;
}
public enum SDMThermostatMode {
HEAT,
COOL,
HEATCOOL,
OFF
}
/**
* This trait belongs to device types of THERMOSTAT that support different thermostat modes.
*/
public static class SDMThermostatModeTrait extends SDMThermostatTrait {
/**
* List of supported thermostat modes.
*/
public List<SDMThermostatMode> availableModes;
/**
* The current thermostat mode.
*/
public SDMThermostatMode mode;
}
/**
* This trait belongs to device types of THERMOSTAT that support setting target temperature and temperature range.
*/
public static class SDMThermostatTemperatureSetpointTrait extends SDMThermostatTrait {
/**
* Target temperature in Celsius for thermostat HEAT and HEATCOOL modes.
*/
public BigDecimal heatCelsius;
/**
* Target temperature in Celsius for thermostat COOL and HEATCOOL modes.
*/
public BigDecimal coolCelsius;
}
public static class SDMThermostatTrait extends SDMTrait {
}
public enum SDMFanTimerMode {
ON,
OFF
}
public static class SDMTrait {
}
@SerializedName("sdm.devices.traits.CameraEventImage")
public SDMCameraEventImageTrait cameraEventImage;
@SerializedName("sdm.devices.traits.CameraImage")
public SDMCameraImageTrait cameraImage;
@SerializedName("sdm.devices.traits.CameraLiveStream")
public SDMCameraLiveStreamTrait cameraLiveStream;
@SerializedName("sdm.devices.traits.CameraMotion")
public SDMCameraMotionTrait cameraMotion;
@SerializedName("sdm.devices.traits.CameraPerson")
public SDMCameraPersonTrait cameraPerson;
@SerializedName("sdm.devices.traits.CameraSound")
public SDMCameraSoundTrait cameraSound;
@SerializedName("sdm.devices.traits.Connectivity")
public SDMConnectivityTrait connectivity;
@SerializedName("sdm.devices.traits.DoorbellChime")
public SDMDoorbellChimeTrait doorbellChime;
@SerializedName("sdm.devices.traits.Fan")
public SDMFanTrait fan;
@SerializedName("sdm.devices.traits.Humidity")
public SDMHumidityTrait humidity;
@SerializedName("sdm.devices.traits.Info")
public SDMDeviceInfoTrait deviceInfo;
@SerializedName("sdm.devices.traits.Settings")
public SDMDeviceSettingsTrait deviceSettings;
@SerializedName("sdm.devices.traits.Temperature")
public SDMTemperatureTrait temperature;
@SerializedName("sdm.devices.traits.ThermostatEco")
public SDMThermostatEcoTrait thermostatEco;
@SerializedName("sdm.devices.traits.ThermostatHvac")
public SDMThermostatHvacTrait thermostatHvac;
@SerializedName("sdm.devices.traits.ThermostatMode")
public SDMThermostatModeTrait thermostatMode;
@SerializedName("sdm.devices.traits.ThermostatTemperatureSetpoint")
public SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint;
@SerializedName("sdm.structures.traits.Info")
public SDMStructureInfoTrait structureInfo;
@SerializedName("sdm.structures.traits.RoomInfo")
public SDMRoomInfoTrait roomInfo;
public <T> Stream<SDMTrait> traitStream() {
return Stream.of(cameraEventImage, cameraImage, cameraLiveStream, cameraMotion, cameraPerson, cameraSound,
connectivity, doorbellChime, fan, humidity, deviceInfo, deviceSettings, temperature, thermostatEco,
thermostatHvac, thermostatMode, thermostatTemperatureSetpoint, structureInfo, roomInfo)
.filter(Objects::nonNull);
}
public List<SDMTrait> traitList() {
return traitStream().collect(Collectors.toList());
}
public Set<SDMTrait> traitSet() {
return traitStream().collect(Collectors.toSet());
}
public void updateTraits(SDMTraits other) {
if (other.cameraEventImage != null) {
cameraEventImage = other.cameraEventImage;
}
if (other.cameraImage != null) {
cameraImage = other.cameraImage;
}
if (other.cameraLiveStream != null) {
cameraLiveStream = other.cameraLiveStream;
}
if (other.cameraMotion != null) {
cameraMotion = other.cameraMotion;
}
if (other.cameraPerson != null) {
cameraPerson = other.cameraPerson;
}
if (other.cameraSound != null) {
cameraSound = other.cameraSound;
}
if (other.connectivity != null) {
connectivity = other.connectivity;
}
if (other.doorbellChime != null) {
doorbellChime = other.doorbellChime;
}
if (other.fan != null) {
fan = other.fan;
}
if (other.humidity != null) {
humidity = other.humidity;
}
if (other.deviceInfo != null) {
deviceInfo = other.deviceInfo;
}
if (other.deviceSettings != null) {
deviceSettings = other.deviceSettings;
}
if (other.temperature != null) {
temperature = other.temperature;
}
if (other.thermostatEco != null) {
thermostatEco = other.thermostatEco;
}
if (other.thermostatHvac != null) {
thermostatHvac = other.thermostatHvac;
}
if (other.thermostatMode != null) {
thermostatMode = other.thermostatMode;
}
if (other.thermostatTemperatureSetpoint != null) {
thermostatTemperatureSetpoint = other.thermostatTemperatureSetpoint;
}
if (other.structureInfo != null) {
structureInfo = other.structureInfo;
}
if (other.roomInfo != null) {
roomInfo = other.roomInfo;
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An error occurred while sending data to the Pub/Sub REST API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class FailedSendingPubSubDataException extends Exception {
private static final long serialVersionUID = 8615651337708366903L;
public FailedSendingPubSubDataException(String message) {
super(message);
}
public FailedSendingPubSubDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedSendingPubSubDataException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An error occurred while sending data to the SDM REST API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class FailedSendingSDMDataException extends Exception {
private static final long serialVersionUID = 5377279669017810297L;
public FailedSendingSDMDataException(String message) {
super(message);
}
public FailedSendingSDMDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedSendingSDMDataException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The OAuth 2.0 access token used with the Pub/Sub REST API is invalid and could not be refreshed.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InvalidPubSubAccessTokenException extends Exception {
private static final long serialVersionUID = -2065751473657555846L;
public InvalidPubSubAccessTokenException(Exception cause) {
super(cause);
}
public InvalidPubSubAccessTokenException(String message, Throwable cause) {
super(message, cause);
}
public InvalidPubSubAccessTokenException(String message) {
super(message);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An authorization code is invalid and cannot be used to obtain the OAuth 2.0 tokens used with the Pub/Sub REST API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InvalidPubSubAuthorizationCodeException extends Exception {
private static final long serialVersionUID = 8422005071870179414L;
public InvalidPubSubAuthorizationCodeException(Exception cause) {
super(cause);
}
public InvalidPubSubAuthorizationCodeException(String message, Throwable cause) {
super(message, cause);
}
public InvalidPubSubAuthorizationCodeException(String message) {
super(message);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The OAuth 2.0 access token used with the SDM REST API is invalid and could not be refreshed.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InvalidSDMAccessTokenException extends Exception {
private static final long serialVersionUID = 6149230876422099759L;
public InvalidSDMAccessTokenException(Exception cause) {
super(cause);
}
public InvalidSDMAccessTokenException(String message, Throwable cause) {
super(message, cause);
}
public InvalidSDMAccessTokenException(String message) {
super(message);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An authorization code is invalid and cannot be used to obtain the OAuth 2.0 tokens used with the SDM API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InvalidSDMAuthorizationCodeException extends Exception {
private static final long serialVersionUID = -8900246112957957403L;
public InvalidSDMAuthorizationCodeException(Exception cause) {
super(cause);
}
public InvalidSDMAuthorizationCodeException(String message, Throwable cause) {
super(message, cause);
}
public InvalidSDMAuthorizationCodeException(String message) {
super(message);
}
}

View File

@ -0,0 +1,333 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.handler;
import static java.util.function.Predicate.not;
import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.sdm.api.PubSubAPI;
import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
import org.openhab.binding.nest.internal.sdm.config.SDMAccountConfiguration;
import org.openhab.binding.nest.internal.sdm.discovery.SDMDiscoveryService;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingPubSubDataException;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAccessTokenException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAuthorizationCodeException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAuthorizationCodeException;
import org.openhab.binding.nest.internal.sdm.listener.PubSubSubscriptionListener;
import org.openhab.binding.nest.internal.sdm.listener.SDMAPIRequestListener;
import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMAccountHandler} provides the {@link SDMAPI} instance used by the device handlers.
* The {@link SDMAPI} is used by device handlers for periodically refreshing device data and sending device commands.
* When Pub/Sub is properly configured, the account handler also sends received {@link SDMEvent}s from the
* {@link PubSubAPI} to the subscribed {@link SDMEventListener}s.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMAccountHandler extends BaseBridgeHandler {
private static final String PUBSUB_TOPIC_NAME_PREFIX = "projects/sdm-prod/topics/enterprise-";
private final Logger logger = LoggerFactory.getLogger(SDMAccountHandler.class);
private HttpClientFactory httpClientFactory;
private OAuthFactory oAuthFactory;
private @NonNullByDefault({}) SDMAccountConfiguration config;
private @Nullable Future<?> initializeFuture;
private @Nullable PubSubAPI pubSubAPI;
private @Nullable Exception pubSubException;
private @Nullable SDMAPI sdmAPI;
private @Nullable Exception sdmException;
private @Nullable Future<?> sdmCheckFuture;
private final Duration sdmCheckDelay = Duration.ofMinutes(1);
private final Map<String, SDMEventListener> listeners = new ConcurrentHashMap<>();
private final SDMAPIRequestListener requestListener = new SDMAPIRequestListener() {
@Override
public void onError(Exception exception) {
sdmException = exception;
logger.debug("SDM exception occurred");
updateThingStatus();
Future<?> future = sdmCheckFuture;
if (future == null || future.isDone()) {
sdmCheckFuture = scheduler.scheduleWithFixedDelay(() -> {
SDMAPI localSDMAPI = sdmAPI;
if (localSDMAPI != null) {
try {
logger.debug("Checking SDM API");
localSDMAPI.listDevices();
} catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
logger.debug("SDM API check failed");
}
}
}, sdmCheckDelay.toNanos(), sdmCheckDelay.toNanos(), TimeUnit.NANOSECONDS);
logger.debug("Scheduled SDM API check job");
}
}
@Override
public void onSuccess() {
if (sdmException != null) {
sdmException = null;
logger.debug("SDM exception cleared");
updateThingStatus();
}
Future<?> future = sdmCheckFuture;
if (future != null) {
future.cancel(true);
sdmCheckFuture = null;
logger.debug("Cancelled SDM API check job");
}
}
};
private final PubSubSubscriptionListener subscriptionListener = new PubSubSubscriptionListener() {
@Override
public void onError(Exception exception) {
pubSubException = exception;
logger.debug("Pub/Sub exception occurred");
updateThingStatus();
}
@Override
public void onMessage(PubSubMessage message) {
if (pubSubException != null) {
pubSubException = null;
logger.debug("Pub/Sub exception cleared");
updateThingStatus();
}
handlePubSubMessage(message);
}
@Override
public void onNoNewMessages() {
if (pubSubException != null) {
pubSubException = null;
logger.debug("Pub/Sub exception cleared");
updateThingStatus();
}
}
};
public SDMAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory) {
super(bridge);
this.httpClientFactory = httpClientFactory;
this.oAuthFactory = oAuthFactory;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
config = getConfigAs(SDMAccountConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
initializeFuture = scheduler.submit(() -> {
sdmAPI = initializeSDMAPI();
if (config.usePubSub()) {
pubSubAPI = initializePubSubAPI();
}
updateThingStatus();
});
}
private @Nullable SDMAPI initializeSDMAPI() {
SDMAPI sdmAPI = new SDMAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(),
config.sdmProjectId, config.sdmClientId, config.sdmClientSecret);
sdmException = null;
try {
if (!config.sdmAuthorizationCode.isBlank()) {
sdmAPI.authorizeClient(config.sdmAuthorizationCode);
Configuration configuration = editConfiguration();
configuration.put(SDMAccountConfiguration.SDM_AUTHORIZATION_CODE, "");
updateConfiguration(configuration);
}
sdmAPI.checkAccessTokenValidity();
sdmAPI.addRequestListener(requestListener);
return sdmAPI;
} catch (InvalidSDMAccessTokenException | InvalidSDMAuthorizationCodeException | IOException e) {
sdmException = e;
return null;
}
}
private @Nullable PubSubAPI initializePubSubAPI() {
PubSubAPI pubSubAPI = new PubSubAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(),
config.pubsubProjectId, config.pubsubClientId, config.pubsubClientSecret);
pubSubException = null;
try {
if (!config.pubsubAuthorizationCode.isBlank()) {
pubSubAPI.authorizeClient(config.pubsubAuthorizationCode);
Configuration configuration = editConfiguration();
configuration.put(SDMAccountConfiguration.PUBSUB_AUTHORIZATION_CODE, "");
updateConfiguration(configuration);
}
pubSubAPI.checkAccessTokenValidity();
pubSubAPI.createSubscription(config.pubsubSubscriptionId, PUBSUB_TOPIC_NAME_PREFIX + config.sdmProjectId);
pubSubAPI.addSubscriptionListener(config.pubsubSubscriptionId, subscriptionListener);
return pubSubAPI;
} catch (FailedSendingPubSubDataException | InvalidPubSubAccessTokenException
| InvalidPubSubAuthorizationCodeException | IOException e) {
pubSubException = e;
return null;
}
}
@Override
public void dispose() {
Future<?> localFuture = initializeFuture;
if (localFuture != null) {
localFuture.cancel(true);
initializeFuture = null;
}
localFuture = sdmCheckFuture;
if (localFuture != null) {
localFuture.cancel(true);
sdmCheckFuture = null;
}
PubSubAPI localPubSubAPI = pubSubAPI;
if (localPubSubAPI != null) {
localPubSubAPI.dispose();
pubSubAPI = null;
}
SDMAPI localSDMAPI = sdmAPI;
if (localSDMAPI != null) {
localSDMAPI.dispose();
sdmAPI = null;
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(SDMDiscoveryService.class);
}
public void addThingDataListener(String deviceId, SDMEventListener listener) {
listeners.put(deviceId, listener);
}
public void removeThingDataListener(String deviceId, SDMEventListener listener) {
listeners.remove(deviceId, listener);
}
public @Nullable SDMAPI getAPI() {
return sdmAPI;
}
private void handlePubSubMessage(PubSubMessage message) {
String messageId = message.messageId;
String json = new String(Base64.getDecoder().decode(message.data), StandardCharsets.UTF_8);
logger.debug("Handling messageId={} with content:", messageId);
logger.debug("{}", json);
SDMEvent event = GSON.fromJson(json, SDMEvent.class);
if (event == null) {
logger.debug("Ignoring messageId={} (empty)", messageId);
return;
}
SDMResourceUpdate resourceUpdate = event.resourceUpdate;
if (resourceUpdate == null) {
logger.debug("Ignoring messageId={} (no resource update)", messageId);
return;
}
String deviceId = resourceUpdate.name.deviceId;
SDMEventListener listener = listeners.get(deviceId);
if (listener != null) {
logger.debug("Sending messageId={} to listener with deviceId={}", messageId, deviceId);
listener.onEvent(event);
} else {
logger.debug("No listener for messageId={} with deviceId={}", messageId, deviceId);
}
}
private void updateThingStatus() {
Exception e = sdmException != null ? sdmException : pubSubException;
if (e != null) {
if (e instanceof InvalidSDMAccessTokenException || e instanceof InvalidSDMAuthorizationCodeException
|| e instanceof InvalidPubSubAccessTokenException
|| e instanceof InvalidPubSubAuthorizationCodeException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} else {
Throwable cause = e.getCause();
String description = Stream
.of(Objects.requireNonNullElse(e.getMessage(), ""),
cause == null ? "" : Objects.requireNonNullElse(cause.getMessage(), ""))
.filter(not(String::isBlank)) //
.collect(Collectors.joining(": "));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
} else {
String description = config.usePubSub() ? "Using periodic refresh and Pub/Sub" : "Using periodic refresh";
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, description);
}
}
}

View File

@ -0,0 +1,319 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.handler;
import static org.openhab.core.thing.ThingStatus.*;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.sdm.SDMBindingConstants;
import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
import org.openhab.binding.nest.internal.sdm.config.SDMDeviceConfiguration;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMDevice;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
import org.openhab.binding.nest.internal.sdm.dto.SDMIdentifiable;
import org.openhab.binding.nest.internal.sdm.dto.SDMParentRelation;
import org.openhab.binding.nest.internal.sdm.dto.SDMResourceName.SDMResourceNameType;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraImageTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraLiveStreamTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityStatus;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceInfoTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMResolution;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMBaseHandler} provides the common functionality of all SDM device thing handlers.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class SDMBaseHandler extends BaseThingHandler implements SDMIdentifiable, SDMEventListener {
private final Logger logger = LoggerFactory.getLogger(SDMBaseHandler.class);
protected @NonNullByDefault({}) SDMDeviceConfiguration config;
protected SDMDevice device = new SDMDevice();
protected String deviceId = "";
protected @Nullable ZonedDateTime lastRefreshDateTime;
protected @Nullable ScheduledFuture<?> refreshJob;
protected final TimeZoneProvider timeZoneProvider;
public SDMBaseHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing);
this.timeZoneProvider = timeZoneProvider;
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
updateBridgeStatus();
}
/**
* Updates the thing state based on that of the bridge.
*/
protected void updateBridgeStatus() {
Bridge bridge = getBridge();
ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
if (bridge == null) {
disableRefresh();
updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridgeStatus == ONLINE && thing.getStatus() != ONLINE) {
enableRefresh();
} else if (bridgeStatus == OFFLINE) {
disableRefresh();
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (bridgeStatus == UNKNOWN) {
disableRefresh();
updateStatus(UNKNOWN);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
delayedRefresh();
}
}
@Override
public void initialize() {
logger.debug("Initializing handler for {}", thing.getUID());
config = getConfigAs(SDMDeviceConfiguration.class);
deviceId = config.deviceId;
updateStatus(ThingStatus.UNKNOWN);
updateBridgeStatus();
}
@Override
public void dispose() {
disableRefresh();
}
@Override
public String getId() {
return deviceId;
}
protected @Nullable SDMAccountHandler getAccountHandler() {
Bridge bridge = getBridge();
return bridge != null ? (SDMAccountHandler) bridge.getHandler() : null;
}
protected @Nullable SDMAPI getAPI() {
SDMAccountHandler accountHandler = getAccountHandler();
return accountHandler != null ? accountHandler.getAPI() : null;
}
protected @Nullable SDMDevice getDeviceInfo() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
SDMAPI api = getAPI();
return api == null ? null : api.getDevice(deviceId);
}
protected <T extends SDMCommandResponse> @Nullable T executeDeviceCommand(SDMCommandRequest<T> request)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
SDMAPI api = getAPI();
return api == null ? null : api.executeDeviceCommand(deviceId, request);
}
protected @Nullable SDMTraits getTraitsForUpdate(SDMEvent event) {
SDMResourceUpdate resourceUpdate = event.resourceUpdate;
if (resourceUpdate == null) {
return null;
}
SDMTraits traits = resourceUpdate.traits;
if (traits == null) {
return null;
}
ZonedDateTime localRefreshDateTime = lastRefreshDateTime;
if (localRefreshDateTime == null || event.timestamp.isBefore(localRefreshDateTime)) {
return null;
}
return traits;
}
@Override
public void onEvent(SDMEvent event) {
SDMTraits traits = getTraitsForUpdate(event);
if (traits != null) {
logger.debug("Updating traits using resource update traits in event");
device.traits.updateTraits(traits);
}
}
protected void refreshDevice() {
try {
SDMDevice localDevice = getDeviceInfo();
if (localDevice == null) {
logger.debug("Cannot refresh device (empty response or handler has no bridge)");
return;
}
this.device = localDevice;
this.lastRefreshDateTime = ZonedDateTime.now();
Map<String, String> properties = editProperties();
properties.putAll(getDeviceProperties(localDevice));
updateProperties(properties);
updateStateWithTraits(localDevice.traits);
} catch (InvalidSDMAccessTokenException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (FailedSendingSDMDataException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
protected void updateStateWithTraits(SDMTraits traits) {
SDMConnectivityTrait connectivity = traits.connectivity;
if (connectivity == null && device.traits.connectivity != null) {
logger.debug("Skipping partial update for device with connectivity trait");
return;
}
ThingStatus thingStatus = connectivity == null || connectivity.status == null
|| connectivity.status == SDMConnectivityStatus.ONLINE ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
if (thing.getStatus() != thingStatus) {
updateStatus(thingStatus);
}
}
protected void enableRefresh() {
scheduleRefreshJob();
SDMAccountHandler handler = getAccountHandler();
if (handler != null) {
handler.addThingDataListener(getId(), this);
}
}
protected void disableRefresh() {
cancelRefreshJob();
SDMAccountHandler handler = getAccountHandler();
if (handler != null) {
handler.removeThingDataListener(getId(), this);
}
}
protected void cancelRefreshJob() {
ScheduledFuture<?> localRefreshJob = refreshJob;
if (localRefreshJob != null && !localRefreshJob.isCancelled()) {
localRefreshJob.cancel(true);
}
}
protected void scheduleRefreshJob() {
ScheduledFuture<?> localRefreshJob = refreshJob;
if (localRefreshJob == null || localRefreshJob.isCancelled()) {
refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 0, config.refreshInterval,
TimeUnit.SECONDS);
}
}
protected void delayedRefresh() {
cancelRefreshJob();
refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 3, config.refreshInterval, TimeUnit.SECONDS);
}
public static Map<String, String> getDeviceProperties(SDMDevice device) {
Map<String, String> properties = new HashMap<>();
SDMTraits traits = device.traits;
SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
if (deviceInfo != null && !deviceInfo.customName.isBlank()) {
properties.put(SDMBindingConstants.PROPERTY_CUSTOM_NAME, deviceInfo.customName);
}
List<SDMParentRelation> parentRelations = device.parentRelations;
for (SDMParentRelation parentRelation : parentRelations) {
if (parentRelation.parent.type == SDMResourceNameType.ROOM && !parentRelation.displayName.isBlank()) {
properties.put(SDMBindingConstants.PROPERTY_ROOM, parentRelation.displayName);
break;
}
}
SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings;
if (deviceSettings != null) {
properties.put(SDMBindingConstants.PROPERTY_TEMPERATURE_SCALE, deviceSettings.temperatureScale.name());
}
SDMCameraImageTrait cameraImage = traits.cameraImage;
if (cameraImage != null) {
SDMResolution resolution = cameraImage.maxImageResolution;
properties.put(SDMBindingConstants.PROPERTY_MAX_IMAGE_RESOLUTION,
String.format("%sx%s", resolution.width, resolution.height));
}
SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream;
if (cameraLiveStream != null) {
List<String> audioCodecs = cameraLiveStream.audioCodecs;
if (audioCodecs != null) {
properties.put(SDMBindingConstants.PROPERTY_AUDIO_CODECS,
audioCodecs.stream().collect(Collectors.joining(", ")));
}
SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution;
if (maxVideoResolution != null) {
SDMResolution resolution = maxVideoResolution;
properties.put(SDMBindingConstants.PROPERTY_MAX_VIDEO_RESOLUTION,
String.format("%sx%s", resolution.width, resolution.height));
}
List<String> supportedProtocols = cameraLiveStream.supportedProtocols;
if (supportedProtocols != null) {
properties.put(SDMBindingConstants.PROPERTY_SUPPORTED_PROTOCOLS,
supportedProtocols.stream().collect(Collectors.joining(", ")));
}
List<String> videoCodecs = cameraLiveStream.videoCodecs;
if (videoCodecs != null) {
properties.put(SDMBindingConstants.PROPERTY_VIDEO_CODECS,
videoCodecs.stream().collect(Collectors.joining(", ")));
}
}
return properties;
}
}

View File

@ -0,0 +1,204 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.handler;
import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*;
import static org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest.EVENT_IMAGE_VALIDITY;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.sdm.SDMBindingConstants;
import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResults;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResults;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMDeviceEvent;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdateEvents;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMCameraHandler} handles state updates of SDM devices with a camera.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMCameraHandler extends SDMBaseHandler {
private final Logger logger = LoggerFactory.getLogger(SDMCameraHandler.class);
private @Nullable ZonedDateTime lastChimeEventTimestamp;
private @Nullable ZonedDateTime lastMotionEventTimestamp;
private @Nullable ZonedDateTime lastPersonEventTimestamp;
private @Nullable ZonedDateTime lastSoundEventTimestamp;
public SDMCameraHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
private void updateLiveStreamChannels() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
boolean channelLinked = Stream.of(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP,
CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, CHANNEL_LIVE_STREAM_URL).anyMatch(this::isLinked);
if (!channelLinked) {
logger.debug("Not updating live stream channels (channels are not linked)");
return;
}
logger.debug("Updating live stream channels");
SDMGenerateCameraRtspStreamResponse response = executeDeviceCommand(new SDMGenerateCameraRtspStreamRequest());
if (response == null) {
logger.debug("Cannot update live stream channels (empty response)");
return;
}
SDMGenerateCameraRtspStreamResults results = response.results;
if (results != null) {
updateState(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, new StringType(results.streamToken));
updateState(CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP,
new DateTimeType(results.expiresAt.withZoneSameInstant(timeZoneProvider.getTimeZone())));
updateState(CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, new StringType(results.streamExtensionToken));
updateState(CHANNEL_LIVE_STREAM_URL, new StringType(results.streamUrls.rtspUrl));
}
}
@Override
public void onEvent(SDMEvent event) {
super.onEvent(event);
SDMResourceUpdate resourceUpdate = event.resourceUpdate;
if (resourceUpdate == null) {
logger.debug("Skipping event without resource update");
return;
}
SDMResourceUpdateEvents events = resourceUpdate.events;
if (events == null) {
logger.debug("Skipping resource update without events");
return;
}
try {
SDMDeviceEvent deviceEvent = events.cameraMotionEvent;
if (deviceEvent != null) {
lastMotionEventTimestamp = updateImageChannelsForEvent(CHANNEL_MOTION_EVENT_TIMESTAMP,
CHANNEL_MOTION_EVENT_IMAGE, lastMotionEventTimestamp, event.timestamp, deviceEvent);
}
deviceEvent = events.cameraPersonEvent;
if (deviceEvent != null) {
lastPersonEventTimestamp = updateImageChannelsForEvent(CHANNEL_PERSON_EVENT_TIMESTAMP,
CHANNEL_PERSON_EVENT_IMAGE, lastPersonEventTimestamp, event.timestamp, deviceEvent);
}
deviceEvent = events.cameraSoundEvent;
if (deviceEvent != null) {
lastSoundEventTimestamp = updateImageChannelsForEvent(CHANNEL_SOUND_EVENT_TIMESTAMP,
CHANNEL_SOUND_EVENT_IMAGE, lastSoundEventTimestamp, event.timestamp, deviceEvent);
}
deviceEvent = events.doorbellChimeEvent;
if (deviceEvent != null) {
lastChimeEventTimestamp = updateImageChannelsForEvent(CHANNEL_CHIME_EVENT_TIMESTAMP,
CHANNEL_CHIME_EVENT_IMAGE, lastChimeEventTimestamp, event.timestamp, deviceEvent);
}
} catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
logger.warn("Handling SDM event failed for {}", thing.getUID(), e);
}
}
private @Nullable ZonedDateTime updateImageChannelsForEvent(String timeChannelName, String imageChannelName,
@Nullable ZonedDateTime lastEventTimestamp, ZonedDateTime eventTimestamp, SDMDeviceEvent event)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
boolean newerEvent = lastEventTimestamp == null || lastEventTimestamp.isBefore(eventTimestamp);
if (!newerEvent) {
logger.debug("Skipping {} channel update (more recent event already occurred)", imageChannelName);
return lastEventTimestamp;
}
if (!isLinked(imageChannelName)) {
logger.debug("Not downloading image for {} channel update (channel is not linked)", imageChannelName);
} else if (Duration.between(eventTimestamp, ZonedDateTime.now()).compareTo(EVENT_IMAGE_VALIDITY) > 0) {
logger.debug("Cannot download image for {} channel update (event image has expired)", imageChannelName);
updateState(timeChannelName, UnDefType.NULL);
} else {
BigDecimal imageWidth = null;
BigDecimal imageHeight = null;
Channel channel = getThing().getChannel(imageChannelName);
if (channel != null) {
Configuration configuration = channel.getConfiguration();
imageWidth = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_WIDTH);
imageHeight = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_HEIGHT);
}
updateState(imageChannelName, getCameraImage(event.eventId, imageWidth, imageHeight));
}
updateState(timeChannelName,
new DateTimeType(eventTimestamp.withZoneSameInstant(timeZoneProvider.getTimeZone())));
logger.debug("Updated {} channel and {} with image of event at {}", imageChannelName, timeChannelName,
eventTimestamp);
updateLiveStreamChannels();
return eventTimestamp;
}
private State getCameraImage(String eventId, @Nullable BigDecimal imageWidth, @Nullable BigDecimal imageHeight)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
SDMGenerateCameraImageResponse response = executeDeviceCommand(new SDMGenerateCameraImageRequest(eventId));
if (response == null) {
logger.debug("Cannot get image for camera event (empty response)");
return UnDefType.NULL;
}
SDMGenerateCameraImageResults results = response.results;
if (results == null) {
logger.debug("Cannot get image for camera event (no results)");
return UnDefType.NULL;
}
SDMAPI api = getAPI();
if (api == null) {
logger.debug("Cannot get image for camera event (handler has no bridge)");
return UnDefType.NULL;
}
byte[] imageBytes = api.getCameraImage(results.url, results.token, imageWidth, imageHeight);
return new RawType(imageBytes, "image/jpeg");
}
}

View File

@ -0,0 +1,357 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.handler;
import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*;
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.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
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.nest.internal.sdm.SDMBindingConstants;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetFanTimerRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatCoolSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatEcoModeRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatHeatSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatModeRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatRangeSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHumidityTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatHvacTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatModeTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatTemperatureSetpointTrait;
import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
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.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SDMThermostatHandler} handles state updates and commands for SDM thermostat devices.
*
* @author Brian Higginbotham - Initial contribution
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMThermostatHandler extends SDMBaseHandler {
private final Logger logger = LoggerFactory.getLogger(SDMThermostatHandler.class);
public SDMThermostatHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@SuppressWarnings("unchecked")
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
try {
if (command instanceof RefreshType) {
delayedRefresh();
} else if (CHANNEL_CURRENT_ECO_MODE.equals(channelUID.getId())) {
if (command instanceof StringType) {
SDMThermostatEcoMode mode = SDMThermostatEcoMode.valueOf(command.toString());
executeDeviceCommand(new SDMSetThermostatEcoModeRequest(mode));
delayedRefresh();
}
} else if (CHANNEL_CURRENT_MODE.equals(channelUID.getId())) {
if (command instanceof StringType) {
SDMThermostatMode mode = SDMThermostatMode.valueOf(command.toString());
executeDeviceCommand(new SDMSetThermostatModeRequest(mode));
delayedRefresh();
}
} else if (CHANNEL_FAN_TIMER_MODE.equals(channelUID.getId())) {
if (command instanceof OnOffType) {
if ((OnOffType) command == OnOffType.ON) {
executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, getFanTimerDuration()));
} else {
executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.OFF));
}
delayedRefresh();
}
} else if (CHANNEL_FAN_TIMER_TIMEOUT.equals(channelUID.getId())) {
if (command instanceof DateTimeType) {
Duration duration = Duration.between(ZonedDateTime.now(),
((DateTimeType) command).getZonedDateTime());
executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, duration));
delayedRefresh();
}
} else if (CHANNEL_MAXIMUM_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
BigDecimal minTemperature = getMinTemperature();
if (minTemperature != null) {
setTargetTemperature(new QuantityType<>(minTemperature, CELSIUS),
(QuantityType<Temperature>) command);
delayedRefresh();
}
}
} else if (CHANNEL_MINIMUM_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
BigDecimal maxTemperature = getMaxTemperature();
if (maxTemperature != null) {
setTargetTemperature((QuantityType<Temperature>) command,
new QuantityType<>(maxTemperature, CELSIUS));
delayedRefresh();
}
}
} else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof QuantityType) {
setTargetTemperature((QuantityType<Temperature>) command);
delayedRefresh();
}
}
} catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
logger.debug("Exception while handling {} command for {}: {}", command, thing.getUID(), e.getMessage());
}
}
@Override
protected void updateStateWithTraits(SDMTraits traits) {
logger.debug("Refreshing channels for: {}", thing.getUID());
super.updateStateWithTraits(traits);
SDMHumidityTrait humidity = traits.humidity;
if (humidity != null) {
updateState(CHANNEL_AMBIENT_HUMIDITY, new QuantityType<>(humidity.ambientHumidityPercent, PERCENT));
}
SDMTemperatureTrait temperature = traits.temperature;
if (temperature != null) {
updateState(CHANNEL_AMBIENT_TEMPERATURE, temperatureToState(temperature.ambientTemperatureCelsius));
}
SDMThermostatModeTrait thermostatMode = traits.thermostatMode;
if (thermostatMode != null) {
updateState(CHANNEL_CURRENT_MODE, new StringType(thermostatMode.mode.name()));
}
SDMThermostatEcoTrait thermostatEco = traits.thermostatEco;
if (thermostatEco != null) {
updateState(CHANNEL_CURRENT_ECO_MODE, new StringType(thermostatEco.mode.name()));
}
SDMFanTrait fan = traits.fan;
if (fan != null) {
updateState(CHANNEL_FAN_TIMER_MODE, fan.timerMode == SDMFanTimerMode.ON ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_FAN_TIMER_TIMEOUT, fan.timerTimeout == null ? UnDefType.NULL
: new DateTimeType(fan.timerTimeout.withZoneSameInstant(timeZoneProvider.getTimeZone())));
}
SDMThermostatHvacTrait thermostatHvac = traits.thermostatHvac;
if (thermostatHvac != null) {
updateState(CHANNEL_HVAC_STATUS, new StringType(thermostatHvac.status.name()));
}
BigDecimal maxTemperature = getMaxTemperature();
if (maxTemperature != null) {
updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(getMaxTemperature()));
}
BigDecimal minTemperature = getMinTemperature();
if (minTemperature != null) {
updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(minTemperature));
}
BigDecimal targetTemperature = getTargetTemperature();
if (targetTemperature != null) {
updateState(CHANNEL_TARGET_TEMPERATURE, temperatureToState(targetTemperature));
}
}
private Duration getFanTimerDuration() {
long seconds = 900;
Channel channel = getThing().getChannel(SDMBindingConstants.CHANNEL_FAN_TIMER_MODE);
if (channel != null) {
Configuration configuration = channel.getConfiguration();
Object fanTimerDuration = configuration.get(SDMBindingConstants.CONFIG_PROPERTY_FAN_TIMER_DURATION);
if (fanTimerDuration instanceof BigDecimal) {
seconds = ((BigDecimal) fanTimerDuration).longValue();
}
}
return Duration.ofSeconds(seconds);
}
private @Nullable BigDecimal getMinTemperature() {
SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco;
if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) {
return thermostatEco.heatCelsius;
}
SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint;
SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode;
if (thermostatMode != null && thermostatMode.mode == SDMThermostatMode.HEATCOOL) {
return thermostatTemperatureSetpoint.heatCelsius;
}
return null;
}
private @Nullable BigDecimal getMaxTemperature() {
SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco;
if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) {
return thermostatEco.coolCelsius;
}
SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint;
SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode;
if (thermostatMode != null && thermostatMode.mode == SDMThermostatMode.HEATCOOL) {
return thermostatTemperatureSetpoint.coolCelsius;
}
return null;
}
private @Nullable BigDecimal getTargetTemperature() {
SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco;
if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) {
return null;
}
SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint;
SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode;
if (thermostatMode != null) {
if (thermostatMode.mode == SDMThermostatMode.COOL) {
return thermostatTemperatureSetpoint.coolCelsius;
}
if (thermostatMode.mode == SDMThermostatMode.HEAT) {
return thermostatTemperatureSetpoint.heatCelsius;
}
}
return null;
}
@Override
public void onEvent(SDMEvent event) {
super.onEvent(event);
SDMTraits traits = getTraitsForUpdate(event);
if (traits == null) {
return;
}
updateStateWithTraits(traits);
SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = traits.thermostatTemperatureSetpoint;
if (thermostatTemperatureSetpoint != null) {
BigDecimal coolCelsius = thermostatTemperatureSetpoint.coolCelsius;
BigDecimal heatCelsius = thermostatTemperatureSetpoint.heatCelsius;
if (coolCelsius != null && heatCelsius != null) {
updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(heatCelsius));
updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(coolCelsius));
}
}
SDMThermostatEcoTrait thermostatEco = traits.thermostatEco;
if (thermostatEco != null) {
if (thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) {
updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(thermostatEco.heatCelsius));
updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(thermostatEco.coolCelsius));
}
}
}
private void setTargetTemperature(QuantityType<Temperature> value)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("setThermostatTargetTemperature value={}", value);
SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode;
if (thermostatMode.mode == SDMThermostatMode.COOL) {
executeDeviceCommand(new SDMSetThermostatCoolSetpointRequest(toCelsiusBigDecimal(value)));
} else if (thermostatMode.mode == SDMThermostatMode.HEAT) {
executeDeviceCommand(new SDMSetThermostatHeatSetpointRequest(toCelsiusBigDecimal(value)));
} else {
throw new IllegalStateException("INVALID use case for setThermostatTargetTemperature");
}
}
private void setTargetTemperature(QuantityType<Temperature> minValue, QuantityType<Temperature> maxValue)
throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
logger.debug("setThermostatTargetTemperature minValue={} maxValue={}", minValue, maxValue);
SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode;
if (thermostatMode.mode == SDMThermostatMode.HEATCOOL) {
executeDeviceCommand(new SDMSetThermostatRangeSetpointRequest(toCelsiusBigDecimal(minValue),
toCelsiusBigDecimal(maxValue)));
} else {
throw new IllegalStateException("INVALID use case for setThermostatTargetTemperature");
}
}
protected State temperatureToState(@Nullable BigDecimal value) {
if (value == null) {
return UnDefType.NULL;
}
QuantityType<Temperature> temperature = new QuantityType<>(value, CELSIUS);
if (getDeviceTemperatureUnit() == FAHRENHEIT) {
QuantityType<Temperature> converted = temperature.toUnit(FAHRENHEIT);
return converted == null ? UnDefType.NULL : converted;
}
return temperature;
}
private Unit<Temperature> getDeviceTemperatureUnit() {
SDMDeviceSettingsTrait deviceSettings = device.traits.deviceSettings;
if (deviceSettings == null) {
return CELSIUS;
}
switch (deviceSettings.temperatureScale) {
case CELSIUS:
return CELSIUS;
case FAHRENHEIT:
return FAHRENHEIT;
default:
return CELSIUS;
}
}
private BigDecimal toCelsiusBigDecimal(QuantityType<Temperature> temperature) {
QuantityType<Temperature> celsiusTemperature = temperature.toUnit(CELSIUS);
if (celsiusTemperature == null) {
throw new IllegalArgumentException(
String.format("Temperature '%s' cannot be converted to Celsius unit", temperature));
}
return celsiusTemperature.toBigDecimal();
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.sdm.api.PubSubAPI;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage;
/**
* Interface for listeners of {@link PubSubAPI} subscription events.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface PubSubSubscriptionListener {
void onError(Exception exception);
void onMessage(PubSubMessage message);
void onNoNewMessages();
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
/**
* Interface for listeners that want to monitor if {@link SDMAPI} requests error or succeed.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface SDMAPIRequestListener {
void onError(Exception exception);
void onSuccess();
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
/**
* Interface for {@link SDMEvent} listeners.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface SDMEventListener {
void onEvent(SDMEvent event);
}

View File

@ -10,21 +10,22 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal;
package org.openhab.binding.nest.internal.wwn;
import java.time.Duration;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NestBindingConstants} class defines common constants, which are
* used across the whole binding.
* The {@link WWNBindingConstants} class defines common constants which are used for the WWN implementation in the
* binding.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
public class NestBindingConstants {
public class WWNBindingConstants {
public static final String BINDING_ID = "nest";
@ -56,11 +57,14 @@ public class NestBindingConstants {
public static final int MIN_SECONDS_BETWEEN_API_CALLS = 60;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "camera");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke_detector");
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_STRUCTURE = new ThingTypeUID(BINDING_ID, "structure");
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "wwn_account");
public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "wwn_camera");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "wwn_smoke_detector");
public static final ThingTypeUID THING_TYPE_STRUCTURE = new ThingTypeUID(BINDING_ID, "wwn_structure");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wwn_thermostat");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_CAMERA,
THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE, THING_TYPE_THERMOSTAT);
// List of all channel group prefixes
public static final String CHANNEL_GROUP_CAMERA_PREFIX = "camera#";

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.wwn;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler;
import org.openhab.binding.nest.internal.wwn.handler.WWNCameraHandler;
import org.openhab.binding.nest.internal.wwn.handler.WWNSmokeDetectorHandler;
import org.openhab.binding.nest.internal.wwn.handler.WWNStructureHandler;
import org.openhab.binding.nest.internal.wwn.handler.WWNThermostatHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link WWNThingHandlerFactory} is responsible for creating WWN thing handlers.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest")
public class WWNThingHandlerFactory extends BaseThingHandlerFactory {
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
@Activate
public WWNThingHandlerFactory(@Reference ClientBuilder clientBuilder,
@Reference SseEventSourceFactory eventSourceFactory) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
}
/**
* The things this factory supports creating.
*/
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Creates a handler for the specific thing. This also creates the discovery service when the bridge is created.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
return new WWNAccountHandler((Bridge) thing, clientBuilder, eventSourceFactory);
} else if (THING_TYPE_CAMERA.equals(thingTypeUID)) {
return new WWNCameraHandler(thing);
} else if (THING_TYPE_SMOKE_DETECTOR.equals(thingTypeUID)) {
return new WWNSmokeDetectorHandler(thing);
} else if (THING_TYPE_STRUCTURE.equals(thingTypeUID)) {
return new WWNStructureHandler(thing);
} else if (THING_TYPE_THERMOSTAT.equals(thingTypeUID)) {
return new WWNThermostatHandler(thing);
}
return null;
}
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal;
package org.openhab.binding.nest.internal.wwn;
import java.io.Reader;
@ -21,17 +21,17 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Utility class for sharing utility methods between objects.
* Utility class for sharing WWN utility methods between objects.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public final class NestUtils {
public final class WWNUtils {
private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private NestUtils() {
private WWNUtils() {
// hidden utility class constructor
}

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.config;
package org.openhab.binding.nest.internal.wwn.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The configuration for the Nest bridge, allowing it to talk to Nest.
* The configuration for the WWN account, allowing it to talk to Nest.
*
* @author David Bennett - Initial contribution
*/
@NonNullByDefault
public class NestBridgeConfiguration {
public class WWNAccountConfiguration {
public static final String PRODUCT_ID = "productId";
/** Product ID from the Nest product page. */
public String productId = "";

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.config;
package org.openhab.binding.nest.internal.wwn.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration for Nest devices.
* The configuration for WWN devices.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Add device configuration to allow file based configuration
*/
@NonNullByDefault
public class NestDeviceConfiguration {
public class WWNDeviceConfiguration {
public static final String DEVICE_ID = "deviceId";
/** Device ID which can be retrieved with the Nest API. */
public String deviceId = "";

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.config;
package org.openhab.binding.nest.internal.wwn.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration for structures.
* The configuration for WWN structures.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Add device configuration to allow file based configuration
*/
@NonNullByDefault
public class NestStructureConfiguration {
public class WWNStructureConfiguration {
public static final String STRUCTURE_ID = "structureId";
/** Structure ID which can be retrieved with the Nest API. */
public String structureId = "";

View File

@ -0,0 +1,176 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.wwn.discovery;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration;
import org.openhab.binding.nest.internal.wwn.config.WWNStructureConfiguration;
import org.openhab.binding.nest.internal.wwn.dto.BaseWWNDevice;
import org.openhab.binding.nest.internal.wwn.dto.WWNCamera;
import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector;
import org.openhab.binding.nest.internal.wwn.dto.WWNStructure;
import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat;
import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler;
import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service connects to the Nest account and creates the correct discovery results for devices
* as they are found through the WWN API.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add representation properties
*/
@NonNullByDefault
public class WWNDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_CAMERA, THING_TYPE_THERMOSTAT,
THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE);
private final Logger logger = LoggerFactory.getLogger(WWNDiscoveryService.class);
private final DiscoveryDataListener<WWNCamera> cameraDiscoveryDataListener = new DiscoveryDataListener<>(
WWNCamera.class, THING_TYPE_CAMERA, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<WWNSmokeDetector> smokeDetectorDiscoveryDataListener = new DiscoveryDataListener<>(
WWNSmokeDetector.class, THING_TYPE_SMOKE_DETECTOR, this::addDeviceDiscoveryResult);
private final DiscoveryDataListener<WWNStructure> structureDiscoveryDataListener = new DiscoveryDataListener<>(
WWNStructure.class, THING_TYPE_STRUCTURE, this::addStructureDiscoveryResult);
private final DiscoveryDataListener<WWNThermostat> thermostatDiscoveryDataListener = new DiscoveryDataListener<>(
WWNThermostat.class, THING_TYPE_THERMOSTAT, this::addDeviceDiscoveryResult);
@SuppressWarnings("rawtypes")
private final List<DiscoveryDataListener> discoveryDataListeners = List.of(cameraDiscoveryDataListener,
smokeDetectorDiscoveryDataListener, structureDiscoveryDataListener, thermostatDiscoveryDataListener);
private @NonNullByDefault({}) WWNAccountHandler accountHandler;
private static class DiscoveryDataListener<T> implements WWNThingDataListener<T> {
private Class<T> dataClass;
private ThingTypeUID thingTypeUID;
private BiConsumer<T, ThingTypeUID> onDiscovered;
private DiscoveryDataListener(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
this.dataClass = dataClass;
this.thingTypeUID = thingTypeUID;
this.onDiscovered = onDiscovered;
}
@Override
public void onNewData(T data) {
onDiscovered.accept(data, thingTypeUID);
}
@Override
public void onUpdatedData(T oldData, T data) {
}
@Override
public void onMissingData(String nestId) {
}
}
public WWNDiscoveryService() {
super(SUPPORTED_THING_TYPES, 60, true);
}
@Override
@SuppressWarnings("unchecked")
public void activate() {
discoveryDataListeners.forEach(listener -> accountHandler.addThingDataListener(listener.dataClass, listener));
addDiscoveryResultsFromLastUpdates();
}
@Override
@SuppressWarnings("unchecked")
public void deactivate() {
discoveryDataListeners
.forEach(listener -> accountHandler.removeThingDataListener(listener.dataClass, listener));
}
@Override
public @Nullable ThingHandler getThingHandler() {
return accountHandler;
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof WWNAccountHandler) {
accountHandler = (WWNAccountHandler) handler;
}
}
@Override
protected void startScan() {
addDiscoveryResultsFromLastUpdates();
}
@SuppressWarnings("unchecked")
private void addDiscoveryResultsFromLastUpdates() {
discoveryDataListeners.forEach(listener -> addDiscoveryResultsFromLastUpdates(listener.dataClass,
listener.thingTypeUID, listener.onDiscovered));
}
private <T> void addDiscoveryResultsFromLastUpdates(Class<T> dataClass, ThingTypeUID thingTypeUID,
BiConsumer<T, ThingTypeUID> onDiscovered) {
List<T> lastUpdates = accountHandler.getLastUpdates(dataClass);
lastUpdates.forEach(lastUpdate -> onDiscovered.accept(lastUpdate, thingTypeUID));
}
private void addDeviceDiscoveryResult(BaseWWNDevice device, ThingTypeUID typeUID) {
ThingUID bridgeUID = accountHandler.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, device.getDeviceId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = Map.of(WWNDeviceConfiguration.DEVICE_ID, device.getDeviceId(),
PROPERTY_FIRMWARE_VERSION, device.getSoftwareVersion());
thingDiscovered(DiscoveryResultBuilder.create(thingUID) //
.withThingType(typeUID) //
.withLabel(device.getNameLong()) //
.withBridge(bridgeUID) //
.withProperties(properties) //
.withRepresentationProperty(WWNDeviceConfiguration.DEVICE_ID) //
.build() //
);
}
public void addStructureDiscoveryResult(WWNStructure structure, ThingTypeUID typeUID) {
ThingUID bridgeUID = accountHandler.getThing().getUID();
ThingUID thingUID = new ThingUID(typeUID, bridgeUID, structure.getStructureId());
logger.debug("Discovered {}", thingUID);
Map<String, Object> properties = Map.of(WWNStructureConfiguration.STRUCTURE_ID, structure.getStructureId());
thingDiscovered(DiscoveryResultBuilder.create(thingUID) //
.withThingType(THING_TYPE_STRUCTURE) //
.withLabel(structure.getName()) //
.withBridge(bridgeUID) //
.withProperties(properties) //
.withRepresentationProperty(WWNStructureConfiguration.STRUCTURE_ID) //
.build() //
);
}
}

View File

@ -10,17 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
/**
* Default properties shared across all Nest devices.
* Default properties shared across all WWN devices.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class BaseNestDevice implements NestIdentifiable {
public class BaseWWNDevice implements WWNIdentifiable {
private String deviceId;
private String name;
@ -80,7 +80,7 @@ public class BaseNestDevice implements NestIdentifiable {
if (getClass() != obj.getClass()) {
return false;
}
BaseNestDevice other = (BaseNestDevice) obj;
BaseWWNDevice other = (BaseWWNDevice) obj;
if (deviceId == null) {
if (other.deviceId != null) {
return false;

View File

@ -10,15 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* Deals with the access token data that comes back from Nest when it is requested.
* Deals with the access token data that comes back from WWN when it is requested.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class AccessTokenData {
public class WWNAccessTokenData {
private String accessToken;
private Long expiresIn;
@ -42,7 +42,7 @@ public class AccessTokenData {
if (getClass() != obj.getClass()) {
return false;
}
AccessTokenData other = (AccessTokenData) obj;
WWNAccessTokenData other = (WWNAccessTokenData) obj;
if (accessToken == null) {
if (other.accessToken != null) {
return false;

View File

@ -10,15 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* The data for a camera activity zone.
* The data for a WWN camera activity zone.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract ActivityZone object from Camera
*/
public class ActivityZone {
public class WWNActivityZone {
private String name;
private int id;
@ -42,7 +42,7 @@ public class ActivityZone {
if (getClass() != obj.getClass()) {
return false;
}
ActivityZone other = (ActivityZone) obj;
WWNActivityZone other = (WWNActivityZone) obj;
if (id != other.id) {
return false;
}

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
import java.util.List;
/**
* The data for the camera.
* The data for the WWN camera.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Camera extends BaseNestDevice {
public class WWNCamera extends BaseWWNDevice {
private Boolean isStreaming;
private Boolean isAudioInputEnabled;
@ -30,10 +30,10 @@ public class Camera extends BaseNestDevice {
private String webUrl;
private String appUrl;
private Boolean isPublicShareEnabled;
private List<ActivityZone> activityZones;
private List<WWNActivityZone> activityZones;
private String publicShareUrl;
private String snapshotUrl;
private CameraEvent lastEvent;
private WWNCameraEvent lastEvent;
public Boolean isStreaming() {
return isStreaming;
@ -63,7 +63,7 @@ public class Camera extends BaseNestDevice {
return isPublicShareEnabled;
}
public List<ActivityZone> getActivityZones() {
public List<WWNActivityZone> getActivityZones() {
return activityZones;
}
@ -75,7 +75,7 @@ public class Camera extends BaseNestDevice {
return snapshotUrl;
}
public CameraEvent getLastEvent() {
public WWNCameraEvent getLastEvent() {
return lastEvent;
}
@ -84,13 +84,13 @@ public class Camera extends BaseNestDevice {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
if (obj == null || !super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Camera other = (Camera) obj;
WWNCamera other = (WWNCamera) obj;
if (activityZones == null) {
if (other.activityZones != null) {
return false;

View File

@ -10,19 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
import java.util.List;
/**
* The data for a camera event.
* The data for a WWN camera event.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract CameraEvent object from Camera
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class CameraEvent {
public class WWNCameraEvent {
private Boolean hasSound;
private Boolean hasMotion;
@ -91,7 +91,7 @@ public class CameraEvent {
if (getClass() != obj.getClass()) {
return false;
}
CameraEvent other = (CameraEvent) obj;
WWNCameraEvent other = (WWNCameraEvent) obj;
if (activityZoneIds == null) {
if (other.activityZoneIds != null) {
return false;

View File

@ -10,33 +10,33 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Map;
/**
* All the Nest devices broken up by type.
* All the WWN devices broken up by type.
*
* @author David Bennett - Initial contribution
*/
public class NestDevices {
public class WWNDevices {
private Map<String, Thermostat> thermostats;
private Map<String, SmokeDetector> smokeCoAlarms;
private Map<String, Camera> cameras;
private Map<String, WWNThermostat> thermostats;
private Map<String, WWNSmokeDetector> smokeCoAlarms;
private Map<String, WWNCamera> cameras;
/** Id to thermostat mapping */
public Map<String, Thermostat> getThermostats() {
public Map<String, WWNThermostat> getThermostats() {
return thermostats;
}
/** Id to camera mapping */
public Map<String, Camera> getCameras() {
public Map<String, WWNCamera> getCameras() {
return cameras;
}
/** Id to smoke detector */
public Map<String, SmokeDetector> getSmokeCoAlarms() {
public Map<String, WWNSmokeDetector> getSmokeCoAlarms() {
return smokeCoAlarms;
}
@ -51,7 +51,7 @@ public class NestDevices {
if (getClass() != obj.getClass()) {
return false;
}
NestDevices other = (NestDevices) obj;
WWNDevices other = (WWNDevices) obj;
if (cameras == null) {
if (other.cameras != null) {
return false;

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
/**
* Used to set and update the ETA values for Nest.
* Used to set and update the WWN ETA values.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract ETA object from Structure
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class ETA {
public class WWNETA {
private String tripId;
private Date estimatedArrivalWindowBegin;
@ -62,7 +62,7 @@ public class ETA {
if (getClass() != obj.getClass()) {
return false;
}
ETA other = (ETA) obj;
WWNETA other = (WWNETA) obj;
if (estimatedArrivalWindowBegin == null) {
if (other.estimatedArrivalWindowBegin != null) {
return false;

View File

@ -10,16 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* The data of Nest API errors.
* The data of WWN API errors.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling
* @author Wouter Born - Add equals and hashCode methods
*/
public class ErrorData {
public class WWNErrorData {
private String error;
private String type;
@ -53,7 +53,7 @@ public class ErrorData {
if (getClass() != obj.getClass()) {
return false;
}
ErrorData other = (ErrorData) obj;
WWNErrorData other = (WWNErrorData) obj;
if (error == null) {
if (other.error != null) {
return false;

View File

@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* Interface for uniquely identifiable Nest objects (device or a structure).
* Interface for uniquely identifiable WWN objects (device or a structure).
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Simplify working with deviceId and structureId
*/
public interface NestIdentifiable {
public interface WWNIdentifiable {
/**
* Returns the identifier that uniquely identifies the Nest object (deviceId or structureId).
* Returns the identifier that uniquely identifies the WWN object (deviceId or structureId).
*/
String getId();
}

View File

@ -10,15 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* The meta data in the data downloads from Nest.
* The WWN meta data in the data downloads.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class NestMetadata {
public class WWNMetadata {
private String accessToken;
private String clientVersion;
@ -42,7 +42,7 @@ public class NestMetadata {
if (getClass() != obj.getClass()) {
return false;
}
NestMetadata other = (NestMetadata) obj;
WWNMetadata other = (WWNMetadata) obj;
if (accessToken == null) {
if (other.accessToken != null) {
return false;

View File

@ -10,19 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* Data for the Nest smoke detector.
* Data for the WWN smoke detector.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class SmokeDetector extends BaseNestDevice {
public class WWNSmokeDetector extends BaseWWNDevice {
private BatteryHealth batteryHealth;
private AlarmState coAlarmState;
@ -87,13 +87,13 @@ public class SmokeDetector extends BaseNestDevice {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
if (obj == null || !super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SmokeDetector other = (SmokeDetector) obj;
WWNSmokeDetector other = (WWNSmokeDetector) obj;
if (batteryHealth != other.batteryHealth) {
return false;
}
@ -117,10 +117,7 @@ public class SmokeDetector extends BaseNestDevice {
if (smokeAlarmState != other.smokeAlarmState) {
return false;
}
if (uiColorState != other.uiColorState) {
return false;
}
return true;
return uiColorState == other.uiColorState;
}
@Override

View File

@ -10,23 +10,23 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.openhab.binding.nest.internal.data.SmokeDetector.AlarmState;
import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector.AlarmState;
import com.google.gson.annotations.SerializedName;
/**
* The structure details from Nest.
* The WWN structure details.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Structure implements NestIdentifiable {
public class WWNStructure implements WWNIdentifiable {
private String structureId;
private List<String> thermostats;
@ -38,13 +38,13 @@ public class Structure implements NestIdentifiable {
private Date peakPeriodEndTime;
private String timeZone;
private Date etaBegin;
private SmokeDetector.AlarmState coAlarmState;
private SmokeDetector.AlarmState smokeAlarmState;
private WWNSmokeDetector.AlarmState coAlarmState;
private WWNSmokeDetector.AlarmState smokeAlarmState;
private Boolean rhrEnrollment;
private Map<String, Where> wheres;
private Map<String, WWNWhere> wheres;
private HomeAwayState away;
private String name;
private ETA eta;
private WWNETA eta;
private SecurityState wwnSecurityState;
@Override
@ -112,11 +112,11 @@ public class Structure implements NestIdentifiable {
return rhrEnrollment;
}
public Map<String, Where> getWheres() {
public Map<String, WWNWhere> getWheres() {
return wheres;
}
public ETA getEta() {
public WWNETA getEta() {
return eta;
}
@ -155,7 +155,7 @@ public class Structure implements NestIdentifiable {
if (getClass() != obj.getClass()) {
return false;
}
Structure other = (Structure) obj;
WWNStructure other = (WWNStructure) obj;
if (away != other.away) {
return false;
}
@ -263,10 +263,7 @@ public class Structure implements NestIdentifiable {
} else if (!wheres.equals(other.wheres)) {
return false;
}
if (wwnSecurityState != other.wwnSecurityState) {
return false;
}
return true;
return wwnSecurityState == other.wwnSecurityState;
}
@Override

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
@ -23,12 +23,12 @@ import javax.measure.quantity.Temperature;
import com.google.gson.annotations.SerializedName;
/**
* Gson class to encapsulate the data for the Nest thermostat.
* Gson class to encapsulate the data for the WWN thermostat.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class Thermostat extends BaseNestDevice {
public class WWNThermostat extends BaseWWNDevice {
private Boolean canCool;
private Boolean canHeat;
@ -262,13 +262,13 @@ public class Thermostat extends BaseNestDevice {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
if (obj == null || !super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Thermostat other = (Thermostat) obj;
WWNThermostat other = (WWNThermostat) obj;
if (ambientTemperatureC == null) {
if (other.ambientTemperatureC != null) {
return false;

View File

@ -10,31 +10,31 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.Map;
/**
* Top level data for all the Nest stuff, this is the format the Nest data comes back from Nest in.
* The top level WWN data that is sent by Nest.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Add equals and hashCode methods
*/
public class TopLevelData {
public class WWNTopLevelData {
private NestDevices devices;
private NestMetadata metadata;
private Map<String, Structure> structures;
private WWNDevices devices;
private WWNMetadata metadata;
private Map<String, WWNStructure> structures;
public NestDevices getDevices() {
public WWNDevices getDevices() {
return devices;
}
public NestMetadata getMetadata() {
public WWNMetadata getMetadata() {
return metadata;
}
public Map<String, Structure> getStructures() {
public Map<String, WWNStructure> getStructures() {
return structures;
}
@ -49,7 +49,7 @@ public class TopLevelData {
if (getClass() != obj.getClass()) {
return false;
}
TopLevelData other = (TopLevelData) obj;
WWNTopLevelData other = (WWNTopLevelData) obj;
if (devices == null) {
if (other.devices != null) {
return false;

View File

@ -10,25 +10,25 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* The top level data that is sent by Nest to a streaming REST client using SSE.
* The top level WWN data that is sent by Nest to a streaming REST client using SSE.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
* @author Wouter Born - Add equals and hashCode methods
*/
public class TopLevelStreamingData {
public class WWNTopLevelStreamingData {
private String path;
private TopLevelData data;
private WWNTopLevelData data;
public String getPath() {
return path;
}
public TopLevelData getData() {
public WWNTopLevelData getData() {
return data;
}
@ -52,7 +52,7 @@ public class TopLevelStreamingData {
if (getClass() != obj.getClass()) {
return false;
}
TopLevelStreamingData other = (TopLevelStreamingData) obj;
WWNTopLevelStreamingData other = (WWNTopLevelStreamingData) obj;
if (data == null) {
if (other.data != null) {
return false;

View File

@ -10,21 +10,21 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.rest;
package org.openhab.binding.nest.internal.wwn.dto;
import java.util.HashMap;
import java.util.Map;
/**
* Contains the data needed to do an update request back to Nest.
* Contains the data needed to do an WWN update request back to Nest.
*
* @author David Bennett - Initial contribution
*/
public class NestUpdateRequest {
public class WWNUpdateRequest {
private final String updatePath;
private final Map<String, Object> values;
private NestUpdateRequest(Builder builder) {
private WWNUpdateRequest(Builder builder) {
this.updatePath = builder.basePath + builder.identifier;
this.values = builder.values;
}
@ -57,8 +57,8 @@ public class NestUpdateRequest {
return this;
}
public NestUpdateRequest build() {
return new NestUpdateRequest(this);
public WWNUpdateRequest build() {
return new WWNUpdateRequest(this);
}
}
}

View File

@ -10,14 +10,14 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.data;
package org.openhab.binding.nest.internal.wwn.dto;
/**
* @author David Bennett - Initial contribution
* @author Wouter Born - Extract Where object from Structure
* @author Wouter Born - Add equals, hashCode, toString methods
*/
public class Where {
public class WWNWhere {
private String whereId;
private String name;
@ -40,7 +40,7 @@ public class Where {
if (getClass() != obj.getClass()) {
return false;
}
Where other = (Where) obj;
WWNWhere other = (WWNWhere) obj;
if (name == null) {
if (other.name != null) {
return false;

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.exceptions;
package org.openhab.binding.nest.internal.wwn.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown when the bridge was unable to resolve the Nest redirect URL.
@ -18,17 +20,18 @@ package org.openhab.binding.nest.internal.exceptions;
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling while sending data
*/
@NonNullByDefault
@SuppressWarnings("serial")
public class FailedResolvingNestUrlException extends Exception {
public FailedResolvingNestUrlException(String message) {
public class FailedResolvingWWNUrlException extends Exception {
public FailedResolvingWWNUrlException(String message) {
super(message);
}
public FailedResolvingNestUrlException(String message, Throwable cause) {
public FailedResolvingWWNUrlException(String message, Throwable cause) {
super(message, cause);
}
public FailedResolvingNestUrlException(Throwable cause) {
public FailedResolvingWWNUrlException(Throwable cause) {
super(cause);
}
}

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.exceptions;
package org.openhab.binding.nest.internal.wwn.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown when the bridge was unable to retrieve data.
@ -18,18 +20,19 @@ package org.openhab.binding.nest.internal.exceptions;
* @author Martin van Wingerden - Initial contribution
* @author Martin van Wingerden - Added more centralized handling of failure when retrieving data
*/
@NonNullByDefault
@SuppressWarnings("serial")
public class FailedRetrievingNestDataException extends Exception {
public class FailedRetrievingWWNDataException extends Exception {
public FailedRetrievingNestDataException(String message) {
public FailedRetrievingWWNDataException(String message) {
super(message);
}
public FailedRetrievingNestDataException(String message, Throwable cause) {
public FailedRetrievingWWNDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedRetrievingNestDataException(Throwable cause) {
public FailedRetrievingWWNDataException(Throwable cause) {
super(cause);
}
}

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.exceptions;
package org.openhab.binding.nest.internal.wwn.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown when the bridge was unable to send data.
@ -18,17 +20,18 @@ package org.openhab.binding.nest.internal.exceptions;
* @author Wouter Born - Initial contribution
* @author Wouter Born - Improve exception handling while sending data
*/
@NonNullByDefault
@SuppressWarnings("serial")
public class FailedSendingNestDataException extends Exception {
public FailedSendingNestDataException(String message) {
public class FailedSendingWWNDataException extends Exception {
public FailedSendingWWNDataException(String message) {
super(message);
}
public FailedSendingNestDataException(String message, Throwable cause) {
public FailedSendingWWNDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedSendingNestDataException(Throwable cause) {
public FailedSendingWWNDataException(Throwable cause) {
super(cause);
}
}

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.exceptions;
package org.openhab.binding.nest.internal.wwn.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown when there is no valid access token and it was not possible to refresh it
@ -18,17 +20,18 @@ package org.openhab.binding.nest.internal.exceptions;
* @author Martin van Wingerden - Initial contribution
* @author Martin van Wingerden - Added more centralized handling of invalid access tokens
*/
@NonNullByDefault
@SuppressWarnings("serial")
public class InvalidAccessTokenException extends Exception {
public InvalidAccessTokenException(Exception cause) {
public class InvalidWWNAccessTokenException extends Exception {
public InvalidWWNAccessTokenException(Exception cause) {
super(cause);
}
public InvalidAccessTokenException(String message, Throwable cause) {
public InvalidWWNAccessTokenException(String message, Throwable cause) {
super(message, cause);
}
public InvalidAccessTokenException(String message) {
public InvalidWWNAccessTokenException(String message) {
super(message);
}
}

View File

@ -10,14 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.JSON_CONTENT_TYPE;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
@ -30,20 +31,21 @@ import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.data.ErrorData;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.binding.nest.internal.exceptions.FailedSendingNestDataException;
import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
import org.openhab.binding.nest.internal.listener.NestStreamingDataListener;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.rest.NestAuthorizer;
import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
import org.openhab.binding.nest.internal.rest.NestUpdateRequest;
import org.openhab.binding.nest.internal.update.NestCompositeUpdateHandler;
import org.openhab.binding.nest.internal.wwn.WWNUtils;
import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration;
import org.openhab.binding.nest.internal.wwn.discovery.WWNDiscoveryService;
import org.openhab.binding.nest.internal.wwn.dto.WWNErrorData;
import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable;
import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData;
import org.openhab.binding.nest.internal.wwn.dto.WWNUpdateRequest;
import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException;
import org.openhab.binding.nest.internal.wwn.exceptions.FailedSendingWWNDataException;
import org.openhab.binding.nest.internal.wwn.exceptions.InvalidWWNAccessTokenException;
import org.openhab.binding.nest.internal.wwn.listener.WWNStreamingDataListener;
import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener;
import org.openhab.binding.nest.internal.wwn.rest.WWNAuthorizer;
import org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient;
import org.openhab.binding.nest.internal.wwn.update.WWNCompositeUpdateHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.thing.Bridge;
@ -53,6 +55,7 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
@ -60,7 +63,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This bridge handler connects to Nest and handles all the API requests. It pulls down the
* This account handler connects to Nest and handles all the WWN API requests. It pulls down the
* updated data, polls the system and does all the co-ordination with the other handlers
* to get the data updated to the correct things.
*
@ -69,32 +72,32 @@ import org.slf4j.LoggerFactory;
* @author Wouter Born - Improve exception and URL redirect handling
*/
@NonNullByDefault
public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener {
public class WWNAccountHandler extends BaseBridgeHandler implements WWNStreamingDataListener {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class);
private final Logger logger = LoggerFactory.getLogger(WWNAccountHandler.class);
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final List<NestUpdateRequest> nestUpdateRequests = new CopyOnWriteArrayList<>();
private final NestCompositeUpdateHandler updateHandler = new NestCompositeUpdateHandler(
private final List<WWNUpdateRequest> nestUpdateRequests = new CopyOnWriteArrayList<>();
private final WWNCompositeUpdateHandler updateHandler = new WWNCompositeUpdateHandler(
this::getPresentThingsNestIds);
private @NonNullByDefault({}) NestAuthorizer authorizer;
private @NonNullByDefault({}) NestBridgeConfiguration config;
private @NonNullByDefault({}) WWNAuthorizer authorizer;
private @NonNullByDefault({}) WWNAccountConfiguration config;
private @Nullable ScheduledFuture<?> initializeJob;
private @Nullable ScheduledFuture<?> transmitJob;
private @Nullable NestRedirectUrlSupplier redirectUrlSupplier;
private @Nullable NestStreamingRestClient streamingRestClient;
private @Nullable WWNRedirectUrlSupplier redirectUrlSupplier;
private @Nullable WWNStreamingRestClient streamingRestClient;
/**
* Creates the bridge handler to connect to Nest.
*
* @param bridge The bridge to connect to Nest with.
*/
public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) {
public WWNAccountHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) {
super(bridge);
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
@ -107,8 +110,8 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
public void initialize() {
logger.debug("Initializing Nest bridge handler");
config = getConfigAs(NestBridgeConfiguration.class);
authorizer = new NestAuthorizer(config);
config = getConfigAs(WWNAccountConfiguration.class);
authorizer = new WWNAuthorizer(config);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query");
initializeJob = scheduler.schedule(() -> {
@ -119,7 +122,7 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
logger.debug("Access Token {}", getExistingOrNewAccessToken());
redirectUrlSupplier = createRedirectUrlSupplier();
restartStreamingUpdates();
} catch (InvalidAccessTokenException e) {
} catch (InvalidWWNAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
@ -154,27 +157,27 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
this.streamingRestClient = null;
}
public <T> boolean addThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
public <T> boolean addThingDataListener(Class<T> dataClass, WWNThingDataListener<T> listener) {
return updateHandler.addListener(dataClass, listener);
}
public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, WWNThingDataListener<T> listener) {
return updateHandler.addListener(dataClass, nestId, listener);
}
/**
* Adds the update request into the queue for doing something with, send immediately if the queue is empty.
*/
public void addUpdateRequest(NestUpdateRequest request) {
public void addUpdateRequest(WWNUpdateRequest request) {
nestUpdateRequests.add(request);
scheduleTransmitJobForPendingRequests();
}
protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
return new NestRedirectUrlSupplier(getHttpHeaders());
protected WWNRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidWWNAccessTokenException {
return new WWNRedirectUrlSupplier(getHttpHeaders());
}
private String getExistingOrNewAccessToken() throws InvalidAccessTokenException {
private String getExistingOrNewAccessToken() throws InvalidWWNAccessTokenException {
String accessToken = config.accessToken;
if (accessToken == null || accessToken.isEmpty()) {
accessToken = authorizer.getNewAccessToken();
@ -182,8 +185,8 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
config.pincode = "";
// Update and save the access token in the bridge configuration
Configuration configuration = editConfiguration();
configuration.put(NestBridgeConfiguration.ACCESS_TOKEN, config.accessToken);
configuration.put(NestBridgeConfiguration.PINCODE, config.pincode);
configuration.put(WWNAccountConfiguration.ACCESS_TOKEN, config.accessToken);
configuration.put(WWNAccountConfiguration.PINCODE, config.pincode);
updateConfiguration(configuration);
logger.debug("Retrieved new access token: {}", config.accessToken);
return accessToken;
@ -193,7 +196,7 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
}
}
protected Properties getHttpHeaders() throws InvalidAccessTokenException {
protected Properties getHttpHeaders() throws InvalidWWNAccessTokenException {
Properties httpHeaders = new Properties();
httpHeaders.put("Authorization", "Bearer " + getExistingOrNewAccessToken());
httpHeaders.put("Content-Type", JSON_CONTENT_TYPE);
@ -208,8 +211,8 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
return updateHandler.getLastUpdates(dataClass);
}
private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException {
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
private WWNRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidWWNAccessTokenException {
WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier == null) {
localRedirectUrlSupplier = createRedirectUrlSupplier();
redirectUrlSupplier = localRedirectUrlSupplier;
@ -222,12 +225,17 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
for (Thing thing : getThing().getThings()) {
ThingHandler handler = thing.getHandler();
if (handler != null && thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.GONE) {
nestIds.add(((NestIdentifiable) handler).getId());
nestIds.add(((WWNIdentifiable) handler).getId());
}
}
return nestIds;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(WWNDiscoveryService.class);
}
/**
* Handles an incoming command update
*/
@ -239,18 +247,18 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
}
}
private void jsonToPutUrl(NestUpdateRequest request)
throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException {
private void jsonToPutUrl(WWNUpdateRequest request)
throws FailedSendingWWNDataException, InvalidWWNAccessTokenException, FailedResolvingWWNUrlException {
try {
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier == null) {
throw new FailedResolvingNestUrlException("redirectUrlSupplier is null");
throw new FailedResolvingWWNUrlException("redirectUrlSupplier is null");
}
String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath();
logger.debug("Putting data to: {}", url);
String jsonContent = NestUtils.toJson(request.getValues());
String jsonContent = WWNUtils.toJson(request.getValues());
logger.debug("PUT content: {}", jsonContent);
ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8));
@ -258,13 +266,13 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
REQUEST_TIMEOUT);
logger.debug("PUT response: {}", jsonResponse);
ErrorData error = NestUtils.fromJson(jsonResponse, ErrorData.class);
WWNErrorData error = WWNUtils.fromJson(jsonResponse, WWNErrorData.class);
if (error.getError() != null && !error.getError().isBlank()) {
logger.debug("Nest API error: {}", error);
logger.warn("Nest API error: {}", error.getMessage());
}
} catch (IOException e) {
throw new FailedSendingNestDataException("Failed to send data", e);
throw new FailedSendingWWNDataException("Failed to send data", e);
}
}
@ -291,16 +299,16 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
}
@Override
public void onNewTopLevelData(TopLevelData data) {
public void onNewTopLevelData(WWNTopLevelData data) {
updateHandler.handleUpdate(data);
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data");
}
public <T> boolean removeThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
public <T> boolean removeThingDataListener(Class<T> dataClass, WWNThingDataListener<T> listener) {
return updateHandler.removeListener(dataClass, listener);
}
public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, WWNThingDataListener<T> listener) {
return updateHandler.removeListener(dataClass, nestId, listener);
}
@ -321,14 +329,14 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
private void startStreamingUpdates() {
synchronized (this) {
try {
NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient(
WWNStreamingRestClient localStreamingRestClient = new WWNStreamingRestClient(
getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory,
getOrCreateRedirectUrlSupplier(), scheduler);
localStreamingRestClient.addStreamingDataListener(this);
localStreamingRestClient.start();
streamingRestClient = localStreamingRestClient;
} catch (InvalidAccessTokenException e) {
} catch (InvalidWWNAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
@ -337,7 +345,7 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
}
private void stopStreamingUpdates() {
NestStreamingRestClient localStreamingRestClient = streamingRestClient;
WWNStreamingRestClient localStreamingRestClient = streamingRestClient;
if (localStreamingRestClient != null) {
synchronized (this) {
localStreamingRestClient.stop();
@ -357,24 +365,24 @@ public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamin
try {
while (!nestUpdateRequests.isEmpty()) {
// nestUpdateRequests is a CopyOnWriteArrayList so its iterator does not support remove operations
NestUpdateRequest request = nestUpdateRequests.get(0);
WWNUpdateRequest request = nestUpdateRequests.get(0);
jsonToPutUrl(request);
nestUpdateRequests.remove(request);
}
} catch (InvalidAccessTokenException e) {
} catch (InvalidWWNAccessTokenException e) {
logger.debug("Invalid access token", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Token is invalid and could not be refreshed: " + e.getMessage());
} catch (FailedResolvingNestUrlException e) {
} catch (FailedResolvingWWNUrlException e) {
logger.debug("Unable to resolve redirect URL", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
} catch (FailedSendingNestDataException e) {
} catch (FailedSendingWWNDataException e) {
logger.debug("Error sending data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
if (localRedirectUrlSupplier != null) {
localRedirectUrlSupplier.resetCache();
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import java.time.Instant;
import java.time.ZonedDateTime;
@ -22,12 +22,13 @@ import java.util.stream.Collectors;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.rest.NestUpdateRequest;
import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration;
import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable;
import org.openhab.binding.nest.internal.wwn.dto.WWNUpdateRequest;
import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -46,7 +47,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Deals with the structures on the Nest API, turning them into a thing in openHAB.
* Deals with the structures on the WWN API, turning them into a thing in openHAB.
*
* @author David Bennett - Initial contribution
* @author Martin van Wingerden - Splitted of NestBaseHandler
@ -55,14 +56,14 @@ import org.slf4j.LoggerFactory;
* @param <T> the type of update data
*/
@NonNullByDefault
public abstract class NestBaseHandler<T> extends BaseThingHandler
implements NestThingDataListener<T>, NestIdentifiable {
private final Logger logger = LoggerFactory.getLogger(NestBaseHandler.class);
public abstract class WWNBaseHandler<@NonNull T> extends BaseThingHandler
implements WWNThingDataListener<T>, WWNIdentifiable {
private final Logger logger = LoggerFactory.getLogger(WWNBaseHandler.class);
private @Nullable String deviceId;
private String deviceId = "";
private Class<T> dataClass;
NestBaseHandler(Thing thing, Class<T> dataClass) {
WWNBaseHandler(Thing thing, Class<T> dataClass) {
super(thing);
this.dataClass = dataClass;
}
@ -71,7 +72,7 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
public void initialize() {
logger.debug("Initializing handler for {}", getClass().getName());
NestBridgeHandler handler = getNestBridgeHandler();
WWNAccountHandler handler = getAccountHandler();
if (handler != null) {
boolean success = handler.addThingDataListener(dataClass, getId(), this);
logger.debug("Adding {} with ID '{}' as device data listener, result: {}", getClass().getSimpleName(),
@ -83,7 +84,7 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for refresh");
T lastUpdate = getLastUpdate();
final @Nullable T lastUpdate = getLastUpdate();
if (lastUpdate != null) {
update(null, lastUpdate);
}
@ -91,14 +92,14 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
@Override
public void dispose() {
NestBridgeHandler handler = getNestBridgeHandler();
WWNAccountHandler handler = getAccountHandler();
if (handler != null) {
handler.removeThingDataListener(dataClass, getId(), this);
}
}
protected @Nullable T getLastUpdate() {
NestBridgeHandler handler = getNestBridgeHandler();
WWNAccountHandler handler = getAccountHandler();
if (handler != null) {
return handler.getLastUpdate(dataClass, getId());
}
@ -106,15 +107,13 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
}
protected void addUpdateRequest(String updatePath, String field, Object value) {
NestBridgeHandler handler = getNestBridgeHandler();
WWNAccountHandler handler = getAccountHandler();
if (handler != null) {
// @formatter:off
handler.addUpdateRequest(new NestUpdateRequest.Builder()
.withBasePath(updatePath)
.withIdentifier(getId())
.withAdditionalValue(field, value)
.build());
// @formatter:on
handler.addUpdateRequest(new WWNUpdateRequest.Builder() //
.withBasePath(updatePath) //
.withIdentifier(getId()) //
.withAdditionalValue(field, value) //
.build());
}
}
@ -125,16 +124,16 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
protected String getDeviceId() {
String localDeviceId = deviceId;
if (localDeviceId == null) {
localDeviceId = getConfigAs(NestDeviceConfiguration.class).deviceId;
if (localDeviceId.isEmpty()) {
localDeviceId = getConfigAs(WWNDeviceConfiguration.class).deviceId;
deviceId = localDeviceId;
}
return localDeviceId;
}
protected @Nullable NestBridgeHandler getNestBridgeHandler() {
protected @Nullable WWNAccountHandler getAccountHandler() {
Bridge bridge = getBridge();
return bridge != null ? (NestBridgeHandler) bridge.getHandler() : null;
return bridge != null ? (WWNAccountHandler) bridge.getHandler() : null;
}
protected abstract State getChannelState(ChannelUID channelUID, T data);
@ -165,23 +164,24 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
return value == null ? UnDefType.NULL : new StringType(value.toString());
}
protected State getAsStringTypeListOrNull(@Nullable Collection<?> values) {
protected State getAsStringTypeListOrNull(@Nullable Collection<@NonNull ?> values) {
return values == null || values.isEmpty() ? UnDefType.NULL
: new StringType(values.stream().map(v -> v.toString()).collect(Collectors.joining(",")));
: new StringType(values.stream().map(value -> value.toString()).collect(Collectors.joining(",")));
}
protected boolean isNotHandling(NestIdentifiable nestIdentifiable) {
protected boolean isNotHandling(WWNIdentifiable nestIdentifiable) {
return !(getId().equals(nestIdentifiable.getId()));
}
protected void updateLinkedChannels(T oldData, T data) {
getThing().getChannels().stream().map(c -> c.getUID()).filter(this::isLinked).forEach(channelUID -> {
State newState = getChannelState(channelUID, data);
if (oldData == null || !getChannelState(channelUID, oldData).equals(newState)) {
logger.debug("Updating {}", channelUID);
updateState(channelUID, newState);
}
});
protected void updateLinkedChannels(@Nullable T oldData, T data) {
getThing().getChannels().stream().map(channel -> channel.getUID()).filter(this::isLinked)
.forEach(channelUID -> {
State newState = getChannelState(channelUID, data);
if (oldData == null || !getChannelState(channelUID, oldData).equals(newState)) {
logger.debug("Updating {}", channelUID);
updateState(channelUID, newState);
}
});
}
@Override
@ -200,5 +200,5 @@ public abstract class NestBaseHandler<T> extends BaseThingHandler
new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Missing from streaming updates"));
}
protected abstract void update(T oldData, T data);
protected abstract void update(@Nullable T oldData, T data);
}

View File

@ -10,15 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.Camera;
import org.openhab.binding.nest.internal.data.CameraEvent;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.wwn.dto.WWNCamera;
import org.openhab.binding.nest.internal.wwn.dto.WWNCameraEvent;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -30,22 +31,21 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles all the updates to the camera as well as handling the commands that send
* updates to Nest.
* Handles all the updates to the camera as well as handling the commands that send updates to the WWN API.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestCameraHandler extends NestBaseHandler<Camera> {
private final Logger logger = LoggerFactory.getLogger(NestCameraHandler.class);
public class WWNCameraHandler extends WWNBaseHandler<WWNCamera> {
private final Logger logger = LoggerFactory.getLogger(WWNCameraHandler.class);
public NestCameraHandler(Thing thing) {
super(thing, Camera.class);
public WWNCameraHandler(Thing thing) {
super(thing, WWNCamera.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Camera camera) {
protected State getChannelState(ChannelUID channelUID, WWNCamera camera) {
if (channelUID.getId().startsWith(CHANNEL_GROUP_CAMERA_PREFIX)) {
return getCameraChannelState(channelUID, camera);
} else if (channelUID.getId().startsWith(CHANNEL_GROUP_LAST_EVENT_PREFIX)) {
@ -56,7 +56,7 @@ public class NestCameraHandler extends NestBaseHandler<Camera> {
}
}
protected State getCameraChannelState(ChannelUID channelUID, Camera camera) {
protected State getCameraChannelState(ChannelUID channelUID, WWNCamera camera) {
switch (channelUID.getId()) {
case CHANNEL_CAMERA_APP_URL:
return getAsStringTypeOrNull(camera.getAppUrl());
@ -82,8 +82,8 @@ public class NestCameraHandler extends NestBaseHandler<Camera> {
}
}
protected State getLastEventChannelState(ChannelUID channelUID, Camera camera) {
CameraEvent lastEvent = camera.getLastEvent();
protected State getLastEventChannelState(ChannelUID channelUID, WWNCamera camera) {
WWNCameraEvent lastEvent = camera.getLastEvent();
if (lastEvent == null) {
return UnDefType.NULL;
}
@ -120,7 +120,7 @@ public class NestCameraHandler extends NestBaseHandler<Camera> {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Camera lastUpdate = getLastUpdate();
WWNCamera lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
@ -138,7 +138,7 @@ public class NestCameraHandler extends NestBaseHandler<Camera> {
}
@Override
protected void update(Camera oldCamera, Camera camera) {
protected void update(@Nullable WWNCamera oldCamera, WWNCamera camera) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldCamera, camera);

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
@ -23,33 +23,33 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.nest.internal.NestBindingConstants;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.binding.nest.internal.wwn.WWNBindingConstants;
import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Supplies resolved redirect URLs of {@link NestBindingConstants#NEST_URL} so they can be used with HTTP clients that
* Supplies resolved redirect URLs of {@link WWNBindingConstants#NEST_URL} so they can be used with HTTP clients that
* do not pass Authorization headers after redirects like the Jetty client used by {@link HttpUtil}.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Extract resolving redirect URL from NestBridgeHandler into NestRedirectUrlSupplier
*/
@NonNullByDefault
public class NestRedirectUrlSupplier {
public class WWNRedirectUrlSupplier {
private final Logger logger = LoggerFactory.getLogger(NestRedirectUrlSupplier.class);
private final Logger logger = LoggerFactory.getLogger(WWNRedirectUrlSupplier.class);
protected String cachedUrl = "";
protected Properties httpHeaders;
public NestRedirectUrlSupplier(Properties httpHeaders) {
public WWNRedirectUrlSupplier(Properties httpHeaders) {
this.httpHeaders = httpHeaders;
}
public String getRedirectUrl() throws FailedResolvingNestUrlException {
public String getRedirectUrl() throws FailedResolvingWWNUrlException {
if (cachedUrl.isEmpty()) {
cachedUrl = resolveRedirectUrl();
}
@ -61,7 +61,7 @@ public class NestRedirectUrlSupplier {
}
/**
* Resolves the redirect URL for calls using the {@link NestBindingConstants#NEST_URL}.
* Resolves the redirect URL for calls using the {@link WWNBindingConstants#NEST_URL}.
*
* The Jetty client used by {@link HttpUtil} will not pass the Authorization header after a redirect resulting in
* "401 Unauthorized error" issues.
@ -70,11 +70,11 @@ public class NestRedirectUrlSupplier {
*
* @see https://developers.nest.com/documentation/cloud/how-to-handle-redirects
*/
private String resolveRedirectUrl() throws FailedResolvingNestUrlException {
private String resolveRedirectUrl() throws FailedResolvingWWNUrlException {
HttpClient httpClient = new HttpClient(new SslContextFactory.Client());
httpClient.setFollowRedirects(false);
Request request = httpClient.newRequest(NestBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30,
Request request = httpClient.newRequest(WWNBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30,
TimeUnit.SECONDS);
for (String httpHeaderKey : httpHeaders.stringPropertyNames()) {
request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey));
@ -86,7 +86,7 @@ public class NestRedirectUrlSupplier {
response = request.send();
httpClient.stop();
} catch (Exception e) {
throw new FailedResolvingNestUrlException("Failed to resolve redirect URL: " + e.getMessage(), e);
throw new FailedResolvingWWNUrlException("Failed to resolve redirect URL: " + e.getMessage(), e);
}
int status = response.getStatus();
@ -95,10 +95,10 @@ public class NestRedirectUrlSupplier {
if (status != HttpStatus.TEMPORARY_REDIRECT_307) {
logger.debug("Redirect status: {}", status);
logger.debug("Redirect response: {}", response.getContentAsString());
throw new FailedResolvingNestUrlException("Failed to get redirect URL, expected status "
throw new FailedResolvingWWNUrlException("Failed to get redirect URL, expected status "
+ HttpStatus.TEMPORARY_REDIRECT_307 + " but was " + status);
} else if (redirectUrl == null || redirectUrl.isEmpty()) {
throw new FailedResolvingNestUrlException("Redirect URL is empty");
throw new FailedResolvingWWNUrlException("Redirect URL is empty");
}
redirectUrl = redirectUrl.endsWith("/") ? redirectUrl.substring(0, redirectUrl.length() - 1) : redirectUrl;

View File

@ -10,15 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.SmokeDetector;
import org.openhab.binding.nest.internal.data.SmokeDetector.BatteryHealth;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector;
import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector.BatteryHealth;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -29,21 +30,21 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The smoke detector handler, it handles the data from Nest for the smoke detector.
* The smoke detector handler, it handles the data from WWN for the smoke detector.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestSmokeDetectorHandler extends NestBaseHandler<SmokeDetector> {
private final Logger logger = LoggerFactory.getLogger(NestSmokeDetectorHandler.class);
public class WWNSmokeDetectorHandler extends WWNBaseHandler<WWNSmokeDetector> {
private final Logger logger = LoggerFactory.getLogger(WWNSmokeDetectorHandler.class);
public NestSmokeDetectorHandler(Thing thing) {
super(thing, SmokeDetector.class);
public WWNSmokeDetectorHandler(Thing thing) {
super(thing, WWNSmokeDetector.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, SmokeDetector smokeDetector) {
protected State getChannelState(ChannelUID channelUID, WWNSmokeDetector smokeDetector) {
switch (channelUID.getId()) {
case CHANNEL_CO_ALARM_STATE:
return getAsStringTypeOrNull(smokeDetector.getCoAlarmState());
@ -72,7 +73,7 @@ public class NestSmokeDetectorHandler extends NestBaseHandler<SmokeDetector> {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
SmokeDetector lastUpdate = getLastUpdate();
WWNSmokeDetector lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
@ -80,7 +81,7 @@ public class NestSmokeDetectorHandler extends NestBaseHandler<SmokeDetector> {
}
@Override
protected void update(SmokeDetector oldSmokeDetector, SmokeDetector smokeDetector) {
protected void update(@Nullable WWNSmokeDetector oldSmokeDetector, WWNSmokeDetector smokeDetector) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldSmokeDetector, smokeDetector);

View File

@ -10,16 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import static org.openhab.core.types.RefreshType.REFRESH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.config.NestStructureConfiguration;
import org.openhab.binding.nest.internal.data.Structure;
import org.openhab.binding.nest.internal.data.Structure.HomeAwayState;
import org.openhab.binding.nest.internal.wwn.config.WWNStructureConfiguration;
import org.openhab.binding.nest.internal.wwn.dto.WWNStructure;
import org.openhab.binding.nest.internal.wwn.dto.WWNStructure.HomeAwayState;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -31,23 +31,23 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Deals with the structures on the Nest API, turning them into a thing in openHAB.
* Deals with the structures on the WWN API, turning them into a thing in openHAB.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestStructureHandler extends NestBaseHandler<Structure> {
private final Logger logger = LoggerFactory.getLogger(NestStructureHandler.class);
public class WWNStructureHandler extends WWNBaseHandler<WWNStructure> {
private final Logger logger = LoggerFactory.getLogger(WWNStructureHandler.class);
private @Nullable String structureId;
public NestStructureHandler(Thing thing) {
super(thing, Structure.class);
public WWNStructureHandler(Thing thing) {
super(thing, WWNStructure.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Structure structure) {
protected State getChannelState(ChannelUID channelUID, WWNStructure structure) {
switch (channelUID.getId()) {
case CHANNEL_AWAY:
return getAsStringTypeOrNull(structure.getAway());
@ -85,7 +85,7 @@ public class NestStructureHandler extends NestBaseHandler<Structure> {
private String getStructureId() {
String localStructureId = structureId;
if (localStructureId == null) {
localStructureId = getConfigAs(NestStructureConfiguration.class).structureId;
localStructureId = getConfigAs(WWNStructureConfiguration.class).structureId;
structureId = localStructureId;
}
return localStructureId;
@ -101,7 +101,7 @@ public class NestStructureHandler extends NestBaseHandler<Structure> {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Structure lastUpdate = getLastUpdate();
WWNStructure lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
@ -116,7 +116,7 @@ public class NestStructureHandler extends NestBaseHandler<Structure> {
}
@Override
protected void update(Structure oldStructure, Structure structure) {
protected void update(@Nullable WWNStructure oldStructure, WWNStructure structure) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldStructure, structure);

View File

@ -10,9 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.handler;
package org.openhab.binding.nest.internal.wwn.handler;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.types.RefreshType.REFRESH;
@ -26,8 +26,8 @@ import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.data.Thermostat;
import org.openhab.binding.nest.internal.data.Thermostat.Mode;
import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat;
import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat.Mode;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
@ -42,22 +42,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NestThermostatHandler} is responsible for handling commands, which are
* The {@link WWNThermostatHandler} is responsible for handling commands, which are
* sent to one of the channels for the thermostat.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Handle channel refresh command
*/
@NonNullByDefault
public class NestThermostatHandler extends NestBaseHandler<Thermostat> {
private final Logger logger = LoggerFactory.getLogger(NestThermostatHandler.class);
public class WWNThermostatHandler extends WWNBaseHandler<WWNThermostat> {
private final Logger logger = LoggerFactory.getLogger(WWNThermostatHandler.class);
public NestThermostatHandler(Thing thing) {
super(thing, Thermostat.class);
public WWNThermostatHandler(Thing thing) {
super(thing, WWNThermostat.class);
}
@Override
protected State getChannelState(ChannelUID channelUID, Thermostat thermostat) {
protected State getChannelState(ChannelUID channelUID, WWNThermostat thermostat) {
switch (channelUID.getId()) {
case CHANNEL_CAN_COOL:
return getAsOnOffTypeOrNull(thermostat.isCanCool());
@ -125,7 +125,7 @@ public class NestThermostatHandler extends NestBaseHandler<Thermostat> {
@SuppressWarnings("unchecked")
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
Thermostat lastUpdate = getLastUpdate();
WWNThermostat lastUpdate = getLastUpdate();
if (lastUpdate != null) {
updateState(channelUID, getChannelState(channelUID, lastUpdate));
}
@ -182,7 +182,7 @@ public class NestThermostatHandler extends NestBaseHandler<Thermostat> {
}
private Unit<Temperature> getTemperatureUnit(Unit<Temperature> fallbackUnit) {
Thermostat lastUpdate = getLastUpdate();
WWNThermostat lastUpdate = getLastUpdate();
if (lastUpdate != null && lastUpdate.getTemperatureUnit() != null) {
return lastUpdate.getTemperatureUnit();
}
@ -204,7 +204,7 @@ public class NestThermostatHandler extends NestBaseHandler<Thermostat> {
}
@Override
protected void update(Thermostat oldThermostat, Thermostat thermostat) {
protected void update(@Nullable WWNThermostat oldThermostat, WWNThermostat thermostat) {
logger.debug("Updating {}", getThing().getUID());
updateLinkedChannels(oldThermostat, thermostat);

View File

@ -10,20 +10,20 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.listener;
package org.openhab.binding.nest.internal.wwn.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData;
import org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient;
/**
* Interface for listeners of events generated by the {@link NestStreamingRestClient}.
* Interface for listeners of events generated by the {@link WWNStreamingRestClient}.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public interface NestStreamingDataListener {
public interface WWNStreamingDataListener {
/**
* Authorization has been revoked for a token.
@ -46,7 +46,7 @@ public interface NestStreamingDataListener {
void onError(String message);
/**
* Initial {@link TopLevelData} or an update is sent.
* Initial {@link WWNTopLevelData} or an update is sent.
*/
void onNewTopLevelData(TopLevelData data);
void onNewTopLevelData(WWNTopLevelData data);
}

View File

@ -10,17 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.listener;
package org.openhab.binding.nest.internal.wwn.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Used to track incoming data for Nest things.
* Used to track incoming data for WWN things.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface NestThingDataListener<T> {
public interface WWNThingDataListener<T> {
/**
* An initial value for the data was received or the value is send again due to a refresh.

View File

@ -10,31 +10,31 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.rest;
package org.openhab.binding.nest.internal.wwn.rest;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nest.internal.NestBindingConstants;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.data.AccessTokenData;
import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
import org.openhab.binding.nest.internal.wwn.WWNBindingConstants;
import org.openhab.binding.nest.internal.wwn.WWNUtils;
import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration;
import org.openhab.binding.nest.internal.wwn.dto.WWNAccessTokenData;
import org.openhab.binding.nest.internal.wwn.exceptions.InvalidWWNAccessTokenException;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Retrieves the Nest access token using the OAuth 2.0 protocol using pin-based authorization.
* Retrieves the WWN access token using the OAuth 2.0 protocol using pin-based authorization.
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Improve exception handling
*/
@NonNullByDefault
public class NestAuthorizer {
private final Logger logger = LoggerFactory.getLogger(NestAuthorizer.class);
public class WWNAuthorizer {
private final Logger logger = LoggerFactory.getLogger(WWNAuthorizer.class);
private final NestBridgeConfiguration config;
private final WWNAccountConfiguration config;
/**
* Create the helper class for the Nest access token. Also creates the folder
@ -42,48 +42,46 @@ public class NestAuthorizer {
*
* @param config The configuration to use for the token
*/
public NestAuthorizer(NestBridgeConfiguration config) {
public WWNAuthorizer(WWNAccountConfiguration config) {
this.config = config;
}
/**
* Get the current access token, refreshing if needed.
*
* @throws InvalidAccessTokenException thrown when the access token is invalid and could not be refreshed
* @throws InvalidWWNAccessTokenException thrown when the access token is invalid and could not be refreshed
*/
public String getNewAccessToken() throws InvalidAccessTokenException {
public String getNewAccessToken() throws InvalidWWNAccessTokenException {
try {
String pincode = config.pincode;
if (pincode == null || pincode.isBlank()) {
throw new InvalidAccessTokenException("Pincode is empty");
throw new InvalidWWNAccessTokenException("Pincode is empty");
}
// @formatter:off
StringBuilder urlBuilder = new StringBuilder(NestBindingConstants.NEST_ACCESS_TOKEN_URL)
.append("?client_id=")
.append(config.productId)
.append("&client_secret=")
.append(config.productSecret)
.append("&code=")
.append(pincode)
StringBuilder urlBuilder = new StringBuilder(WWNBindingConstants.NEST_ACCESS_TOKEN_URL) //
.append("?client_id=") //
.append(config.productId) //
.append("&client_secret=") //
.append(config.productSecret) //
.append("&code=") //
.append(pincode) //
.append("&grant_type=authorization_code");
// @formatter:on
logger.debug("Requesting access token from URL: {}", urlBuilder);
String responseContentAsString = HttpUtil.executeUrl("POST", urlBuilder.toString(), null, null,
"application/x-www-form-urlencoded", 10_000);
AccessTokenData data = NestUtils.fromJson(responseContentAsString, AccessTokenData.class);
WWNAccessTokenData data = WWNUtils.fromJson(responseContentAsString, WWNAccessTokenData.class);
logger.debug("Received: {}", data);
String accessToken = data.getAccessToken();
if (accessToken == null || accessToken.isBlank()) {
throw new InvalidAccessTokenException("Pincode to obtain access token is already used or invalid)");
throw new InvalidWWNAccessTokenException("Pincode to obtain access token is already used or invalid)");
}
return accessToken;
} catch (IOException e) {
throw new InvalidAccessTokenException("Access token request failed", e);
throw new InvalidWWNAccessTokenException("Access token request failed", e);
}
}
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.rest;
package org.openhab.binding.nest.internal.wwn.rest;
import java.io.IOException;
@ -23,16 +23,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Inserts Authorization and Cache-Control headers for requests on the streaming REST API.
* Inserts Authorization and Cache-Control headers for requests on the streaming WWN REST API.
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public class NestStreamingRequestFilter implements ClientRequestFilter {
public class WWNStreamingRequestFilter implements ClientRequestFilter {
private final String accessToken;
public NestStreamingRequestFilter(String accessToken) {
public WWNStreamingRequestFilter(String accessToken) {
this.accessToken = accessToken;
}

View File

@ -10,9 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.rest;
package org.openhab.binding.nest.internal.wwn.rest;
import static org.openhab.binding.nest.internal.NestBindingConstants.KEEP_ALIVE_MILLIS;
import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.KEEP_ALIVE_MILLIS;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -27,24 +27,24 @@ import javax.ws.rs.sse.SseEventSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.data.TopLevelStreamingData;
import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier;
import org.openhab.binding.nest.internal.listener.NestStreamingDataListener;
import org.openhab.binding.nest.internal.wwn.WWNUtils;
import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData;
import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelStreamingData;
import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException;
import org.openhab.binding.nest.internal.wwn.handler.WWNRedirectUrlSupplier;
import org.openhab.binding.nest.internal.wwn.listener.WWNStreamingDataListener;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A client that generates events based on Nest streaming REST API Server-Sent Events (SSE).
* A client that generates events based on Nest streaming WWN REST API Server-Sent Events (SSE).
*
* @author Wouter Born - Initial contribution
* @author Wouter Born - Replace polling with REST streaming
*/
@NonNullByDefault
public class NestStreamingRestClient {
public class WWNStreamingRestClient {
// Assume connection timeout when 2 keep alive message should have been received
private static final long CONNECTION_TIMEOUT_MILLIS = 2 * KEEP_ALIVE_MILLIS + KEEP_ALIVE_MILLIS / 2;
@ -55,25 +55,25 @@ public class NestStreamingRestClient {
public static final String OPEN = "open";
public static final String PUT = "put";
private final Logger logger = LoggerFactory.getLogger(NestStreamingRestClient.class);
private final Logger logger = LoggerFactory.getLogger(WWNStreamingRestClient.class);
private final String accessToken;
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final NestRedirectUrlSupplier redirectUrlSupplier;
private final WWNRedirectUrlSupplier redirectUrlSupplier;
private final ScheduledExecutorService scheduler;
private final Object startStopLock = new Object();
private final List<NestStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
private final List<WWNStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
private @Nullable ScheduledFuture<?> checkConnectionJob;
private boolean connected;
private @Nullable SseEventSource eventSource;
private long lastEventTimestamp;
private @Nullable TopLevelData lastReceivedTopLevelData;
private @Nullable WWNTopLevelData lastReceivedTopLevelData;
public NestStreamingRestClient(String accessToken, ClientBuilder clientBuilder,
SseEventSourceFactory eventSourceFactory, NestRedirectUrlSupplier redirectUrlSupplier,
public WWNStreamingRestClient(String accessToken, ClientBuilder clientBuilder,
SseEventSourceFactory eventSourceFactory, WWNRedirectUrlSupplier redirectUrlSupplier,
ScheduledExecutorService scheduler) {
this.accessToken = accessToken;
this.clientBuilder = clientBuilder;
@ -82,8 +82,8 @@ public class NestStreamingRestClient {
this.scheduler = scheduler;
}
private SseEventSource createEventSource() throws FailedResolvingNestUrlException {
Client client = clientBuilder.register(new NestStreamingRequestFilter(accessToken)).build();
private SseEventSource createEventSource() throws FailedResolvingWWNUrlException {
Client client = clientBuilder.register(new WWNStreamingRequestFilter(accessToken)).build();
SseEventSource eventSource = eventSourceFactory.newSource(client.target(redirectUrlSupplier.getRedirectUrl()));
eventSource.register(this::onEvent, this::onError);
return eventSource;
@ -122,7 +122,7 @@ public class NestStreamingRestClient {
localEventSource.open();
eventSource = localEventSource;
} catch (FailedResolvingNestUrlException e) {
} catch (FailedResolvingWWNUrlException e) {
logger.debug("Failed to resolve Nest redirect URL while opening new EventSource");
}
}
@ -175,15 +175,15 @@ public class NestStreamingRestClient {
}
}
public boolean addStreamingDataListener(NestStreamingDataListener listener) {
public boolean addStreamingDataListener(WWNStreamingDataListener listener) {
return listeners.add(listener);
}
public boolean removeStreamingDataListener(NestStreamingDataListener listener) {
public boolean removeStreamingDataListener(WWNStreamingDataListener listener) {
return listeners.remove(listener);
}
public @Nullable TopLevelData getLastReceivedTopLevelData() {
public @Nullable WWNTopLevelData getLastReceivedTopLevelData() {
return lastReceivedTopLevelData;
}
@ -214,7 +214,7 @@ public class NestStreamingRestClient {
logger.debug("Event stream opened");
} else if (PUT.equals(name)) {
logger.debug("Data has changed (or initial data sent)");
TopLevelData topLevelData = NestUtils.fromJson(data, TopLevelStreamingData.class).getData();
WWNTopLevelData topLevelData = WWNUtils.fromJson(data, WWNTopLevelStreamingData.class).getData();
lastReceivedTopLevelData = topLevelData;
listeners.forEach(listener -> listener.onNewTopLevelData(topLevelData));
} else {

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.update;
package org.openhab.binding.nest.internal.wwn.update;
import java.util.HashSet;
import java.util.List;
@ -20,36 +20,37 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.data.NestIdentifiable;
import org.openhab.binding.nest.internal.data.TopLevelData;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable;
import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData;
import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener;
/**
* Handles all Nest data updates through delegation to the {@link NestUpdateHandler} for the respective data type.
* Handles all Nest data updates through delegation to the {@link WWNUpdateHandler} for the respective data type.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class NestCompositeUpdateHandler {
public class WWNCompositeUpdateHandler {
private final Supplier<Set<String>> presentNestIdsSupplier;
private final Map<Class<?>, NestUpdateHandler<?>> updateHandlersMap = new ConcurrentHashMap<>();
private final Map<Class<?>, WWNUpdateHandler<?>> updateHandlersMap = new ConcurrentHashMap<>();
public NestCompositeUpdateHandler(Supplier<Set<String>> presentNestIdsSupplier) {
public WWNCompositeUpdateHandler(Supplier<Set<String>> presentNestIdsSupplier) {
this.presentNestIdsSupplier = presentNestIdsSupplier;
}
public <T> boolean addListener(Class<T> dataClass, NestThingDataListener<T> listener) {
public <T> boolean addListener(Class<T> dataClass, WWNThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).addListener(listener);
}
public <T> boolean addListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
public <T> boolean addListener(Class<T> dataClass, String nestId, WWNThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).addListener(nestId, listener);
}
private Set<String> findMissingNestIds(Set<NestIdentifiable> updates) {
private Set<String> findMissingNestIds(Set<WWNIdentifiable> updates) {
Set<String> nestIds = updates.stream().map(u -> u.getId()).collect(Collectors.toSet());
Set<String> missingNestIds = presentNestIdsSupplier.get();
missingNestIds.removeAll(nestIds);
@ -64,8 +65,8 @@ public class NestCompositeUpdateHandler {
return getOrCreateUpdateHandler(dataClass).getLastUpdates();
}
private Set<NestIdentifiable> getNestUpdates(TopLevelData data) {
Set<NestIdentifiable> updates = new HashSet<>();
private Set<WWNIdentifiable> getNestUpdates(WWNTopLevelData data) {
Set<WWNIdentifiable> updates = new HashSet<>();
if (data.getDevices() != null) {
if (data.getDevices().getCameras() != null) {
updates.addAll(data.getDevices().getCameras().values());
@ -84,20 +85,20 @@ public class NestCompositeUpdateHandler {
}
@SuppressWarnings("unchecked")
private <T> NestUpdateHandler<T> getOrCreateUpdateHandler(Class<T> dataClass) {
NestUpdateHandler<T> handler = (NestUpdateHandler<T>) updateHandlersMap.get(dataClass);
private <@NonNull T> WWNUpdateHandler<T> getOrCreateUpdateHandler(Class<T> dataClass) {
WWNUpdateHandler<T> handler = (WWNUpdateHandler<T>) updateHandlersMap.get(dataClass);
if (handler == null) {
handler = new NestUpdateHandler<>();
handler = new WWNUpdateHandler<>();
updateHandlersMap.put(dataClass, handler);
}
return handler;
}
@SuppressWarnings("unchecked")
public void handleUpdate(TopLevelData data) {
Set<NestIdentifiable> updates = getNestUpdates(data);
public void handleUpdate(WWNTopLevelData data) {
Set<WWNIdentifiable> updates = getNestUpdates(data);
updates.forEach(update -> {
Class<NestIdentifiable> updateClass = (Class<NestIdentifiable>) update.getClass();
Class<WWNIdentifiable> updateClass = (Class<WWNIdentifiable>) update.getClass();
getOrCreateUpdateHandler(updateClass).handleUpdate(updateClass, update.getId(), update);
});
@ -109,11 +110,11 @@ public class NestCompositeUpdateHandler {
}
}
public <T> boolean removeListener(Class<T> dataClass, NestThingDataListener<T> listener) {
public <T> boolean removeListener(Class<T> dataClass, WWNThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).removeListener(listener);
}
public <T> boolean removeListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
public <T> boolean removeListener(Class<T> dataClass, String nestId, WWNThingDataListener<T> listener) {
return getOrCreateUpdateHandler(dataClass).removeListener(nestId, listener);
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.update;
package org.openhab.binding.nest.internal.wwn.update;
import java.util.ArrayList;
import java.util.HashSet;
@ -20,9 +20,10 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener;
/**
* Handles the updates of one type of data by notifying listeners of changes and storing the update value.
@ -32,7 +33,7 @@ import org.openhab.binding.nest.internal.listener.NestThingDataListener;
* @param <T> the type of update data
*/
@NonNullByDefault
public class NestUpdateHandler<T> {
public class WWNUpdateHandler<@NonNull T> {
/**
* The ID used for listeners that subscribe to any Nest update.
@ -40,13 +41,13 @@ public class NestUpdateHandler<T> {
private static final String ANY_ID = "*";
private final Map<String, T> lastUpdates = new ConcurrentHashMap<>();
private final Map<String, Set<NestThingDataListener<T>>> listenersMap = new ConcurrentHashMap<>();
private final Map<String, Set<WWNThingDataListener<T>>> listenersMap = new ConcurrentHashMap<>();
public boolean addListener(NestThingDataListener<T> listener) {
public boolean addListener(WWNThingDataListener<T> listener) {
return addListener(ANY_ID, listener);
}
public boolean addListener(String nestId, NestThingDataListener<T> listener) {
public boolean addListener(String nestId, WWNThingDataListener<T> listener) {
return getOrCreateListeners(nestId).add(listener);
}
@ -58,21 +59,21 @@ public class NestUpdateHandler<T> {
return new ArrayList<>(lastUpdates.values());
}
private Set<NestThingDataListener<T>> getListeners(String nestId) {
Set<NestThingDataListener<T>> listeners = new HashSet<>();
Set<NestThingDataListener<T>> idListeners = listenersMap.get(nestId);
private Set<WWNThingDataListener<T>> getListeners(String nestId) {
Set<WWNThingDataListener<T>> listeners = new HashSet<>();
Set<WWNThingDataListener<T>> idListeners = listenersMap.get(nestId);
if (idListeners != null) {
listeners.addAll(idListeners);
}
Set<NestThingDataListener<T>> anyListeners = listenersMap.get(ANY_ID);
Set<WWNThingDataListener<T>> anyListeners = listenersMap.get(ANY_ID);
if (anyListeners != null) {
listeners.addAll(anyListeners);
}
return listeners;
}
private Set<NestThingDataListener<T>> getOrCreateListeners(String nestId) {
Set<NestThingDataListener<T>> listeners = listenersMap.get(nestId);
private Set<WWNThingDataListener<T>> getOrCreateListeners(String nestId) {
Set<WWNThingDataListener<T>> listeners = listenersMap.get(nestId);
if (listeners == null) {
listeners = new CopyOnWriteArraySet<>();
listenersMap.put(nestId, listeners);
@ -88,13 +89,13 @@ public class NestUpdateHandler<T> {
}
public void handleUpdate(Class<T> dataClass, String nestId, T update) {
T lastUpdate = getLastUpdate(nestId);
final @Nullable T lastUpdate = getLastUpdate(nestId);
lastUpdates.put(nestId, update);
notifyListeners(nestId, lastUpdate, update);
}
private void notifyListeners(String nestId, @Nullable T lastUpdate, T update) {
Set<NestThingDataListener<T>> listeners = getListeners(nestId);
Set<WWNThingDataListener<T>> listeners = getListeners(nestId);
if (lastUpdate == null) {
listeners.forEach(l -> l.onNewData(update));
} else if (!lastUpdate.equals(update)) {
@ -102,11 +103,11 @@ public class NestUpdateHandler<T> {
}
}
public boolean removeListener(NestThingDataListener<T> listener) {
public boolean removeListener(WWNThingDataListener<T> listener) {
return removeListener(ANY_ID, listener);
}
public boolean removeListener(String nestId, NestThingDataListener<T> listener) {
public boolean removeListener(String nestId, WWNThingDataListener<T> listener) {
return getOrCreateListeners(nestId).remove(listener);
}

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:nest:sdm_account">
<parameter-group name="sdm">
<label>SDM</label>
<description>The parameters used when communicating with the SDM API</description>
</parameter-group>
<parameter-group name="pubsub">
<label>Pub/Sub</label>
<description>The parameters used when communicating with the Pub/Sub API</description>
</parameter-group>
<parameter name="sdmProjectId" type="text" required="true" groupName="sdm">
<label>Project ID</label>
<description>The UUID that identifies the SDM project in the SDM "Device Access Console"</description>
</parameter>
<parameter name="sdmClientId" type="text" required="true" groupName="sdm">
<label>Client ID</label>
<description>Identifies the OAuth 2.0 client used for accessing the SDM project</description>
</parameter>
<parameter name="sdmClientSecret" type="text" required="true" groupName="sdm">
<context>password</context>
<label>Client Secret</label>
<description>The OAuth 2.0 client secret used for accessing the SDM project</description>
</parameter>
<parameter name="sdmAuthorizationCode" type="text" groupName="sdm">
<label>Authorization Code</label>
<description><![CDATA[The one time authorization code used to retrieve the refresh and access token used with the SDM API. The code is obtained by following the instructions at the following URL in your browser:<br><br>https://nestservices.google.com/partnerconnections/{{ProjectID}}/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&access_type=offline&prompt=consent&client_id={{ClientID}}&response_type=code&scope=https://www.googleapis.com/auth/sdm.service]]></description>
</parameter>
<parameter name="pubsubProjectId" type="text" groupName="pubsub">
<label>Project ID</label>
<description>Identifies the Google Cloud Platform project where the Pub/Sub subscription is created</description>
</parameter>
<parameter name="pubsubSubscriptionId" type="text" groupName="pubsub">
<label>Subscription ID</label>
<description>Identifies the subscription that is created for subscribing to SDM Pub/Sub events</description>
</parameter>
<parameter name="pubsubClientId" type="text" groupName="pubsub">
<label>Client ID</label>
<description>Identifies the OAuth 2.0 client used for accessing the Pub/Sub subscription</description>
</parameter>
<parameter name="pubsubClientSecret" type="text" groupName="pubsub">
<context>password</context>
<label>Client Secret</label>
<description>The OAuth 2.0 client secret used for accessing the Pub/Sub subscription</description>
</parameter>
<parameter name="pubsubAuthorizationCode" type="text" groupName="pubsub">
<label>Authorization Code</label>
<description><![CDATA[The one time authorization code used to retrieve the refresh and access token used with the Pub/Sub API. The code is obtained by following the instructions at the following URL in your browser:<br><br>https://accounts.google.com/o/oauth2/auth?client_id={{ClientID}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/pubsub]]></description>
</parameter>
</config-description>
<config-description uri="thing-type:nest:sdm_device">
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
</parameter>
<parameter name="refreshInterval" type="integer" min="30" step="1" unit="s">
<label>Refresh Interval</label>
<description>This is refresh interval in seconds to update the Nest device information</description>
<default>300</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
<config-description uri="channel-type:nest:sdm_camera_image">
<parameter name="imageWidth" type="integer" min="1" step="1">
<label>Image Width</label>
<description>The width in pixels used for generating event images. A default value of 480 pixels is used if not
configured.</description>
<unitLabel>px</unitLabel>
</parameter>
<parameter name="imageHeight" type="integer" min="1" step="1">
<label>Image Height</label>
<description>The height in pixels used for generating event images. This parameter is ignored when the image width
parameter is also configured.</description>
<unitLabel>px</unitLabel>
</parameter>
</config-description>
<config-description uri="channel-type:nest:sdm_fan_timer_mode">
<parameter name="fanTimerDuration" type="integer" min="1" max="43200" step="1" unit="s">
<label>Fan Timer Duration</label>
<description>Specifies the length of time in seconds that the timer is set to run.</description>
<default>900</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -4,14 +4,10 @@
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:nest:account">
<config-description uri="thing-type:nest:wwn_account">
<parameter-group name="oauth">
<label>Nest API OAuth</label>
<description>The OAuth parameters used when communicating with the Nest API</description>
</parameter-group>
<parameter-group name="binding">
<label>Binding Settings</label>
<description>Local settings</description>
<label>WWN API OAuth</label>
<description>The OAuth parameters used when communicating with the WWN API</description>
</parameter-group>
<parameter name="productId" type="text" groupName="oauth" required="true">
@ -22,30 +18,19 @@
<label>Product Secret</label>
<description>The product secret from the Nest product page</description>
</parameter>
<parameter name="pincode" type="text" groupName="oauth">
<label>Pincode</label>
<description>The single use pincode for obtaining an OAuth access token.
Get the pincode by accepting to the terms
shown at the product authorization URL.
This value is automatically reset when the access token has been obtained</description>
</parameter>
<parameter name="accessToken" type="text" groupName="oauth">
<parameter name="accessToken" type="text" groupName="oauth" required="true">
<label>Access Token</label>
<description>The access token used for authenticating to the Nest API.
It is automatically obtained from Nest when the
value is empty and
a valid pincode parameter is entered</description>
<advanced>true</advanced>
<description>The access token used for authenticating to the WWN API</description>
</parameter>
</config-description>
<config-description uri="thing-type:nest:device">
<config-description uri="thing-type:nest:wwn_device">
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
</parameter>
</config-description>
<config-description uri="thing-type:nest:structure">
<config-description uri="thing-type:nest:wwn_structure">
<parameter name="structureId" type="text" required="true">
<label>Structure ID</label>
</parameter>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="sdm_account">
<label>Nest SDM Account</label>
<description>An account for using the Smart Device Management (SDM) API</description>
<config-description-ref uri="thing-type:nest:sdm_account"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sdm_camera" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="sdm_account"/>
</supported-bridge-type-refs>
<label>Nest Camera</label>
<description>A Nest Camera registered with your SDM account</description>
<channel-groups>
<channel-group id="motion_event" typeId="SDMMotionEvent"/>
<channel-group id="person_event" typeId="SDMPersonEvent"/>
<channel-group id="sound_event" typeId="SDMSoundEvent"/>
<channel-group id="live_stream" typeId="SDMLiveStream"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:sdm_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Camera -->
<channel-group-type id="SDMChimeEvent">
<label>Chime Event</label>
<description>Information about the last chime event</description>
<channels>
<channel id="image" typeId="SDMCameraEventImage">
<label>Chime Event Image</label>
<description>Static image based on a chime event</description>
</channel>
<channel id="timestamp" typeId="SDMCameraEventTimestamp">
<label>Chime Event Timestamp</label>
<description>The last time that the door chime was pressed</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="SDMMotionEvent">
<label>Motion Event</label>
<description>Information about the last motion event</description>
<channels>
<channel id="image" typeId="SDMCameraEventImage">
<label>Motion Event Image</label>
<description>Static image based on a motion event</description>
</channel>
<channel id="timestamp" typeId="SDMCameraEventTimestamp">
<label>Motion Event Timestamp</label>
<description>The last time that motion was detected</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="SDMPersonEvent">
<label>Person Event</label>
<description>Information about the last person event</description>
<channels>
<channel id="image" typeId="SDMCameraEventImage">
<label>Person Event Image</label>
<description>Static image based on a person event</description>
</channel>
<channel id="timestamp" typeId="SDMCameraEventTimestamp">
<label>Person Event Timestamp</label>
<description>The last time that a person was detected</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="SDMSoundEvent">
<label>Sound Event</label>
<description>Information about the last sound event</description>
<channels>
<channel id="image" typeId="SDMCameraEventImage">
<label>Sound Event Image</label>
<description>Static image based on a sound event</description>
</channel>
<channel id="timestamp" typeId="SDMCameraEventTimestamp">
<label>Sound Event Timestamp</label>
<description>The last time that a sound was detected</description>
</channel>
</channels>
</channel-group-type>
<channel-type id="SDMCameraEventImage">
<item-type>Image</item-type>
<label>Image</label>
<description>Static image based on a event</description>
<state readOnly="true"/>
<config-description-ref uri="channel-type:nest:sdm_camera_image"/>
</channel-type>
<channel-type id="SDMCameraEventTimestamp">
<item-type>DateTime</item-type>
<label>Timestamp</label>
<description>The time that the event occurred</description>
<state readOnly="true"/>
</channel-type>
<channel-group-type id="SDMLiveStream">
<label>Live Stream</label>
<description>Information for accessing the live stream</description>
<channels>
<channel id="url" typeId="SDMLiveStreamUrl"/>
<channel id="expiration_timestamp" typeId="SDMLiveStreamExpirationTimestamp"/>
<channel id="current_token" typeId="SDMLiveStreamCurrentToken"/>
<channel id="extension_token" typeId="SDMLiveStreamExtensionToken"/>
</channels>
</channel-group-type>
<channel-type id="SDMLiveStreamUrl">
<item-type>String</item-type>
<label>Live Stream URL</label>
<description>The RTSP video stream URL for the most recent event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SDMLiveStreamExpirationTimestamp">
<item-type>DateTime</item-type>
<label>Live Stream Expiration Timestamp</label>
<description>Live stream token expiration time</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SDMLiveStreamCurrentToken">
<item-type>String</item-type>
<label>Live Stream Current Token</label>
<description>Live stream current token value</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SDMLiveStreamExtensionToken">
<item-type>String</item-type>
<label>Live Stream Extension Token</label>
<description>Live stream token extension value</description>
<state readOnly="true"/>
</channel-type>
<!-- Thermostat -->
<channel-type id="SDMAmbientHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Ambient Humidity</label>
<description>Lists the current ambient humidity percentage from the thermostat</description>
<category>Humidity</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="SDMAmbientTemperature">
<item-type>Number:Temperature</item-type>
<label>Ambient Temperature</label>
<description>Lists the current ambient temperature from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="SDMCurrentEcoMode">
<item-type>String</item-type>
<label>Current Eco Mode</label>
<description>Lists the current eco mode from the thermostat</description>
<state>
<options>
<option value="OFF">off</option>
<option value="MANUAL_ECO">manual eco</option>
</options>
</state>
</channel-type>
<channel-type id="SDMCurrentMode">
<item-type>String</item-type>
<label>Current Mode</label>
<description>Lists the current mode from the thermostat</description>
<state>
<options>
<option value="OFF">off</option>
<option value="HEAT">heating</option>
<option value="COOL">cooling</option>
<option value="HEATCOOL">heat/cool</option>
</options>
</state>
</channel-type>
<channel-type id="SDMFanTimerMode">
<item-type>Switch</item-type>
<label>Fan Timer Mode</label>
<description>Lists the current fan timer mode</description>
<config-description-ref uri="channel-type:nest:sdm_fan_timer_mode"/>
</channel-type>
<channel-type id="SDMFanTimerTimeout">
<item-type>DateTime</item-type>
<label>Fan Timer Timeout</label>
<description>Timestamp at which timer mode turns OFF</description>
</channel-type>
<channel-type id="SDMHVACStatus">
<item-type>String</item-type>
<label>HVAC Status</label>
<description>Provides the thermostat HVAC Status</description>
<state readOnly="true">
<options>
<option value="OFF">off</option>
<option value="HEATING">heating</option>
<option value="COOLING">cooling</option>
</options>
</state>
</channel-type>
<channel-type id="SDMMaximumTemperature">
<item-type>Number:Temperature</item-type>
<label>Maximum Temperature Setting</label>
<description>Lists the maximum temperature setting from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="SDMMinimumTemperature">
<item-type>Number:Temperature</item-type>
<label>Minimum Temperature Setting</label>
<description>Lists the minimum temperature setting from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="SDMTargetTemperature">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<description>Lists the target temperature setting from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="SDMTemperatureCool">
<item-type>Number:Temperature</item-type>
<label>Cool Temperature</label>
<description>Lists the cool temperature setting from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="SDMTemperatureHeat">
<item-type>Number:Temperature</item-type>
<label>Heat Temperature</label>
<description>Lists the heat temperature setting from the thermostat</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sdm_display" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="sdm_account"/>
</supported-bridge-type-refs>
<label>Nest Display</label>
<description>A Nest Display registered with your SDM account</description>
<channel-groups>
<channel-group id="motion_event" typeId="SDMMotionEvent"/>
<channel-group id="person_event" typeId="SDMPersonEvent"/>
<channel-group id="sound_event" typeId="SDMSoundEvent"/>
<channel-group id="live_stream" typeId="SDMLiveStream"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:sdm_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sdm_doorbell" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="sdm_account"/>
</supported-bridge-type-refs>
<label>Nest Doorbell</label>
<description>A Nest Doorbell registered with your SDM account</description>
<channel-groups>
<channel-group id="chime_event" typeId="SDMChimeEvent"/>
<channel-group id="motion_event" typeId="SDMMotionEvent"/>
<channel-group id="person_event" typeId="SDMPersonEvent"/>
<channel-group id="sound_event" typeId="SDMSoundEvent"/>
<channel-group id="live_stream" typeId="SDMLiveStream"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:sdm_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sdm_thermostat" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="sdm_account"/>
</supported-bridge-type-refs>
<label>Nest Thermostat</label>
<description>A Thermostat to control the various aspects of the house's HVAC system</description>
<channels>
<channel id="ambient_humidity" typeId="SDMAmbientHumidity"/>
<channel id="ambient_temperature" typeId="SDMAmbientTemperature"/>
<channel id="fan_timer_mode" typeId="SDMFanTimerMode"/>
<channel id="fan_timer_timeout" typeId="SDMFanTimerTimeout"/>
<channel id="temperature_heat" typeId="SDMTemperatureHeat"/>
<channel id="temperature_cool" typeId="SDMTemperatureCool"/>
<channel id="current_mode" typeId="SDMCurrentMode"/>
<channel id="current_eco_mode" typeId="SDMCurrentEcoMode"/>
<channel id="target_temperature" typeId="SDMTargetTemperature"/>
<channel id="minimum_temperature" typeId="SDMMinimumTemperature"/>
<channel id="maximum_temperature" typeId="SDMMaximumTemperature"/>
<channel id="hvac_status" typeId="SDMHVACStatus"/>
</channels>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:sdm_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Nest Thermostat</label>
<description>A Thermostat to control the various aspects of the house's HVAC system</description>
<channels>
<channel id="temperature" typeId="Temperature"/>
<channel id="humidity" typeId="Humidity"/>
<channel id="mode" typeId="Mode"/>
<channel id="previous_mode" typeId="PreviousMode"/>
<channel id="state" typeId="State"/>
<channel id="set_point" typeId="SetPoint"/>
<channel id="max_set_point" typeId="MaxSetPoint"/>
<channel id="min_set_point" typeId="MinSetPoint"/>
<channel id="can_heat" typeId="CanHeat"/>
<channel id="can_cool" typeId="CanCool"/>
<channel id="fan_timer_active" typeId="FanTimerActive"/>
<channel id="fan_timer_duration" typeId="FanTimerDuration"/>
<channel id="fan_timer_timeout" typeId="FanTimerTimeout"/>
<channel id="has_fan" typeId="HasFan"/>
<channel id="has_leaf" typeId="HasLeaf"/>
<channel id="sunlight_correction_enabled" typeId="SunlightCorrectionEnabled"/>
<channel id="sunlight_correction_active" typeId="SunlightCorrectionActive"/>
<channel id="using_emergency_heat" typeId="UsingEmergencyHeat"/>
<channel id="eco_max_set_point" typeId="EcoMaxSetPoint"/>
<channel id="eco_min_set_point" typeId="EcoMinSetPoint"/>
<channel id="locked" typeId="Locked"/>
<channel id="locked_max_set_point" typeId="LockedMaxSetPoint"/>
<channel id="locked_min_set_point" typeId="LockedMinSetPoint"/>
<channel id="time_to_target" typeId="TimeToTarget"/>
<channel id="last_connection" typeId="LastConnection"/>
</channels>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -4,9 +4,10 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="account">
<label>Nest Account</label>
<description>An account for using the Nest REST API</description>
<config-description-ref uri="thing-type:nest:account"/>
<bridge-type id="wwn_account">
<label>Nest WWN Account</label>
<description>An account for using the Works with Nest (WWN) API</description>
<config-description-ref uri="thing-type:nest:wwn_account"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -4,17 +4,17 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="camera">
<thing-type id="wwn_camera" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
<bridge-type-ref id="wwn_account"/>
</supported-bridge-type-refs>
<label>Nest Cam</label>
<description>A Nest Cam registered with your account</description>
<description>A Nest Camera registered with your WWN account</description>
<channel-groups>
<channel-group id="camera" typeId="Camera"/>
<channel-group id="last_event" typeId="CameraEvent">
<channel-group id="camera" typeId="WWNCamera"/>
<channel-group id="last_event" typeId="WWNCameraEvent">
<label>Last Event</label>
<description>Information about the last camera event (requires Nest Aware subscription)</description>
</channel-group>
@ -26,6 +26,7 @@
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
<config-description-ref uri="thing-type:nest:wwn_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Common -->
<channel-type id="LastConnection" advanced="true">
<channel-type id="WWNLastConnection" advanced="true">
<item-type>DateTime</item-type>
<label>Last Connection</label>
<description>Timestamp of the last successful interaction with Nest</description>
@ -13,7 +13,7 @@
</channel-type>
<!-- Structure -->
<channel-type id="Away">
<channel-type id="WWNAway">
<item-type>String</item-type>
<label>Away</label>
<description>Away state of the structure</description>
@ -25,39 +25,39 @@
</state>
</channel-type>
<channel-type id="CountryCode" advanced="true">
<channel-type id="WWNCountryCode" advanced="true">
<item-type>String</item-type>
<label>Country Code</label>
<description>Country code of the structure</description>
</channel-type>
<channel-type id="PostalCode" advanced="true">
<channel-type id="WWNPostalCode" advanced="true">
<item-type>String</item-type>
<label>Postal Code</label>
<description>Postal code of the structure</description>
</channel-type>
<channel-type id="TimeZone">
<channel-type id="WWNTimeZone">
<item-type>String</item-type>
<label>Time Zone</label>
<description>The time zone for the structure</description>
</channel-type>
<channel-type id="PeakPeriodStartTime" advanced="true">
<channel-type id="WWNPeakPeriodStartTime" advanced="true">
<item-type>DateTime</item-type>
<label>Peak Period Start Time</label>
<description>Peak period start for the Rush Hour Rewards program</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="PeakPeriodEndTime" advanced="true">
<channel-type id="WWNPeakPeriodEndTime" advanced="true">
<item-type>DateTime</item-type>
<label>Peak Period End Time</label>
<description>Peak period end for the Rush Hour Rewards program</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="EtaBegin" advanced="true">
<channel-type id="WWNEtaBegin" advanced="true">
<item-type>DateTime</item-type>
<label>ETA</label>
<description>
@ -66,14 +66,14 @@
</description>
</channel-type>
<channel-type id="RushHourRewardsEnrollment">
<channel-type id="WWNRushHourRewardsEnrollment">
<item-type>Switch</item-type>
<label>Rush Hour Rewards</label>
<description>If rush hour rewards system is enabled or not</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SecurityState">
<channel-type id="WWNSecurityState">
<item-type>String</item-type>
<label>Security State</label>
<description>Security state of the structure</description>
@ -86,166 +86,166 @@
</channel-type>
<!-- Camera -->
<channel-group-type id="Camera">
<channel-group-type id="WWNCamera">
<label>Camera</label>
<description>Information about the camera</description>
<channels>
<channel id="streaming" typeId="Streaming"/>
<channel id="audio_input_enabled" typeId="AudioInputEnabled"/>
<channel id="public_share_enabled" typeId="PublicShareEnabled"/>
<channel id="video_history_enabled" typeId="VideoHistoryEnabled"/>
<channel id="app_url" typeId="AppUrl"/>
<channel id="snapshot_url" typeId="SnapshotUrl"/>
<channel id="public_share_url" typeId="PublicShareUrl"/>
<channel id="web_url" typeId="WebUrl"/>
<channel id="last_online_change" typeId="LastOnlineChange"/>
<channel id="streaming" typeId="WWNStreaming"/>
<channel id="audio_input_enabled" typeId="WWNAudioInputEnabled"/>
<channel id="public_share_enabled" typeId="WWNPublicShareEnabled"/>
<channel id="video_history_enabled" typeId="WWNVideoHistoryEnabled"/>
<channel id="app_url" typeId="WWNAppUrl"/>
<channel id="snapshot_url" typeId="WWNSnapshotUrl"/>
<channel id="public_share_url" typeId="WWNPublicShareUrl"/>
<channel id="web_url" typeId="WWNWebUrl"/>
<channel id="last_online_change" typeId="WWNLastOnlineChange"/>
</channels>
</channel-group-type>
<channel-type id="AudioInputEnabled" advanced="true">
<channel-type id="WWNAudioInputEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Audio Input Enabled</label>
<description>If the audio input is enabled for this camera</description>
</channel-type>
<channel-type id="VideoHistoryEnabled" advanced="true">
<channel-type id="WWNVideoHistoryEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Video History Enabled</label>
<description>If the video history is enabled for this camera</description>
</channel-type>
<channel-type id="PublicShareEnabled" advanced="true">
<channel-type id="WWNPublicShareEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Public Share Enabled</label>
<description>If the public sharing of this camera is enabled</description>
</channel-type>
<channel-type id="Streaming">
<channel-type id="WWNStreaming">
<item-type>Switch</item-type>
<label>Streaming</label>
<description>If the camera is currently streaming</description>
</channel-type>
<channel-type id="WebUrl">
<channel-type id="WWNWebUrl">
<item-type>String</item-type>
<label>Web URL</label>
<description>The web URL for the camera, allows you to see the camera in a web page</description>
</channel-type>
<channel-type id="PublicShareUrl">
<channel-type id="WWNPublicShareUrl">
<item-type>String</item-type>
<label>Public Share URL</label>
<description>The publicly available URL for the camera</description>
</channel-type>
<channel-type id="SnapshotUrl" advanced="true">
<channel-type id="WWNSnapshotUrl" advanced="true">
<item-type>String</item-type>
<label>Snapshot URL</label>
<description>The URL showing a snapshot of the camera</description>
</channel-type>
<channel-type id="AppUrl" advanced="true">
<channel-type id="WWNAppUrl" advanced="true">
<item-type>String</item-type>
<label>App URL</label>
<description>The app URL for the camera, allows you to see the camera in an app</description>
</channel-type>
<channel-type id="LastOnlineChange" advanced="true">
<channel-type id="WWNLastOnlineChange" advanced="true">
<item-type>DateTime</item-type>
<label>Last Online Change</label>
<description>Timestamp of the last online status change</description>
<state readOnly="true"/>
</channel-type>
<channel-group-type id="CameraEvent">
<channel-group-type id="WWNCameraEvent">
<label>Camera Event</label>
<description>Information about the camera event</description>
<channels>
<channel id="has_motion" typeId="CameraEventHasMotion"/>
<channel id="has_sound" typeId="CameraEventHasSound"/>
<channel id="has_person" typeId="CameraEventHasPerson"/>
<channel id="start_time" typeId="CameraEventStartTime"/>
<channel id="end_time" typeId="CameraEventEndTime"/>
<channel id="urls_expire_time" typeId="CameraEventUrlsExpireTime"/>
<channel id="animated_image_url" typeId="CameraEventAnimatedImageUrl"/>
<channel id="app_url" typeId="CameraEventAppUrl"/>
<channel id="image_url" typeId="CameraEventImageUrl"/>
<channel id="web_url" typeId="CameraEventWebUrl"/>
<channel id="activity_zones" typeId="CameraEventActivityZones"/>
<channel id="has_motion" typeId="WWNCameraEventHasMotion"/>
<channel id="has_sound" typeId="WWNCameraEventHasSound"/>
<channel id="has_person" typeId="WWNCameraEventHasPerson"/>
<channel id="start_time" typeId="WWNCameraEventStartTime"/>
<channel id="end_time" typeId="WWNCameraEventEndTime"/>
<channel id="urls_expire_time" typeId="WWNCameraEventUrlsExpireTime"/>
<channel id="animated_image_url" typeId="WWNCameraEventAnimatedImageUrl"/>
<channel id="app_url" typeId="WWNCameraEventAppUrl"/>
<channel id="image_url" typeId="WWNCameraEventImageUrl"/>
<channel id="web_url" typeId="WWNCameraEventWebUrl"/>
<channel id="activity_zones" typeId="WWNCameraEventActivityZones"/>
</channels>
</channel-group-type>
<channel-type id="CameraEventHasSound" advanced="true">
<channel-type id="WWNCameraEventHasSound" advanced="true">
<item-type>Switch</item-type>
<label>Has Sound</label>
<description>If sound was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventHasMotion" advanced="true">
<channel-type id="WWNCameraEventHasMotion" advanced="true">
<item-type>Switch</item-type>
<label>Has Motion</label>
<description>If motion was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventHasPerson" advanced="true">
<channel-type id="WWNCameraEventHasPerson" advanced="true">
<item-type>Switch</item-type>
<label>Has Person</label>
<description>If a person was detected in the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventStartTime" advanced="true">
<channel-type id="WWNCameraEventStartTime" advanced="true">
<item-type>DateTime</item-type>
<label>Start Time</label>
<description>Timestamp when the camera event started</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventEndTime" advanced="true">
<channel-type id="WWNCameraEventEndTime" advanced="true">
<item-type>DateTime</item-type>
<label>End Time</label>
<description>Timestamp when the camera event ended</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventUrlsExpireTime" advanced="true">
<channel-type id="WWNCameraEventUrlsExpireTime" advanced="true">
<item-type>DateTime</item-type>
<label>URLs Expire Time</label>
<description>Timestamp when the camera event URLs expire</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventWebUrl" advanced="true">
<channel-type id="WWNCameraEventWebUrl" advanced="true">
<item-type>String</item-type>
<label>Web URL</label>
<description>The web URL for the camera event, allows you to see the camera event in a web page</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventAppUrl" advanced="true">
<channel-type id="WWNCameraEventAppUrl" advanced="true">
<item-type>String</item-type>
<label>App URL</label>
<description>The app URL for the camera event, allows you to see the camera event in an app</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventImageUrl" advanced="true">
<channel-type id="WWNCameraEventImageUrl" advanced="true">
<item-type>String</item-type>
<label>Image URL</label>
<description>The URL showing an image for the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventAnimatedImageUrl" advanced="true">
<channel-type id="WWNCameraEventAnimatedImageUrl" advanced="true">
<item-type>String</item-type>
<label>Animated Image URL</label>
<description>The URL showing an animated image for the camera event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CameraEventActivityZones" advanced="true">
<channel-type id="WWNCameraEventActivityZones" advanced="true">
<item-type>String</item-type>
<label>Activity Zones</label>
<description>Identifiers for activity zones that detected the event (comma separated)</description>
@ -253,7 +253,7 @@
</channel-type>
<!-- Smoke detector -->
<channel-type id="UiColorState" advanced="true">
<channel-type id="WWNUiColorState" advanced="true">
<item-type>String</item-type>
<label>UI Color State</label>
<description>Current color state of the protect</description>
@ -267,7 +267,7 @@
</state>
</channel-type>
<channel-type id="CoAlarmState">
<channel-type id="WWNCoAlarmState">
<item-type>String</item-type>
<label>CO Alarm State</label>
<description>Carbon monoxide alarm state</description>
@ -280,7 +280,7 @@
</state>
</channel-type>
<channel-type id="SmokeAlarmState">
<channel-type id="WWNSmokeAlarmState">
<item-type>String</item-type>
<label>Smoke Alarm State</label>
<description>Smoke alarm state</description>
@ -293,14 +293,14 @@
</state>
</channel-type>
<channel-type id="ManualTestActive" advanced="true">
<channel-type id="WWNManualTestActive" advanced="true">
<item-type>Switch</item-type>
<label>Manual Test Active</label>
<description>If the manual test is currently active</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="LastManualTestTime" advanced="true">
<channel-type id="WWNLastManualTestTime" advanced="true">
<item-type>DateTime</item-type>
<label>Last Manual Test Time</label>
<description>Timestamp of the last successful manual test</description>
@ -308,7 +308,7 @@
</channel-type>
<!-- Thermostat -->
<channel-type id="Temperature">
<channel-type id="WWNTemperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature</description>
@ -316,7 +316,7 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="SetPoint">
<channel-type id="WWNSetPoint">
<item-type>Number:Temperature</item-type>
<label>Set Point</label>
<description>The set point temperature</description>
@ -324,7 +324,7 @@
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="MaxSetPoint">
<channel-type id="WWNMaxSetPoint">
<item-type>Number:Temperature</item-type>
<label>Max Set Point</label>
<description>The max set point temperature</description>
@ -332,7 +332,7 @@
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="MinSetPoint">
<channel-type id="WWNMinSetPoint">
<item-type>Number:Temperature</item-type>
<label>Min Set Point</label>
<description>The min set point temperature</description>
@ -340,7 +340,7 @@
<state pattern="%.1f %unit%" step="0.5"/>
</channel-type>
<channel-type id="EcoMaxSetPoint" advanced="true">
<channel-type id="WWNEcoMaxSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Eco Max Set Point</label>
<description>The eco range max set point temperature</description>
@ -348,7 +348,7 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="EcoMinSetPoint" advanced="true">
<channel-type id="WWNEcoMinSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Eco Min Set Point</label>
<description>The eco range min set point temperature</description>
@ -356,7 +356,7 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="LockedMaxSetPoint" advanced="true">
<channel-type id="WWNLockedMaxSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Locked Max Set Point</label>
<description>The locked range max set point temperature</description>
@ -364,7 +364,7 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="LockedMinSetPoint" advanced="true">
<channel-type id="WWNLockedMinSetPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Locked Min Set Point</label>
<description>The locked range min set point temperature</description>
@ -372,14 +372,14 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="Locked" advanced="true">
<channel-type id="WWNLocked" advanced="true">
<item-type>Switch</item-type>
<label>Locked</label>
<description>If the thermostat has the temperature locked to only be within a set range</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="Mode">
<channel-type id="WWNMode">
<item-type>String</item-type>
<label>Mode</label>
<description>Current mode of the Nest thermostat</description>
@ -394,7 +394,7 @@
</state>
</channel-type>
<channel-type id="PreviousMode" advanced="true">
<channel-type id="WWNPreviousMode" advanced="true">
<item-type>String</item-type>
<label>Previous Mode</label>
<description>The previous mode of the Nest thermostat</description>
@ -409,7 +409,7 @@
</state>
</channel-type>
<channel-type id="State" advanced="true">
<channel-type id="WWNState" advanced="true">
<item-type>String</item-type>
<label>State</label>
<description>The active state of the Nest thermostat</description>
@ -422,7 +422,7 @@
</state>
</channel-type>
<channel-type id="Humidity">
<channel-type id="WWNHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Indicates the current relative humidity</description>
@ -430,35 +430,35 @@
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="TimeToTarget">
<channel-type id="WWNTimeToTarget">
<item-type>Number:Time</item-type>
<label>Time to Target</label>
<description>Time left to the target temperature approximately</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="CanHeat" advanced="true">
<channel-type id="WWNCanHeat" advanced="true">
<item-type>Switch</item-type>
<label>Can Heat</label>
<description>If the thermostat can actually turn on heating</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="CanCool" advanced="true">
<channel-type id="WWNCanCool" advanced="true">
<item-type>Switch</item-type>
<label>Can Cool</label>
<description>If the thermostat can actually turn on cooling</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="FanTimerActive" advanced="true">
<channel-type id="WWNFanTimerActive" advanced="true">
<item-type>Switch</item-type>
<label>Fan Timer Active</label>
<description>If the fan timer is engaged</description>
<state/>
</channel-type>
<channel-type id="FanTimerDuration" advanced="true">
<channel-type id="WWNFanTimerDuration" advanced="true">
<item-type>Number:Time</item-type>
<label>Fan Timer Duration</label>
<description>Length of time that the fan is set to run</description>
@ -476,45 +476,46 @@
</state>
</channel-type>
<channel-type id="FanTimerTimeout" advanced="true">
<channel-type id="WWNFanTimerTimeout" advanced="true">
<item-type>DateTime</item-type>
<label>Fan Timer Timeout</label>
<description>Timestamp when the fan stops running</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="HasFan" advanced="true">
<channel-type id="WWNHasFan" advanced="true">
<item-type>Switch</item-type>
<label>Has Fan</label>
<description>If the thermostat can control the fan</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="HasLeaf" advanced="true">
<channel-type id="WWNHasLeaf" advanced="true">
<item-type>Switch</item-type>
<label>Has Leaf</label>
<description>If the thermostat is currently in a leaf mode</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SunlightCorrectionEnabled" advanced="true">
<channel-type id="WWNSunlightCorrectionEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Sunlight Correction Enabled</label>
<description>If sunlight correction is enabled</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="SunlightCorrectionActive" advanced="true">
<channel-type id="WWNSunlightCorrectionActive" advanced="true">
<item-type>Switch</item-type>
<label>Sunlight Correction Active</label>
<description>If sunlight correction is active</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="UsingEmergencyHeat" advanced="true">
<channel-type id="WWNUsingEmergencyHeat" advanced="true">
<item-type>Switch</item-type>
<label>Using Emergency Heat</label>
<description>If the system is currently using emergency heat</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -4,22 +4,22 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="smoke_detector">
<thing-type id="wwn_smoke_detector" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
<bridge-type-ref id="wwn_account"/>
</supported-bridge-type-refs>
<label>Nest Protect</label>
<description>The smoke detector/Nest Protect for the account</description>
<channels>
<channel id="ui_color_state" typeId="UiColorState"/>
<channel id="ui_color_state" typeId="WWNUiColorState"/>
<channel id="low_battery" typeId="system.low-battery"/>
<channel id="co_alarm_state" typeId="CoAlarmState"/>
<channel id="smoke_alarm_state" typeId="SmokeAlarmState"/>
<channel id="manual_test_active" typeId="ManualTestActive"/>
<channel id="last_manual_test_time" typeId="LastManualTestTime"/>
<channel id="last_connection" typeId="LastConnection"/>
<channel id="co_alarm_state" typeId="WWNCoAlarmState"/>
<channel id="smoke_alarm_state" typeId="WWNSmokeAlarmState"/>
<channel id="manual_test_active" typeId="WWNManualTestActive"/>
<channel id="last_manual_test_time" typeId="WWNLastManualTestTime"/>
<channel id="last_connection" typeId="WWNLastConnection"/>
</channels>
<properties>
@ -28,6 +28,7 @@
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:device"/>
<config-description-ref uri="thing-type:nest:wwn_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -4,9 +4,9 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="structure">
<thing-type id="wwn_structure" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
<bridge-type-ref id="wwn_account"/>
</supported-bridge-type-refs>
<label>Nest Structure</label>
@ -15,17 +15,17 @@
structure if you have more than one house</description>
<channels>
<channel id="country_code" typeId="CountryCode"/>
<channel id="postal_code" typeId="PostalCode"/>
<channel id="time_zone" typeId="TimeZone"/>
<channel id="peak_period_start_time" typeId="PeakPeriodStartTime"/>
<channel id="peak_period_end_time" typeId="PeakPeriodEndTime"/>
<channel id="rush_hour_rewards_enrollment" typeId="RushHourRewardsEnrollment"/>
<channel id="eta_begin" typeId="EtaBegin"/>
<channel id="co_alarm_state" typeId="CoAlarmState"/>
<channel id="smoke_alarm_state" typeId="SmokeAlarmState"/>
<channel id="security_state" typeId="SecurityState"/>
<channel id="away" typeId="Away"/>
<channel id="country_code" typeId="WWNCountryCode"/>
<channel id="postal_code" typeId="WWNPostalCode"/>
<channel id="time_zone" typeId="WWNTimeZone"/>
<channel id="peak_period_start_time" typeId="WWNPeakPeriodStartTime"/>
<channel id="peak_period_end_time" typeId="WWNPeakPeriodEndTime"/>
<channel id="rush_hour_rewards_enrollment" typeId="WWNRushHourRewardsEnrollment"/>
<channel id="eta_begin" typeId="WWNEtaBegin"/>
<channel id="co_alarm_state" typeId="WWNCoAlarmState"/>
<channel id="smoke_alarm_state" typeId="WWNSmokeAlarmState"/>
<channel id="security_state" typeId="WWNSecurityState"/>
<channel id="away" typeId="WWNAway"/>
</channels>
<properties>
@ -34,7 +34,7 @@
<representation-property>structureId</representation-property>
<config-description-ref uri="thing-type:nest:structure"/>
<config-description-ref uri="thing-type:nest:wwn_structure"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="wwn_thermostat" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="wwn_account"/>
</supported-bridge-type-refs>
<label>Nest Thermostat</label>
<description>A Thermostat to control the various aspects of the house's HVAC system</description>
<channels>
<channel id="temperature" typeId="WWNTemperature"/>
<channel id="humidity" typeId="WWNHumidity"/>
<channel id="mode" typeId="WWNMode"/>
<channel id="previous_mode" typeId="WWNPreviousMode"/>
<channel id="state" typeId="WWNState"/>
<channel id="set_point" typeId="WWNSetPoint"/>
<channel id="max_set_point" typeId="WWNMaxSetPoint"/>
<channel id="min_set_point" typeId="WWNMinSetPoint"/>
<channel id="can_heat" typeId="WWNCanHeat"/>
<channel id="can_cool" typeId="WWNCanCool"/>
<channel id="fan_timer_active" typeId="WWNFanTimerActive"/>
<channel id="fan_timer_duration" typeId="WWNFanTimerDuration"/>
<channel id="fan_timer_timeout" typeId="WWNFanTimerTimeout"/>
<channel id="has_fan" typeId="WWNHasFan"/>
<channel id="has_leaf" typeId="WWNHasLeaf"/>
<channel id="sunlight_correction_enabled" typeId="WWNSunlightCorrectionEnabled"/>
<channel id="sunlight_correction_active" typeId="WWNSunlightCorrectionActive"/>
<channel id="using_emergency_heat" typeId="WWNUsingEmergencyHeat"/>
<channel id="eco_max_set_point" typeId="WWNEcoMaxSetPoint"/>
<channel id="eco_min_set_point" typeId="WWNEcoMinSetPoint"/>
<channel id="locked" typeId="WWNLocked"/>
<channel id="locked_max_set_point" typeId="WWNLockedMaxSetPoint"/>
<channel id="locked_min_set_point" typeId="WWNLockedMinSetPoint"/>
<channel id="time_to_target" typeId="WWNTimeToTarget"/>
<channel id="last_connection" typeId="WWNLastConnection"/>
</channels>
<properties>
<property name="vendor">Nest</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:nest:wwn_device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubAcknowledgeRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubCreateRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullRequest;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullResponse;
import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubReceivedMessage;
/**
* Tests (de)serialization of {@link
* org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses} from/to JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PubSubRequestsResponsesTest {
@Test
public void deserializePullSubscriptionResponse() throws IOException {
PubSubPullResponse response = fromJson("pull-subscription-response.json", PubSubPullResponse.class);
assertThat(response, is(notNullValue()));
List<PubSubReceivedMessage> receivedMessages = response.receivedMessages;
assertThat(receivedMessages, is(notNullValue()));
assertThat(receivedMessages, hasSize(3));
PubSubReceivedMessage receivedMessage = receivedMessages.get(0);
assertThat(receivedMessage, is(notNullValue()));
assertThat(receivedMessage.ackId, is("AID1"));
PubSubMessage message = receivedMessage.message;
assertThat(message, is(notNullValue()));
assertThat(message.data, is("ZGF0YTE="));
assertThat(message.messageId, is("1000000000000001"));
assertThat(message.publishTime, is(ZonedDateTime.parse("2021-01-01T01:00:00.000Z")));
receivedMessage = receivedMessages.get(1);
assertThat(receivedMessage, is(notNullValue()));
assertThat(receivedMessage.ackId, is("AID2"));
message = receivedMessage.message;
assertThat(message, is(notNullValue()));
assertThat(message.data, is("ZGF0YTI="));
assertThat(message.messageId, is("2000000000000002"));
assertThat(message.publishTime, is(ZonedDateTime.parse("2021-02-02T02:00:00.000Z")));
receivedMessage = receivedMessages.get(2);
assertThat(receivedMessage, is(notNullValue()));
assertThat(receivedMessage.ackId, is("AID3"));
message = receivedMessage.message;
assertThat(message, is(notNullValue()));
assertThat(message.data, is("ZGF0YTM="));
assertThat(message.messageId, is("3000000000000003"));
assertThat(message.publishTime, is(ZonedDateTime.parse("2021-03-03T03:00:00.000Z")));
}
@Test
public void serializeAcknowledgeSubscriptionRequest() throws IOException {
String json = toJson(new PubSubAcknowledgeRequest(List.of("AID1", "AID2", "AID3")));
assertThat(json, is(fromFile("acknowledge-subscription-request.json")));
}
@Test
public void serializeCreateSubscriptionRequest() throws IOException {
String json = toJson(new PubSubCreateRequest("projects/sdm-prod/topics/enterprise-project-id", true));
assertThat(json, is(fromFile("create-subscription-request.json")));
}
@Test
public void serializePullSubscriptionRequest() throws IOException {
String json = toJson(new PubSubPullRequest(123));
assertThat(json, is(fromFile("pull-subscription-request.json")));
}
}

View File

@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCameraRtspStreamUrls;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamResults;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResults;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResponse;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResults;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetFanTimerRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatCoolSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatEcoModeRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatHeatSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatModeRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatRangeSetpointRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMStopCameraRtspStreamRequest;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode;
/**
* Tests (de)serialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMCommands} requests
* and responses from/to JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMCommandsTest {
@Test
public void deserializeExtendCameraRtspStreamResponse() throws IOException {
SDMExtendCameraRtspStreamResponse response = fromJson("extend-camera-rtsp-stream-response.json",
SDMExtendCameraRtspStreamResponse.class);
assertThat(response, is(notNullValue()));
SDMExtendCameraRtspStreamResults results = response.results;
assertThat(results, is(notNullValue()));
assertThat(results.streamExtensionToken, is("dGNUlTU2CjY5Y3VKaTZwR3o4Y1..."));
assertThat(results.streamToken, is("g.0.newStreamingToken"));
assertThat(results.expiresAt, is(ZonedDateTime.parse("2018-01-04T18:30:00.000Z")));
}
@Test
public void deserializeGenerateCameraImageResponse() throws IOException {
SDMGenerateCameraImageResponse response = fromJson("generate-camera-image-response.json",
SDMGenerateCameraImageResponse.class);
assertThat(response, is(notNullValue()));
SDMGenerateCameraImageResults results = response.results;
assertThat(results, is(notNullValue()));
assertThat(results.url, is("https://domain/sdm_resource/dGNUlTU2CjY5Y3VKaTZwR3o4Y1..."));
assertThat(results.token, is("g.0.eventToken"));
}
@Test
public void deserializeGenerateCameraRtspStreamResponse() throws IOException {
SDMGenerateCameraRtspStreamResponse response = fromJson("generate-camera-rtsp-stream-response.json",
SDMGenerateCameraRtspStreamResponse.class);
assertThat(response, is(notNullValue()));
SDMGenerateCameraRtspStreamResults results = response.results;
assertThat(results, is(notNullValue()));
SDMCameraRtspStreamUrls streamUrls = results.streamUrls;
assertThat(streamUrls, is(notNullValue()));
assertThat(streamUrls.rtspUrl, is("rtsps://someurl.com/CjY5Y3VKaTZwR3o4Y19YbTVfMF...?auth=g.0.streamingToken"));
assertThat(results.streamExtensionToken, is("CjY5Y3VKaTZwR3o4Y19YbTVfMF..."));
assertThat(results.streamToken, is("g.0.streamingToken"));
assertThat(results.expiresAt, is(ZonedDateTime.parse("2018-01-04T18:30:00.000Z")));
}
@Test
public void serializeExtendCameraRtspStreamRequest() throws IOException {
String json = toJson(new SDMExtendCameraRtspStreamRequest("CjY5Y3VKaTZwR3o4Y19YbTVfMF..."));
assertThat(json, is(fromFile("extend-camera-rtsp-stream-request.json")));
}
@Test
public void serializeGenerateCameraImageRequest() throws IOException {
String json = toJson(new SDMGenerateCameraImageRequest("FWWVQVUdGNUlTU2V4MGV2aTNXV..."));
assertThat(json, is(fromFile("generate-camera-image-request.json")));
}
@Test
public void serializeGenerateCameraRtspStreamRequest() throws IOException {
String json = toJson(new SDMGenerateCameraRtspStreamRequest());
assertThat(json, is(fromFile("generate-camera-rtsp-stream-request.json")));
}
@Test
public void serializeSetFanTimerRequestWithDuration() throws IOException {
String json = toJson(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, Duration.ofSeconds(3600)));
assertThat(json, is(fromFile("set-fan-timer-request-with-duration.json")));
}
@Test
public void serializeSetFanTimerRequestWithoutDuration() throws IOException {
String json = toJson(new SDMSetFanTimerRequest(SDMFanTimerMode.ON));
assertThat(json, is(fromFile("set-fan-timer-request-without-duration.json")));
}
@Test
public void serializeSetThermostatCoolSetpointRequest() throws IOException {
String json = toJson(new SDMSetThermostatCoolSetpointRequest(new BigDecimal("20.0")));
assertThat(json, is(fromFile("set-thermostat-cool-setpoint-request.json")));
}
@Test
public void serializeSetThermostatEcoModeRequest() throws IOException {
String json = toJson(new SDMSetThermostatEcoModeRequest(SDMThermostatEcoMode.MANUAL_ECO));
assertThat(json, is(fromFile("set-thermostat-eco-mode-request.json")));
}
@Test
public void serializeSetThermostatHeatSetpointRequest() throws IOException {
String json = toJson(new SDMSetThermostatHeatSetpointRequest(new BigDecimal("15.0")));
assertThat(json, is(fromFile("set-thermostat-heat-setpoint-request.json")));
}
@Test
public void serializeSetThermostatModeRequest() throws IOException {
String json = toJson(new SDMSetThermostatModeRequest(SDMThermostatMode.HEATCOOL));
assertThat(json, is(fromFile("set-thermostat-mode-request.json")));
}
@Test
public void serializeSetThermostatRangeSetpointRequest() throws IOException {
String json = toJson(new SDMSetThermostatRangeSetpointRequest(new BigDecimal("15.0"), new BigDecimal("20.0")));
assertThat(json, is(fromFile("set-thermostat-range-setpoint-request.json")));
}
@Test
public void serializeStopCameraRtspStreamRequest() throws IOException {
String json = toJson(new SDMStopCameraRtspStreamRequest("CjY5Y3VKaTZwR3o4Y19YbTVfMF..."));
assertThat(json, is(fromFile("stop-camera-rtsp-stream-request.json")));
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.stream.JsonWriter;
/**
* Utility class for working with Nest SDM test data in unit tests.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMDataUtil {
public static Reader openDataReader(String fileName) throws UnsupportedEncodingException, FileNotFoundException {
String packagePath = (SDMDataUtil.class.getPackage().getName()).replaceAll("\\.", "/");
String filePath = "src/test/resources/" + packagePath + "/" + fileName;
InputStream inputStream = new FileInputStream(filePath);
return new InputStreamReader(inputStream, "UTF-8");
}
public static <T> T fromJson(String fileName, Class<T> dataClass) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return GSON.fromJson(reader, dataClass);
}
}
public static String fromFile(String fileName) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n"));
}
}
public static String toJson(Object object) {
StringWriter writer = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(writer);
jsonWriter.setIndent(" ");
GSON.toJson(object, object.getClass(), jsonWriter);
return writer.toString();
}
}

View File

@ -0,0 +1,298 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nest.internal.sdm.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraImageTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraLiveStreamTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityStatus;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceInfoTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHumidityTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHvacStatus;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMResolution;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureScale;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatHvacTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatModeTrait;
import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatTemperatureSetpointTrait;
/**
* Tests deserialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMDevice}s from JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SDMDeviceTest {
@Test
public void deserializeThermostatDevice() throws IOException {
SDMDevice device = getThermostatDevice();
assertThat(device, is(notNullValue()));
assertThat(device.name.name, is("enterprises/project-id/devices/thermostat-device-id"));
assertThat(device.type, is(SDMDeviceType.THERMOSTAT));
SDMTraits traits = device.traits;
assertThat(traits, is(notNullValue()));
assertThat(traits.traitList(), hasSize(10));
SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
assertThat(deviceInfo, is(notNullValue()));
assertThat(deviceInfo.customName, is(""));
SDMHumidityTrait humidity = traits.humidity;
assertThat(humidity, is(notNullValue()));
assertThat(humidity.ambientHumidityPercent, is(new BigDecimal(26)));
SDMConnectivityTrait connectivity = traits.connectivity;
assertThat(connectivity, is(notNullValue()));
assertThat(connectivity.status, is(SDMConnectivityStatus.ONLINE));
SDMFanTrait fan = traits.fan;
assertThat(fan, is(notNullValue()));
assertThat(fan.timerMode, is(SDMFanTimerMode.ON));
assertThat(fan.timerTimeout, is(ZonedDateTime.parse("2019-05-10T03:22:54Z")));
SDMThermostatModeTrait thermostatMode = traits.thermostatMode;
assertThat(thermostatMode, is(notNullValue()));
assertThat(thermostatMode.mode, is(SDMThermostatMode.HEAT));
assertThat(thermostatMode.availableModes, is(List.of(SDMThermostatMode.HEAT, SDMThermostatMode.OFF)));
SDMThermostatEcoTrait thermostatEco = traits.thermostatEco;
assertThat(thermostatEco, is(notNullValue()));
assertThat(thermostatEco.availableModes,
is(List.of(SDMThermostatEcoMode.OFF, SDMThermostatEcoMode.MANUAL_ECO)));
assertThat(thermostatEco.mode, is(SDMThermostatEcoMode.OFF));
assertThat(thermostatEco.heatCelsius, is(new BigDecimal("15.34473")));
assertThat(thermostatEco.coolCelsius, is(new BigDecimal("24.44443")));
SDMThermostatHvacTrait thermostatHvac = traits.thermostatHvac;
assertThat(thermostatHvac, is(notNullValue()));
assertThat(thermostatHvac.status, is(SDMHvacStatus.OFF));
SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings;
assertThat(deviceSettings, is(notNullValue()));
assertThat(deviceSettings.temperatureScale, is(SDMTemperatureScale.CELSIUS));
SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = traits.thermostatTemperatureSetpoint;
assertThat(thermostatTemperatureSetpoint, is(notNullValue()));
assertThat(thermostatTemperatureSetpoint.heatCelsius, is(new BigDecimal("14.92249")));
assertThat(thermostatTemperatureSetpoint.coolCelsius, is(nullValue()));
SDMTemperatureTrait temperature = traits.temperature;
assertThat(temperature, is(notNullValue()));
assertThat(temperature.ambientTemperatureCelsius, is(new BigDecimal("19.73")));
List<SDMParentRelation> parentRelations = device.parentRelations;
assertThat(parentRelations, is(notNullValue()));
assertThat(parentRelations, hasSize(1));
assertThat(parentRelations.get(0).parent.name,
is("enterprises/project-id/structures/structure-id/rooms/thermostat-room-id"));
assertThat(parentRelations.get(0).displayName, is("Thermostat Room Name"));
}
protected SDMDevice getThermostatDevice() throws IOException {
return fromJson("thermostat-device-response.json", SDMDevice.class);
}
@Test
public void deserializeCameraDevice() throws IOException {
SDMDevice device = getCameraDevice();
assertThat(device, is(notNullValue()));
assertThat(device.name.name, is("enterprises/project-id/devices/camera-device-id"));
assertThat(device.type, is(SDMDeviceType.CAMERA));
SDMTraits traits = device.traits;
assertThat(traits, is(notNullValue()));
assertThat(traits.traitList(), hasSize(7));
SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
assertThat(deviceInfo, is(notNullValue()));
assertThat(deviceInfo.customName, is(""));
SDMConnectivityTrait connectivity = traits.connectivity;
assertThat(connectivity, is(nullValue()));
SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream;
assertThat(cameraLiveStream, is(notNullValue()));
SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution;
assertThat(maxVideoResolution, is(notNullValue()));
assertThat(maxVideoResolution.width, is(640));
assertThat(maxVideoResolution.height, is(480));
assertThat(cameraLiveStream.videoCodecs, is(List.of("H264")));
assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC")));
SDMCameraImageTrait cameraImage = traits.cameraImage;
assertThat(cameraImage, is(notNullValue()));
SDMResolution maxImageResolution = cameraImage.maxImageResolution;
assertThat(maxImageResolution, is(notNullValue()));
assertThat(maxImageResolution.width, is(1920));
assertThat(maxImageResolution.height, is(1200));
assertThat(traits.cameraPerson, is(notNullValue()));
assertThat(traits.cameraSound, is(notNullValue()));
assertThat(traits.cameraMotion, is(notNullValue()));
assertThat(traits.cameraEventImage, is(notNullValue()));
assertThat(traits.doorbellChime, is(nullValue()));
List<SDMParentRelation> parentRelations = device.parentRelations;
assertThat(parentRelations, is(notNullValue()));
assertThat(parentRelations, hasSize(1));
assertThat(parentRelations.get(0).parent.name,
is("enterprises/project-id/structures/structure-id/rooms/camera-room-id"));
assertThat(parentRelations.get(0).displayName, is("Camera Room Name"));
}
protected SDMDevice getCameraDevice() throws IOException {
return fromJson("camera-device-response.json", SDMDevice.class);
}
@Test
public void deserializeDisplayDevice() throws IOException {
SDMDevice device = getDisplayDevice();
assertThat(device, is(notNullValue()));
assertThat(device.name.name, is("enterprises/project-id/devices/display-device-id"));
assertThat(device.type, is(SDMDeviceType.DISPLAY));
SDMTraits traits = device.traits;
assertThat(traits, is(notNullValue()));
assertThat(traits.traitList(), hasSize(7));
SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
assertThat(deviceInfo, is(notNullValue()));
assertThat(deviceInfo.customName, is(""));
SDMConnectivityTrait connectivity = traits.connectivity;
assertThat(connectivity, is(nullValue()));
SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream;
assertThat(cameraLiveStream, is(notNullValue()));
SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution;
assertThat(maxVideoResolution, is(notNullValue()));
assertThat(maxVideoResolution.width, is(640));
assertThat(maxVideoResolution.height, is(480));
assertThat(cameraLiveStream.videoCodecs, is(List.of("H264")));
assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC")));
SDMCameraImageTrait cameraImage = traits.cameraImage;
assertThat(cameraImage, is(notNullValue()));
SDMResolution maxImageResolution = cameraImage.maxImageResolution;
assertThat(maxImageResolution, is(notNullValue()));
assertThat(maxImageResolution.width, is(1920));
assertThat(maxImageResolution.height, is(1200));
assertThat(traits.cameraPerson, is(notNullValue()));
assertThat(traits.cameraSound, is(notNullValue()));
assertThat(traits.cameraMotion, is(notNullValue()));
assertThat(traits.cameraEventImage, is(notNullValue()));
assertThat(traits.doorbellChime, is(nullValue()));
List<SDMParentRelation> parentRelations = device.parentRelations;
assertThat(parentRelations, is(notNullValue()));
assertThat(parentRelations, hasSize(1));
assertThat(parentRelations.get(0).parent.name,
is("enterprises/project-id/structures/structure-id/rooms/display-room-id"));
assertThat(parentRelations.get(0).displayName, is("Display Room Name"));
}
protected SDMDevice getDisplayDevice() throws IOException {
return fromJson("display-device-response.json", SDMDevice.class);
}
@Test
public void deserializeDoorbellDevice() throws IOException {
SDMDevice device = getDoorbellDevice();
assertThat(device, is(notNullValue()));
assertThat(device.name.name, is("enterprises/project-id/devices/doorbell-device-id"));
assertThat(device.type, is(SDMDeviceType.DOORBELL));
SDMTraits traits = device.traits;
assertThat(traits, is(notNullValue()));
assertThat(traits.traitList(), hasSize(8));
SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
assertThat(deviceInfo, is(notNullValue()));
assertThat(deviceInfo.customName, is(""));
SDMConnectivityTrait connectivity = traits.connectivity;
assertThat(connectivity, is(nullValue()));
SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream;
assertThat(cameraLiveStream, is(notNullValue()));
SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution;
assertThat(maxVideoResolution, is(notNullValue()));
assertThat(maxVideoResolution.width, is(640));
assertThat(maxVideoResolution.height, is(480));
assertThat(cameraLiveStream.videoCodecs, is(List.of("H264")));
assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC")));
SDMCameraImageTrait cameraImage = traits.cameraImage;
assertThat(cameraImage, is(notNullValue()));
SDMResolution maxImageResolution = cameraImage.maxImageResolution;
assertThat(maxImageResolution, is(notNullValue()));
assertThat(maxImageResolution.width, is(1920));
assertThat(maxImageResolution.height, is(1200));
assertThat(traits.cameraPerson, is(notNullValue()));
assertThat(traits.cameraSound, is(notNullValue()));
assertThat(traits.cameraMotion, is(notNullValue()));
assertThat(traits.cameraEventImage, is(notNullValue()));
assertThat(traits.doorbellChime, is(notNullValue()));
List<SDMParentRelation> parentRelations = device.parentRelations;
assertThat(parentRelations, is(notNullValue()));
assertThat(parentRelations, hasSize(1));
assertThat(parentRelations.get(0).parent.name,
is("enterprises/project-id/structures/structure-id/rooms/doorbell-room-id"));
assertThat(parentRelations.get(0).displayName, is("Doorbell Room Name"));
}
protected SDMDevice getDoorbellDevice() throws IOException {
return fromJson("doorbell-device-response.json", SDMDevice.class);
}
}

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