Codebase as of c53e4aed26 as an initial commit for the shrunk repo
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.nest/.classpath
Normal file
32
bundles/org.openhab.binding.nest/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.nest/.project
Normal file
23
bundles/org.openhab.binding.nest/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.nest</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.nest/NOTICE
Normal file
13
bundles/org.openhab.binding.nest/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
260
bundles/org.openhab.binding.nest/README.md
Normal file
260
bundles/org.openhab.binding.nest/README.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 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).
|
||||
|
||||
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.
|
||||
|
||||
> 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/).
|
||||
|
||||
## 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 |
|
||||
|
||||
## Authorization
|
||||
|
||||
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 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).
|
||||
|
||||
While registering a new Product (on the Product Details page) make sure to:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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).
|
||||
|
||||
## Channels
|
||||
|
||||
### Account Channels
|
||||
|
||||
The account Thing Type does not have any channels.
|
||||
|
||||
### Camera Channels
|
||||
|
||||
**Camera group channels**
|
||||
|
||||
Information about the camera.
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read Write |
|
||||
|-----------------------|-----------|---------------------------------------------------|:----------:|
|
||||
| app_url | String | The app URL to see the camera | R |
|
||||
| audio_input_enabled | Switch | If the audio input is currently enabled | R |
|
||||
| last_online_change | DateTime | Timestamp of the last online status change | R |
|
||||
| public_share_enabled | Switch | If public sharing is currently enabled | R |
|
||||
| public_share_url | String | The URL to see the public share of the camera | R |
|
||||
| snapshot_url | String | The URL to use for a snapshot of the video stream | R |
|
||||
| streaming | Switch | If the camera is currently streaming | R/W |
|
||||
| video_history_enabled | Switch | If the video history is currently enabled | R |
|
||||
| web_url | String | The web URL to see the camera | R |
|
||||
|
||||
**Last event group channels**
|
||||
|
||||
Information about the last camera event (requires Nest Aware subscription).
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read Write |
|
||||
|--------------------|-----------|------------------------------------------------------------------------------------|:----------:|
|
||||
| activity_zones | String | Identifiers for activity zones that detected the event (comma separated) | R |
|
||||
| animated_image_url | String | The URL showing an animated image for the camera event | R |
|
||||
| app_url | String | The app URL for the camera event, allows you to see the camera event in an app | R |
|
||||
| end_time | DateTime | Timestamp when the camera event ended | R |
|
||||
| has_motion | Switch | If motion was detected in the camera event | R |
|
||||
| has_person | Switch | If a person was detected in the camera event | R |
|
||||
| has_sound | Switch | If sound was detected in the camera event | R |
|
||||
| image_url | String | The URL showing an image for the camera event | R |
|
||||
| start_time | DateTime | Timestamp when the camera event started | R |
|
||||
| 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
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read Write |
|
||||
|-----------------------|-----------|-----------------------------------------------------------------------------------|:----------:|
|
||||
| co_alarm_state | String | The carbon monoxide alarm state of the Nest Protect (OK, EMERGENCY, WARNING) | R |
|
||||
| last_connection | DateTime | Timestamp of the last successful interaction with Nest | R |
|
||||
| last_manual_test_time | DateTime | Timestamp of the last successful manual test | R |
|
||||
| low_battery | Switch | Reports whether the battery of the Nest protect is low (if it is battery powered) | R |
|
||||
| manual_test_active | Switch | Manual test active at the moment | R |
|
||||
| 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
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read Write |
|
||||
|------------------------------|-----------|--------------------------------------------------------------------------------------------------------|:----------:|
|
||||
| away | String | Away state of the structure (HOME, AWAY) | R/W |
|
||||
| country_code | String | Country code of the structure ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) | R |
|
||||
| co_alarm_state | String | Carbon Monoxide alarm state (OK, EMERGENCY, WARNING) | R |
|
||||
| eta_begin | DateTime | Estimated time of arrival at home, will setup the heat to turn on and be warm | R |
|
||||
| peak_period_end_time | DateTime | Peak period end for the Rush Hour Rewards program | R |
|
||||
| peak_period_start_time | DateTime | Peak period start for the Rush Hour Rewards program | R |
|
||||
| postal_code | String | Postal code of the structure | R |
|
||||
| rush_hour_rewards_enrollment | Switch | If rush hour rewards system is enabled or not | R |
|
||||
| security_state | String | Security state of the structure (OK, DETER) | R |
|
||||
| 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
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read Write |
|
||||
|-----------------------------|----------------------|----------------------------------------------------------------------------------------|:----------:|
|
||||
| can_cool | Switch | If the thermostat can actually turn on cooling | R |
|
||||
| can_heat | Switch | If the thermostat can actually turn on heating | R |
|
||||
| eco_max_set_point | Number:Temperature | The eco range max set point temperature | R |
|
||||
| eco_min_set_point | Number:Temperature | The eco range min set point temperature | R |
|
||||
| fan_timer_active | Switch | If the fan timer is engaged | R/W |
|
||||
| fan_timer_duration | Number:Time | Length of time that the fan is set to run (15, 30, 45, 60, 120, 240, 480, 960 minutes) | R/W |
|
||||
| fan_timer_timeout | DateTime | Timestamp when the fan stops running | R |
|
||||
| has_fan | Switch | If the thermostat can control the fan | R |
|
||||
| has_leaf | Switch | If the thermostat is currently in a leaf mode | R |
|
||||
| humidity | Number:Dimensionless | Indicates the current relative humidity | R |
|
||||
| last_connection | DateTime | Timestamp of the last successful interaction with Nest | R |
|
||||
| locked | Switch | If the thermostat has the temperature locked to only be within a set range | R |
|
||||
| locked_max_set_point | Number:Temperature | The locked range max set point temperature | R |
|
||||
| locked_min_set_point | Number:Temperature | The locked range min set point temperature | R |
|
||||
| max_set_point | Number:Temperature | The max set point temperature | R/W |
|
||||
| min_set_point | Number:Temperature | The min set point temperature | R/W |
|
||||
| mode | String | Current mode of the Nest thermostat (HEAT, COOL, HEAT_COOL, ECO, OFF) | R/W |
|
||||
| previous_mode | String | The previous mode of the Nest thermostat (HEAT, COOL, HEAT_COOL, ECO, OFF) | R |
|
||||
| state | String | The active state of the Nest thermostat (HEATING, COOLING, OFF) | R |
|
||||
| temperature | Number:Temperature | Current temperature | R |
|
||||
| time_to_target | Number:Time | Time left to the target temperature approximately | R |
|
||||
| set_point | Number:Temperature | The set point temperature | R/W |
|
||||
| sunlight_correction_active | Switch | If sunlight correction is active | R |
|
||||
| sunlight_correction_enabled | Switch | If sunlight correction is enabled | R |
|
||||
| using_emergency_heat | Switch | If the system is currently using emergency heat | R |
|
||||
|
||||
Note that the Nest API rounds Thermostat values so they will differ from what shows up in the Nest App.
|
||||
The Nest API applies the following rounding:
|
||||
|
||||
* degrees Celsius to 0.5 degrees
|
||||
* degrees Fahrenheit to whole degrees
|
||||
* humidity to 5%
|
||||
|
||||
## Example
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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" ]
|
||||
}
|
||||
```
|
||||
|
||||
### 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" }
|
||||
|
||||
/* 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" }
|
||||
|
||||
/* 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" }
|
||||
|
||||
/* 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" }
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
This documentation contains parts written by John Cocula which were copied from the 1.0 Nest binding.
|
||||
17
bundles/org.openhab.binding.nest/pom.xml
Normal file
17
bundles/org.openhab.binding.nest/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.nest</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Nest Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.nest-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-nest" description="Nest Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nest/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
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.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "nest";
|
||||
|
||||
/** The URL to use to connect to Nest with. */
|
||||
public static final String NEST_URL = "https://developer-api.nest.com";
|
||||
|
||||
/** The URL to get the access token when talking to Nest. */
|
||||
public static final String NEST_ACCESS_TOKEN_URL = "https://api.home.nest.com/oauth2/access_token";
|
||||
|
||||
/** The path to set values on the thermostat when talking to Nest. */
|
||||
public static final String NEST_THERMOSTAT_UPDATE_PATH = "/devices/thermostats/";
|
||||
|
||||
/** The path to set values on the structure when talking to Nest. */
|
||||
public static final String NEST_STRUCTURE_UPDATE_PATH = "/structures/";
|
||||
|
||||
/** The path to set values on the camera when talking to Nest. */
|
||||
public static final String NEST_CAMERA_UPDATE_PATH = "/devices/cameras/";
|
||||
|
||||
/** The path to set values on the camera when talking to Nest. */
|
||||
public static final String NEST_SMOKE_ALARM_UPDATE_PATH = "/devices/smoke_co_alarms/";
|
||||
|
||||
/** The JSON content type used when talking to Nest. */
|
||||
public static final String JSON_CONTENT_TYPE = "application/json";
|
||||
|
||||
/** To keep the streaming REST connection alive Nest sends every 30 seconds a message. */
|
||||
public static final long KEEP_ALIVE_MILLIS = Duration.ofSeconds(30).toMillis();
|
||||
|
||||
/** To avoid API throttling errors (429 Too Many Requests) Nest recommends making at most one call per minute. */
|
||||
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");
|
||||
|
||||
// List of all channel group prefixes
|
||||
public static final String CHANNEL_GROUP_CAMERA_PREFIX = "camera#";
|
||||
public static final String CHANNEL_GROUP_LAST_EVENT_PREFIX = "last_event#";
|
||||
|
||||
// List of all Channel IDs
|
||||
// read only channels (common)
|
||||
public static final String CHANNEL_LAST_CONNECTION = "last_connection";
|
||||
|
||||
// read/write channels (thermostat)
|
||||
public static final String CHANNEL_MODE = "mode";
|
||||
public static final String CHANNEL_SET_POINT = "set_point";
|
||||
public static final String CHANNEL_MAX_SET_POINT = "max_set_point";
|
||||
public static final String CHANNEL_MIN_SET_POINT = "min_set_point";
|
||||
public static final String CHANNEL_FAN_TIMER_ACTIVE = "fan_timer_active";
|
||||
public static final String CHANNEL_FAN_TIMER_DURATION = "fan_timer_duration";
|
||||
|
||||
// read only channels (thermostat)
|
||||
public static final String CHANNEL_ECO_MAX_SET_POINT = "eco_max_set_point";
|
||||
public static final String CHANNEL_ECO_MIN_SET_POINT = "eco_min_set_point";
|
||||
public static final String CHANNEL_LOCKED = "locked";
|
||||
public static final String CHANNEL_LOCKED_MAX_SET_POINT = "locked_max_set_point";
|
||||
public static final String CHANNEL_LOCKED_MIN_SET_POINT = "locked_min_set_point";
|
||||
public static final String CHANNEL_TEMPERATURE = "temperature";
|
||||
public static final String CHANNEL_HUMIDITY = "humidity";
|
||||
public static final String CHANNEL_PREVIOUS_MODE = "previous_mode";
|
||||
public static final String CHANNEL_STATE = "state";
|
||||
public static final String CHANNEL_CAN_HEAT = "can_heat";
|
||||
public static final String CHANNEL_CAN_COOL = "can_cool";
|
||||
public static final String CHANNEL_FAN_TIMER_TIMEOUT = "fan_timer_timeout";
|
||||
public static final String CHANNEL_HAS_FAN = "has_fan";
|
||||
public static final String CHANNEL_HAS_LEAF = "has_leaf";
|
||||
public static final String CHANNEL_SUNLIGHT_CORRECTION_ENABLED = "sunlight_correction_enabled";
|
||||
public static final String CHANNEL_SUNLIGHT_CORRECTION_ACTIVE = "sunlight_correction_active";
|
||||
public static final String CHANNEL_TIME_TO_TARGET = "time_to_target";
|
||||
public static final String CHANNEL_USING_EMERGENCY_HEAT = "using_emergency_heat";
|
||||
|
||||
// read/write channels (camera)
|
||||
public static final String CHANNEL_CAMERA_STREAMING = "camera#streaming";
|
||||
|
||||
// read only channels (camera)
|
||||
public static final String CHANNEL_CAMERA_AUDIO_INPUT_ENABLED = "camera#audio_input_enabled";
|
||||
public static final String CHANNEL_CAMERA_VIDEO_HISTORY_ENABLED = "camera#video_history_enabled";
|
||||
public static final String CHANNEL_CAMERA_WEB_URL = "camera#web_url";
|
||||
public static final String CHANNEL_CAMERA_APP_URL = "camera#app_url";
|
||||
public static final String CHANNEL_CAMERA_PUBLIC_SHARE_ENABLED = "camera#public_share_enabled";
|
||||
public static final String CHANNEL_CAMERA_PUBLIC_SHARE_URL = "camera#public_share_url";
|
||||
public static final String CHANNEL_CAMERA_SNAPSHOT_URL = "camera#snapshot_url";
|
||||
public static final String CHANNEL_CAMERA_LAST_ONLINE_CHANGE = "camera#last_online_change";
|
||||
|
||||
public static final String CHANNEL_LAST_EVENT_HAS_SOUND = "last_event#has_sound";
|
||||
public static final String CHANNEL_LAST_EVENT_HAS_MOTION = "last_event#has_motion";
|
||||
public static final String CHANNEL_LAST_EVENT_HAS_PERSON = "last_event#has_person";
|
||||
public static final String CHANNEL_LAST_EVENT_START_TIME = "last_event#start_time";
|
||||
public static final String CHANNEL_LAST_EVENT_END_TIME = "last_event#end_time";
|
||||
public static final String CHANNEL_LAST_EVENT_URLS_EXPIRE_TIME = "last_event#urls_expire_time";
|
||||
public static final String CHANNEL_LAST_EVENT_WEB_URL = "last_event#web_url";
|
||||
public static final String CHANNEL_LAST_EVENT_APP_URL = "last_event#app_url";
|
||||
public static final String CHANNEL_LAST_EVENT_IMAGE_URL = "last_event#image_url";
|
||||
public static final String CHANNEL_LAST_EVENT_ANIMATED_IMAGE_URL = "last_event#animated_image_url";
|
||||
public static final String CHANNEL_LAST_EVENT_ACTIVITY_ZONES = "last_event#activity_zones";
|
||||
|
||||
// read/write channels (smoke detector)
|
||||
|
||||
// read only channels (smoke detector)
|
||||
public static final String CHANNEL_UI_COLOR_STATE = "ui_color_state";
|
||||
public static final String CHANNEL_LOW_BATTERY = "low_battery";
|
||||
public static final String CHANNEL_CO_ALARM_STATE = "co_alarm_state"; // Also in structure
|
||||
public static final String CHANNEL_SMOKE_ALARM_STATE = "smoke_alarm_state"; // Also in structure
|
||||
public static final String CHANNEL_MANUAL_TEST_ACTIVE = "manual_test_active";
|
||||
public static final String CHANNEL_LAST_MANUAL_TEST_TIME = "last_manual_test_time";
|
||||
|
||||
// read/write channel (structure)
|
||||
public static final String CHANNEL_AWAY = "away";
|
||||
|
||||
// read only channels (structure)
|
||||
public static final String CHANNEL_COUNTRY_CODE = "country_code";
|
||||
public static final String CHANNEL_POSTAL_CODE = "postal_code";
|
||||
public static final String CHANNEL_PEAK_PERIOD_START_TIME = "peak_period_start_time";
|
||||
public static final String CHANNEL_PEAK_PERIOD_END_TIME = "peak_period_end_time";
|
||||
public static final String CHANNEL_TIME_ZONE = "time_zone";
|
||||
public static final String CHANNEL_ETA_BEGIN = "eta_begin";
|
||||
public static final String CHANNEL_RUSH_HOUR_REWARDS_ENROLLMENT = "rush_hour_rewards_enrollment";
|
||||
public static final String CHANNEL_SECURITY_STATE = "security_state";
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
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, @Nullable 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal;
|
||||
|
||||
import java.io.Reader;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* Utility class for sharing utility methods between objects.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class NestUtils {
|
||||
|
||||
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() {
|
||||
// hidden utility class constructor
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String json, Class<T> dataClass) {
|
||||
return GSON.fromJson(json, dataClass);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(Reader reader, Class<T> dataClass) {
|
||||
return GSON.fromJson(reader, dataClass);
|
||||
}
|
||||
|
||||
public static String toJson(Object object) {
|
||||
return GSON.toJson(object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.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.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestBridgeConfiguration {
|
||||
public static final String PRODUCT_ID = "productId";
|
||||
/** Product ID from the Nest product page. */
|
||||
public String productId = "";
|
||||
|
||||
public static final String PRODUCT_SECRET = "productSecret";
|
||||
/** Product secret from the Nest product page. */
|
||||
public String productSecret = "";
|
||||
|
||||
public static final String PINCODE = "pincode";
|
||||
/** Product pincode from the Nest authorization page. */
|
||||
public @Nullable String pincode;
|
||||
|
||||
public static final String ACCESS_TOKEN = "accessToken";
|
||||
/** The access token to use once retrieved from Nest. */
|
||||
public @Nullable String accessToken;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The configuration for Nest devices.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Add device configuration to allow file based configuration
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestDeviceConfiguration {
|
||||
public static final String DEVICE_ID = "deviceId";
|
||||
/** Device ID which can be retrieved with the Nest API. */
|
||||
public String deviceId = "";
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The configuration for structures.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Add device configuration to allow file based configuration
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestStructureConfiguration {
|
||||
public static final String STRUCTURE_ID = "structureId";
|
||||
/** Structure ID which can be retrieved with the Nest API. */
|
||||
public String structureId = "";
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* Deals with the access token data that comes back from Nest when it is requested.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class AccessTokenData {
|
||||
|
||||
private String accessToken;
|
||||
private Long expiresIn;
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public Long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AccessTokenData other = (AccessTokenData) obj;
|
||||
if (accessToken == null) {
|
||||
if (other.accessToken != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!accessToken.equals(other.accessToken)) {
|
||||
return false;
|
||||
}
|
||||
if (expiresIn == null) {
|
||||
if (other.expiresIn != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!expiresIn.equals(other.expiresIn)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((accessToken == null) ? 0 : accessToken.hashCode());
|
||||
result = prime * result + ((expiresIn == null) ? 0 : expiresIn.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("AccessTokenData [accessToken=").append(accessToken).append(", expiresIn=").append(expiresIn)
|
||||
.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* The data for a camera activity zone.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Extract ActivityZone object from Camera
|
||||
*/
|
||||
public class ActivityZone {
|
||||
|
||||
private String name;
|
||||
private int id;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ActivityZone other = (ActivityZone) obj;
|
||||
if (id != other.id) {
|
||||
return false;
|
||||
}
|
||||
if (name == null) {
|
||||
if (other.name != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + id;
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("CameraActivityZone [name=").append(name).append(", id=").append(id).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Default properties shared across all Nest devices.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class BaseNestDevice implements NestIdentifiable {
|
||||
|
||||
private String deviceId;
|
||||
private String name;
|
||||
private String nameLong;
|
||||
private Date lastConnection;
|
||||
private Boolean isOnline;
|
||||
private String softwareVersion;
|
||||
private String structureId;
|
||||
|
||||
private String whereId;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public Date getLastConnection() {
|
||||
return lastConnection;
|
||||
}
|
||||
|
||||
public Boolean isOnline() {
|
||||
return isOnline;
|
||||
}
|
||||
|
||||
public String getNameLong() {
|
||||
return nameLong;
|
||||
}
|
||||
|
||||
public String getSoftwareVersion() {
|
||||
return softwareVersion;
|
||||
}
|
||||
|
||||
public String getStructureId() {
|
||||
return structureId;
|
||||
}
|
||||
|
||||
public String getWhereId() {
|
||||
return whereId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
BaseNestDevice other = (BaseNestDevice) obj;
|
||||
if (deviceId == null) {
|
||||
if (other.deviceId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!deviceId.equals(other.deviceId)) {
|
||||
return false;
|
||||
}
|
||||
if (isOnline == null) {
|
||||
if (other.isOnline != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isOnline.equals(other.isOnline)) {
|
||||
return false;
|
||||
}
|
||||
if (lastConnection == null) {
|
||||
if (other.lastConnection != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lastConnection.equals(other.lastConnection)) {
|
||||
return false;
|
||||
}
|
||||
if (name == null) {
|
||||
if (other.name != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
if (nameLong == null) {
|
||||
if (other.nameLong != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!nameLong.equals(other.nameLong)) {
|
||||
return false;
|
||||
}
|
||||
if (softwareVersion == null) {
|
||||
if (other.softwareVersion != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!softwareVersion.equals(other.softwareVersion)) {
|
||||
return false;
|
||||
}
|
||||
if (structureId == null) {
|
||||
if (other.structureId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!structureId.equals(other.structureId)) {
|
||||
return false;
|
||||
}
|
||||
if (whereId == null) {
|
||||
if (other.whereId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!whereId.equals(other.whereId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((deviceId == null) ? 0 : deviceId.hashCode());
|
||||
result = prime * result + ((isOnline == null) ? 0 : isOnline.hashCode());
|
||||
result = prime * result + ((lastConnection == null) ? 0 : lastConnection.hashCode());
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
result = prime * result + ((nameLong == null) ? 0 : nameLong.hashCode());
|
||||
result = prime * result + ((softwareVersion == null) ? 0 : softwareVersion.hashCode());
|
||||
result = prime * result + ((structureId == null) ? 0 : structureId.hashCode());
|
||||
result = prime * result + ((whereId == null) ? 0 : whereId.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("BaseNestDevice [deviceId=").append(deviceId).append(", name=").append(name)
|
||||
.append(", nameLong=").append(nameLong).append(", lastConnection=").append(lastConnection)
|
||||
.append(", isOnline=").append(isOnline).append(", softwareVersion=").append(softwareVersion)
|
||||
.append(", structureId=").append(structureId).append(", whereId=").append(whereId).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The data for the camera.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class Camera extends BaseNestDevice {
|
||||
|
||||
private Boolean isStreaming;
|
||||
private Boolean isAudioInputEnabled;
|
||||
private Date lastIsOnlineChange;
|
||||
private Boolean isVideoHistoryEnabled;
|
||||
private String webUrl;
|
||||
private String appUrl;
|
||||
private Boolean isPublicShareEnabled;
|
||||
private List<ActivityZone> activityZones;
|
||||
private String publicShareUrl;
|
||||
private String snapshotUrl;
|
||||
private CameraEvent lastEvent;
|
||||
|
||||
public Boolean isStreaming() {
|
||||
return isStreaming;
|
||||
}
|
||||
|
||||
public Boolean isAudioInputEnabled() {
|
||||
return isAudioInputEnabled;
|
||||
}
|
||||
|
||||
public Date getLastIsOnlineChange() {
|
||||
return lastIsOnlineChange;
|
||||
}
|
||||
|
||||
public Boolean isVideoHistoryEnabled() {
|
||||
return isVideoHistoryEnabled;
|
||||
}
|
||||
|
||||
public String getWebUrl() {
|
||||
return webUrl;
|
||||
}
|
||||
|
||||
public String getAppUrl() {
|
||||
return appUrl;
|
||||
}
|
||||
|
||||
public Boolean isPublicShareEnabled() {
|
||||
return isPublicShareEnabled;
|
||||
}
|
||||
|
||||
public List<ActivityZone> getActivityZones() {
|
||||
return activityZones;
|
||||
}
|
||||
|
||||
public String getPublicShareUrl() {
|
||||
return publicShareUrl;
|
||||
}
|
||||
|
||||
public String getSnapshotUrl() {
|
||||
return snapshotUrl;
|
||||
}
|
||||
|
||||
public CameraEvent getLastEvent() {
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Camera other = (Camera) obj;
|
||||
if (activityZones == null) {
|
||||
if (other.activityZones != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!activityZones.equals(other.activityZones)) {
|
||||
return false;
|
||||
}
|
||||
if (appUrl == null) {
|
||||
if (other.appUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!appUrl.equals(other.appUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (isAudioInputEnabled == null) {
|
||||
if (other.isAudioInputEnabled != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isAudioInputEnabled.equals(other.isAudioInputEnabled)) {
|
||||
return false;
|
||||
}
|
||||
if (isPublicShareEnabled == null) {
|
||||
if (other.isPublicShareEnabled != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isPublicShareEnabled.equals(other.isPublicShareEnabled)) {
|
||||
return false;
|
||||
}
|
||||
if (isStreaming == null) {
|
||||
if (other.isStreaming != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isStreaming.equals(other.isStreaming)) {
|
||||
return false;
|
||||
}
|
||||
if (isVideoHistoryEnabled == null) {
|
||||
if (other.isVideoHistoryEnabled != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isVideoHistoryEnabled.equals(other.isVideoHistoryEnabled)) {
|
||||
return false;
|
||||
}
|
||||
if (lastEvent == null) {
|
||||
if (other.lastEvent != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lastEvent.equals(other.lastEvent)) {
|
||||
return false;
|
||||
}
|
||||
if (lastIsOnlineChange == null) {
|
||||
if (other.lastIsOnlineChange != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lastIsOnlineChange.equals(other.lastIsOnlineChange)) {
|
||||
return false;
|
||||
}
|
||||
if (publicShareUrl == null) {
|
||||
if (other.publicShareUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!publicShareUrl.equals(other.publicShareUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (snapshotUrl == null) {
|
||||
if (other.snapshotUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!snapshotUrl.equals(other.snapshotUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (webUrl == null) {
|
||||
if (other.webUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!webUrl.equals(other.webUrl)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
result = prime * result + ((activityZones == null) ? 0 : activityZones.hashCode());
|
||||
result = prime * result + ((appUrl == null) ? 0 : appUrl.hashCode());
|
||||
result = prime * result + ((isAudioInputEnabled == null) ? 0 : isAudioInputEnabled.hashCode());
|
||||
result = prime * result + ((isPublicShareEnabled == null) ? 0 : isPublicShareEnabled.hashCode());
|
||||
result = prime * result + ((isStreaming == null) ? 0 : isStreaming.hashCode());
|
||||
result = prime * result + ((isVideoHistoryEnabled == null) ? 0 : isVideoHistoryEnabled.hashCode());
|
||||
result = prime * result + ((lastEvent == null) ? 0 : lastEvent.hashCode());
|
||||
result = prime * result + ((lastIsOnlineChange == null) ? 0 : lastIsOnlineChange.hashCode());
|
||||
result = prime * result + ((publicShareUrl == null) ? 0 : publicShareUrl.hashCode());
|
||||
result = prime * result + ((snapshotUrl == null) ? 0 : snapshotUrl.hashCode());
|
||||
result = prime * result + ((webUrl == null) ? 0 : webUrl.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Camera [isStreaming=").append(isStreaming).append(", isAudioInputEnabled=")
|
||||
.append(isAudioInputEnabled).append(", lastIsOnlineChange=").append(lastIsOnlineChange)
|
||||
.append(", isVideoHistoryEnabled=").append(isVideoHistoryEnabled).append(", webUrl=").append(webUrl)
|
||||
.append(", appUrl=").append(appUrl).append(", isPublicShareEnabled=").append(isPublicShareEnabled)
|
||||
.append(", activityZones=").append(activityZones).append(", publicShareUrl=").append(publicShareUrl)
|
||||
.append(", snapshotUrl=").append(snapshotUrl).append(", lastEvent=").append(lastEvent)
|
||||
.append(", getId()=").append(getId()).append(", getName()=").append(getName())
|
||||
.append(", getDeviceId()=").append(getDeviceId()).append(", getLastConnection()=")
|
||||
.append(getLastConnection()).append(", isOnline()=").append(isOnline()).append(", getNameLong()=")
|
||||
.append(getNameLong()).append(", getSoftwareVersion()=").append(getSoftwareVersion())
|
||||
.append(", getStructureId()=").append(getStructureId()).append(", getWhereId()=").append(getWhereId())
|
||||
.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The data for a 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 {
|
||||
|
||||
private Boolean hasSound;
|
||||
private Boolean hasMotion;
|
||||
private Boolean hasPerson;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Date urlsExpireTime;
|
||||
private String webUrl;
|
||||
private String appUrl;
|
||||
private String imageUrl;
|
||||
private String animatedImageUrl;
|
||||
private List<String> activityZoneIds;
|
||||
|
||||
public Boolean isHasSound() {
|
||||
return hasSound;
|
||||
}
|
||||
|
||||
public Boolean isHasMotion() {
|
||||
return hasMotion;
|
||||
}
|
||||
|
||||
public Boolean isHasPerson() {
|
||||
return hasPerson;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public Date getUrlsExpireTime() {
|
||||
return urlsExpireTime;
|
||||
}
|
||||
|
||||
public String getWebUrl() {
|
||||
return webUrl;
|
||||
}
|
||||
|
||||
public String getAppUrl() {
|
||||
return appUrl;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public String getAnimatedImageUrl() {
|
||||
return animatedImageUrl;
|
||||
}
|
||||
|
||||
public List<String> getActivityZones() {
|
||||
return activityZoneIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
CameraEvent other = (CameraEvent) obj;
|
||||
if (activityZoneIds == null) {
|
||||
if (other.activityZoneIds != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!activityZoneIds.equals(other.activityZoneIds)) {
|
||||
return false;
|
||||
}
|
||||
if (animatedImageUrl == null) {
|
||||
if (other.animatedImageUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!animatedImageUrl.equals(other.animatedImageUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (appUrl == null) {
|
||||
if (other.appUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!appUrl.equals(other.appUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (endTime == null) {
|
||||
if (other.endTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!endTime.equals(other.endTime)) {
|
||||
return false;
|
||||
}
|
||||
if (hasMotion == null) {
|
||||
if (other.hasMotion != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!hasMotion.equals(other.hasMotion)) {
|
||||
return false;
|
||||
}
|
||||
if (hasPerson == null) {
|
||||
if (other.hasPerson != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!hasPerson.equals(other.hasPerson)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSound == null) {
|
||||
if (other.hasSound != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!hasSound.equals(other.hasSound)) {
|
||||
return false;
|
||||
}
|
||||
if (imageUrl == null) {
|
||||
if (other.imageUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!imageUrl.equals(other.imageUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (startTime == null) {
|
||||
if (other.startTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!startTime.equals(other.startTime)) {
|
||||
return false;
|
||||
}
|
||||
if (urlsExpireTime == null) {
|
||||
if (other.urlsExpireTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!urlsExpireTime.equals(other.urlsExpireTime)) {
|
||||
return false;
|
||||
}
|
||||
if (webUrl == null) {
|
||||
if (other.webUrl != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!webUrl.equals(other.webUrl)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((activityZoneIds == null) ? 0 : activityZoneIds.hashCode());
|
||||
result = prime * result + ((animatedImageUrl == null) ? 0 : animatedImageUrl.hashCode());
|
||||
result = prime * result + ((appUrl == null) ? 0 : appUrl.hashCode());
|
||||
result = prime * result + ((endTime == null) ? 0 : endTime.hashCode());
|
||||
result = prime * result + ((hasMotion == null) ? 0 : hasMotion.hashCode());
|
||||
result = prime * result + ((hasPerson == null) ? 0 : hasPerson.hashCode());
|
||||
result = prime * result + ((hasSound == null) ? 0 : hasSound.hashCode());
|
||||
result = prime * result + ((imageUrl == null) ? 0 : imageUrl.hashCode());
|
||||
result = prime * result + ((startTime == null) ? 0 : startTime.hashCode());
|
||||
result = prime * result + ((urlsExpireTime == null) ? 0 : urlsExpireTime.hashCode());
|
||||
result = prime * result + ((webUrl == null) ? 0 : webUrl.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Event [hasSound=").append(hasSound).append(", hasMotion=").append(hasMotion)
|
||||
.append(", hasPerson=").append(hasPerson).append(", startTime=").append(startTime).append(", endTime=")
|
||||
.append(endTime).append(", urlsExpireTime=").append(urlsExpireTime).append(", webUrl=").append(webUrl)
|
||||
.append(", appUrl=").append(appUrl).append(", imageUrl=").append(imageUrl).append(", animatedImageUrl=")
|
||||
.append(animatedImageUrl).append(", activityZoneIds=").append(activityZoneIds).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Used to set and update the ETA values for Nest.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Extract ETA object from Structure
|
||||
* @author Wouter Born - Add equals, hashCode, toString methods
|
||||
*/
|
||||
public class ETA {
|
||||
|
||||
private String tripId;
|
||||
private Date estimatedArrivalWindowBegin;
|
||||
private Date estimatedArrivalWindowEnd;
|
||||
|
||||
public String getTripId() {
|
||||
return tripId;
|
||||
}
|
||||
|
||||
public void setTripId(String tripId) {
|
||||
this.tripId = tripId;
|
||||
}
|
||||
|
||||
public Date getEstimatedArrivalWindowBegin() {
|
||||
return estimatedArrivalWindowBegin;
|
||||
}
|
||||
|
||||
public void setEstimatedArrivalWindowBegin(Date estimatedArrivalWindowBegin) {
|
||||
this.estimatedArrivalWindowBegin = estimatedArrivalWindowBegin;
|
||||
}
|
||||
|
||||
public Date getEstimatedArrivalWindowEnd() {
|
||||
return estimatedArrivalWindowEnd;
|
||||
}
|
||||
|
||||
public void setEstimatedArrivalWindowEnd(Date estimatedArrivalWindowEnd) {
|
||||
this.estimatedArrivalWindowEnd = estimatedArrivalWindowEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ETA other = (ETA) obj;
|
||||
if (estimatedArrivalWindowBegin == null) {
|
||||
if (other.estimatedArrivalWindowBegin != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!estimatedArrivalWindowBegin.equals(other.estimatedArrivalWindowBegin)) {
|
||||
return false;
|
||||
}
|
||||
if (estimatedArrivalWindowEnd == null) {
|
||||
if (other.estimatedArrivalWindowEnd != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!estimatedArrivalWindowEnd.equals(other.estimatedArrivalWindowEnd)) {
|
||||
return false;
|
||||
}
|
||||
if (tripId == null) {
|
||||
if (other.tripId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!tripId.equals(other.tripId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((estimatedArrivalWindowBegin == null) ? 0 : estimatedArrivalWindowBegin.hashCode());
|
||||
result = prime * result + ((estimatedArrivalWindowEnd == null) ? 0 : estimatedArrivalWindowEnd.hashCode());
|
||||
result = prime * result + ((tripId == null) ? 0 : tripId.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("ETA [tripId=").append(tripId).append(", estimatedArrivalWindowBegin=")
|
||||
.append(estimatedArrivalWindowBegin).append(", estimatedArrivalWindowEnd=")
|
||||
.append(estimatedArrivalWindowEnd).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* The data of Nest API errors.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Improve exception handling
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class ErrorData {
|
||||
|
||||
private String error;
|
||||
private String type;
|
||||
private String message;
|
||||
private String instance;
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ErrorData other = (ErrorData) obj;
|
||||
if (error == null) {
|
||||
if (other.error != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!error.equals(other.error)) {
|
||||
return false;
|
||||
}
|
||||
if (instance == null) {
|
||||
if (other.instance != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!instance.equals(other.instance)) {
|
||||
return false;
|
||||
}
|
||||
if (message == null) {
|
||||
if (other.message != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!message.equals(other.message)) {
|
||||
return false;
|
||||
}
|
||||
if (type == null) {
|
||||
if (other.type != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!type.equals(other.type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((error == null) ? 0 : error.hashCode());
|
||||
result = prime * result + ((instance == null) ? 0 : instance.hashCode());
|
||||
result = prime * result + ((message == null) ? 0 : message.hashCode());
|
||||
result = prime * result + ((type == null) ? 0 : type.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("ErrorData [error=").append(error).append(", type=").append(type).append(", message=")
|
||||
.append(message).append(", instance=").append(instance).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* All the Nest devices broken up by type.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
*/
|
||||
public class NestDevices {
|
||||
|
||||
private Map<String, Thermostat> thermostats;
|
||||
private Map<String, SmokeDetector> smokeCoAlarms;
|
||||
private Map<String, Camera> cameras;
|
||||
|
||||
/** Id to thermostat mapping */
|
||||
public Map<String, Thermostat> getThermostats() {
|
||||
return thermostats;
|
||||
}
|
||||
|
||||
/** Id to camera mapping */
|
||||
public Map<String, Camera> getCameras() {
|
||||
return cameras;
|
||||
}
|
||||
|
||||
/** Id to smoke detector */
|
||||
public Map<String, SmokeDetector> getSmokeCoAlarms() {
|
||||
return smokeCoAlarms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
NestDevices other = (NestDevices) obj;
|
||||
if (cameras == null) {
|
||||
if (other.cameras != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!cameras.equals(other.cameras)) {
|
||||
return false;
|
||||
}
|
||||
if (smokeCoAlarms == null) {
|
||||
if (other.smokeCoAlarms != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!smokeCoAlarms.equals(other.smokeCoAlarms)) {
|
||||
return false;
|
||||
}
|
||||
if (thermostats == null) {
|
||||
if (other.thermostats != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!thermostats.equals(other.thermostats)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((cameras == null) ? 0 : cameras.hashCode());
|
||||
result = prime * result + ((smokeCoAlarms == null) ? 0 : smokeCoAlarms.hashCode());
|
||||
result = prime * result + ((thermostats == null) ? 0 : thermostats.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("NestDevices [thermostats=").append(thermostats).append(", smokeCoAlarms=").append(smokeCoAlarms)
|
||||
.append(", cameras=").append(cameras).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* Interface for uniquely identifiable Nest objects (device or a structure).
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Simplify working with deviceId and structureId
|
||||
*/
|
||||
public interface NestIdentifiable {
|
||||
|
||||
/**
|
||||
* Returns the identifier that uniquely identifies the Nest object (deviceId or structureId).
|
||||
*/
|
||||
String getId();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* The meta data in the data downloads from Nest.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class NestMetadata {
|
||||
|
||||
private String accessToken;
|
||||
private String clientVersion;
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getClientVersion() {
|
||||
return clientVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
NestMetadata other = (NestMetadata) obj;
|
||||
if (accessToken == null) {
|
||||
if (other.accessToken != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!accessToken.equals(other.accessToken)) {
|
||||
return false;
|
||||
}
|
||||
if (clientVersion == null) {
|
||||
if (other.clientVersion != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!clientVersion.equals(other.clientVersion)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((accessToken == null) ? 0 : accessToken.hashCode());
|
||||
result = prime * result + ((clientVersion == null) ? 0 : clientVersion.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("NestMetadata [accessToken=").append(accessToken).append(", clientVersion=")
|
||||
.append(clientVersion).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Data for the Nest smoke detector.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class SmokeDetector extends BaseNestDevice {
|
||||
|
||||
private BatteryHealth batteryHealth;
|
||||
private AlarmState coAlarmState;
|
||||
private Date lastManualTestTime;
|
||||
private AlarmState smokeAlarmState;
|
||||
private Boolean isManualTestActive;
|
||||
private UiColorState uiColorState;
|
||||
|
||||
public UiColorState getUiColorState() {
|
||||
return uiColorState;
|
||||
}
|
||||
|
||||
public BatteryHealth getBatteryHealth() {
|
||||
return batteryHealth;
|
||||
}
|
||||
|
||||
public AlarmState getCoAlarmState() {
|
||||
return coAlarmState;
|
||||
}
|
||||
|
||||
public Date getLastManualTestTime() {
|
||||
return lastManualTestTime;
|
||||
}
|
||||
|
||||
public AlarmState getSmokeAlarmState() {
|
||||
return smokeAlarmState;
|
||||
}
|
||||
|
||||
public Boolean isManualTestActive() {
|
||||
return isManualTestActive;
|
||||
}
|
||||
|
||||
public enum BatteryHealth {
|
||||
@SerializedName("ok")
|
||||
OK,
|
||||
@SerializedName("replace")
|
||||
REPLACE
|
||||
}
|
||||
|
||||
public enum AlarmState {
|
||||
@SerializedName("ok")
|
||||
OK,
|
||||
@SerializedName("emergency")
|
||||
EMERGENCY,
|
||||
@SerializedName("warning")
|
||||
WARNING
|
||||
}
|
||||
|
||||
public enum UiColorState {
|
||||
@SerializedName("gray")
|
||||
GRAY,
|
||||
@SerializedName("green")
|
||||
GREEN,
|
||||
@SerializedName("yellow")
|
||||
YELLOW,
|
||||
@SerializedName("red")
|
||||
RED
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
SmokeDetector other = (SmokeDetector) obj;
|
||||
if (batteryHealth != other.batteryHealth) {
|
||||
return false;
|
||||
}
|
||||
if (coAlarmState != other.coAlarmState) {
|
||||
return false;
|
||||
}
|
||||
if (isManualTestActive == null) {
|
||||
if (other.isManualTestActive != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isManualTestActive.equals(other.isManualTestActive)) {
|
||||
return false;
|
||||
}
|
||||
if (lastManualTestTime == null) {
|
||||
if (other.lastManualTestTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lastManualTestTime.equals(other.lastManualTestTime)) {
|
||||
return false;
|
||||
}
|
||||
if (smokeAlarmState != other.smokeAlarmState) {
|
||||
return false;
|
||||
}
|
||||
if (uiColorState != other.uiColorState) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
result = prime * result + ((batteryHealth == null) ? 0 : batteryHealth.hashCode());
|
||||
result = prime * result + ((coAlarmState == null) ? 0 : coAlarmState.hashCode());
|
||||
result = prime * result + ((isManualTestActive == null) ? 0 : isManualTestActive.hashCode());
|
||||
result = prime * result + ((lastManualTestTime == null) ? 0 : lastManualTestTime.hashCode());
|
||||
result = prime * result + ((smokeAlarmState == null) ? 0 : smokeAlarmState.hashCode());
|
||||
result = prime * result + ((uiColorState == null) ? 0 : uiColorState.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("SmokeDetector [batteryHealth=").append(batteryHealth).append(", coAlarmState=")
|
||||
.append(coAlarmState).append(", lastManualTestTime=").append(lastManualTestTime)
|
||||
.append(", smokeAlarmState=").append(smokeAlarmState).append(", isManualTestActive=")
|
||||
.append(isManualTestActive).append(", uiColorState=").append(uiColorState).append(", getId()=")
|
||||
.append(getId()).append(", getName()=").append(getName()).append(", getDeviceId()=")
|
||||
.append(getDeviceId()).append(", getLastConnection()=").append(getLastConnection())
|
||||
.append(", isOnline()=").append(isOnline()).append(", getNameLong()=").append(getNameLong())
|
||||
.append(", getSoftwareVersion()=").append(getSoftwareVersion()).append(", getStructureId()=")
|
||||
.append(getStructureId()).append(", getWhereId()=").append(getWhereId()).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.binding.nest.internal.data.SmokeDetector.AlarmState;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The structure details from Nest.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class Structure implements NestIdentifiable {
|
||||
|
||||
private String structureId;
|
||||
private List<String> thermostats;
|
||||
private List<String> smokeCoAlarms;
|
||||
private List<String> cameras;
|
||||
private String countryCode;
|
||||
private String postalCode;
|
||||
private Date peakPeriodStartTime;
|
||||
private Date peakPeriodEndTime;
|
||||
private String timeZone;
|
||||
private Date etaBegin;
|
||||
private SmokeDetector.AlarmState coAlarmState;
|
||||
private SmokeDetector.AlarmState smokeAlarmState;
|
||||
private Boolean rhrEnrollment;
|
||||
private Map<String, Where> wheres;
|
||||
private HomeAwayState away;
|
||||
private String name;
|
||||
private ETA eta;
|
||||
private SecurityState wwnSecurityState;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return structureId;
|
||||
}
|
||||
|
||||
public HomeAwayState getAway() {
|
||||
return away;
|
||||
}
|
||||
|
||||
public void setAway(HomeAwayState away) {
|
||||
this.away = away;
|
||||
}
|
||||
|
||||
public String getStructureId() {
|
||||
return structureId;
|
||||
}
|
||||
|
||||
public List<String> getThermostats() {
|
||||
return thermostats;
|
||||
}
|
||||
|
||||
public List<String> getSmokeCoAlarms() {
|
||||
return smokeCoAlarms;
|
||||
}
|
||||
|
||||
public List<String> getCameras() {
|
||||
return cameras;
|
||||
}
|
||||
|
||||
public String getCountryCode() {
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
public String getPostalCode() {
|
||||
return postalCode;
|
||||
}
|
||||
|
||||
public Date getPeakPeriodStartTime() {
|
||||
return peakPeriodStartTime;
|
||||
}
|
||||
|
||||
public Date getPeakPeriodEndTime() {
|
||||
return peakPeriodEndTime;
|
||||
}
|
||||
|
||||
public String getTimeZone() {
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
public Date getEtaBegin() {
|
||||
return etaBegin;
|
||||
}
|
||||
|
||||
public AlarmState getCoAlarmState() {
|
||||
return coAlarmState;
|
||||
}
|
||||
|
||||
public AlarmState getSmokeAlarmState() {
|
||||
return smokeAlarmState;
|
||||
}
|
||||
|
||||
public Boolean isRhrEnrollment() {
|
||||
return rhrEnrollment;
|
||||
}
|
||||
|
||||
public Map<String, Where> getWheres() {
|
||||
return wheres;
|
||||
}
|
||||
|
||||
public ETA getEta() {
|
||||
return eta;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public SecurityState getWwnSecurityState() {
|
||||
return wwnSecurityState;
|
||||
}
|
||||
|
||||
public enum HomeAwayState {
|
||||
@SerializedName("home")
|
||||
HOME,
|
||||
@SerializedName("away")
|
||||
AWAY,
|
||||
@SerializedName("unknown")
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public enum SecurityState {
|
||||
@SerializedName("ok")
|
||||
OK,
|
||||
@SerializedName("deter")
|
||||
DETER
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Structure other = (Structure) obj;
|
||||
if (away != other.away) {
|
||||
return false;
|
||||
}
|
||||
if (cameras == null) {
|
||||
if (other.cameras != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!cameras.equals(other.cameras)) {
|
||||
return false;
|
||||
}
|
||||
if (coAlarmState != other.coAlarmState) {
|
||||
return false;
|
||||
}
|
||||
if (countryCode == null) {
|
||||
if (other.countryCode != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!countryCode.equals(other.countryCode)) {
|
||||
return false;
|
||||
}
|
||||
if (eta == null) {
|
||||
if (other.eta != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!eta.equals(other.eta)) {
|
||||
return false;
|
||||
}
|
||||
if (etaBegin == null) {
|
||||
if (other.etaBegin != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!etaBegin.equals(other.etaBegin)) {
|
||||
return false;
|
||||
}
|
||||
if (name == null) {
|
||||
if (other.name != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
if (peakPeriodEndTime == null) {
|
||||
if (other.peakPeriodEndTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!peakPeriodEndTime.equals(other.peakPeriodEndTime)) {
|
||||
return false;
|
||||
}
|
||||
if (peakPeriodStartTime == null) {
|
||||
if (other.peakPeriodStartTime != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!peakPeriodStartTime.equals(other.peakPeriodStartTime)) {
|
||||
return false;
|
||||
}
|
||||
if (postalCode == null) {
|
||||
if (other.postalCode != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!postalCode.equals(other.postalCode)) {
|
||||
return false;
|
||||
}
|
||||
if (rhrEnrollment == null) {
|
||||
if (other.rhrEnrollment != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!rhrEnrollment.equals(other.rhrEnrollment)) {
|
||||
return false;
|
||||
}
|
||||
if (smokeAlarmState != other.smokeAlarmState) {
|
||||
return false;
|
||||
}
|
||||
if (smokeCoAlarms == null) {
|
||||
if (other.smokeCoAlarms != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!smokeCoAlarms.equals(other.smokeCoAlarms)) {
|
||||
return false;
|
||||
}
|
||||
if (structureId == null) {
|
||||
if (other.structureId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!structureId.equals(other.structureId)) {
|
||||
return false;
|
||||
}
|
||||
if (thermostats == null) {
|
||||
if (other.thermostats != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!thermostats.equals(other.thermostats)) {
|
||||
return false;
|
||||
}
|
||||
if (timeZone == null) {
|
||||
if (other.timeZone != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!timeZone.equals(other.timeZone)) {
|
||||
return false;
|
||||
}
|
||||
if (wheres == null) {
|
||||
if (other.wheres != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!wheres.equals(other.wheres)) {
|
||||
return false;
|
||||
}
|
||||
if (wwnSecurityState != other.wwnSecurityState) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((away == null) ? 0 : away.hashCode());
|
||||
result = prime * result + ((cameras == null) ? 0 : cameras.hashCode());
|
||||
result = prime * result + ((coAlarmState == null) ? 0 : coAlarmState.hashCode());
|
||||
result = prime * result + ((countryCode == null) ? 0 : countryCode.hashCode());
|
||||
result = prime * result + ((eta == null) ? 0 : eta.hashCode());
|
||||
result = prime * result + ((etaBegin == null) ? 0 : etaBegin.hashCode());
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
result = prime * result + ((peakPeriodEndTime == null) ? 0 : peakPeriodEndTime.hashCode());
|
||||
result = prime * result + ((peakPeriodStartTime == null) ? 0 : peakPeriodStartTime.hashCode());
|
||||
result = prime * result + ((postalCode == null) ? 0 : postalCode.hashCode());
|
||||
result = prime * result + ((rhrEnrollment == null) ? 0 : rhrEnrollment.hashCode());
|
||||
result = prime * result + ((smokeAlarmState == null) ? 0 : smokeAlarmState.hashCode());
|
||||
result = prime * result + ((smokeCoAlarms == null) ? 0 : smokeCoAlarms.hashCode());
|
||||
result = prime * result + ((structureId == null) ? 0 : structureId.hashCode());
|
||||
result = prime * result + ((thermostats == null) ? 0 : thermostats.hashCode());
|
||||
result = prime * result + ((timeZone == null) ? 0 : timeZone.hashCode());
|
||||
result = prime * result + ((wheres == null) ? 0 : wheres.hashCode());
|
||||
result = prime * result + ((wwnSecurityState == null) ? 0 : wwnSecurityState.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Structure [structureId=").append(structureId).append(", thermostats=").append(thermostats)
|
||||
.append(", smokeCoAlarms=").append(smokeCoAlarms).append(", cameras=").append(cameras)
|
||||
.append(", countryCode=").append(countryCode).append(", postalCode=").append(postalCode)
|
||||
.append(", peakPeriodStartTime=").append(peakPeriodStartTime).append(", peakPeriodEndTime=")
|
||||
.append(peakPeriodEndTime).append(", timeZone=").append(timeZone).append(", etaBegin=").append(etaBegin)
|
||||
.append(", coAlarmState=").append(coAlarmState).append(", smokeAlarmState=").append(smokeAlarmState)
|
||||
.append(", rhrEnrollment=").append(rhrEnrollment).append(", wheres=").append(wheres).append(", away=")
|
||||
.append(away).append(", name=").append(name).append(", eta=").append(eta).append(", wwnSecurityState=")
|
||||
.append(wwnSecurityState).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Gson class to encapsulate the data for the Nest thermostat.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class Thermostat extends BaseNestDevice {
|
||||
|
||||
private Boolean canCool;
|
||||
private Boolean canHeat;
|
||||
private Boolean isUsingEmergencyHeat;
|
||||
private Boolean hasFan;
|
||||
private Boolean fanTimerActive;
|
||||
private Date fanTimerTimeout;
|
||||
private Boolean hasLeaf;
|
||||
private String temperatureScale;
|
||||
private Double ambientTemperatureC;
|
||||
private Double ambientTemperatureF;
|
||||
private Integer humidity;
|
||||
private Double targetTemperatureC;
|
||||
private Double targetTemperatureF;
|
||||
private Double targetTemperatureHighC;
|
||||
private Double targetTemperatureHighF;
|
||||
private Double targetTemperatureLowC;
|
||||
private Double targetTemperatureLowF;
|
||||
private Mode hvacMode;
|
||||
private Mode previousHvacMode;
|
||||
private State hvacState;
|
||||
private Double ecoTemperatureHighC;
|
||||
private Double ecoTemperatureHighF;
|
||||
private Double ecoTemperatureLowC;
|
||||
private Double ecoTemperatureLowF;
|
||||
private Boolean isLocked;
|
||||
private Double lockedTempMaxC;
|
||||
private Double lockedTempMaxF;
|
||||
private Double lockedTempMinC;
|
||||
private Double lockedTempMinF;
|
||||
private Boolean sunlightCorrectionEnabled;
|
||||
private Boolean sunlightCorrectionActive;
|
||||
private Integer fanTimerDuration;
|
||||
private String timeToTarget;
|
||||
private String whereName;
|
||||
|
||||
public Unit<Temperature> getTemperatureUnit() {
|
||||
if ("C".equals(temperatureScale)) {
|
||||
return CELSIUS;
|
||||
} else if ("F".equals(temperatureScale)) {
|
||||
return FAHRENHEIT;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Double getTargetTemperature() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return targetTemperatureC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return targetTemperatureF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Double getTargetTemperatureHigh() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return targetTemperatureHighC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return targetTemperatureHighF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Double getTargetTemperatureLow() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return targetTemperatureLowC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return targetTemperatureLowF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
return hvacMode;
|
||||
}
|
||||
|
||||
public Double getEcoTemperatureHigh() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return ecoTemperatureHighC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return ecoTemperatureHighF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Double getEcoTemperatureLow() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return ecoTemperatureLowC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return ecoTemperatureLowF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean isLocked() {
|
||||
return isLocked;
|
||||
}
|
||||
|
||||
public Double getLockedTempMax() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return lockedTempMaxC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return lockedTempMaxF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Double getLockedTempMin() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return lockedTempMinC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return lockedTempMinF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean isCanCool() {
|
||||
return canCool;
|
||||
}
|
||||
|
||||
public Boolean isCanHeat() {
|
||||
return canHeat;
|
||||
}
|
||||
|
||||
public Boolean isUsingEmergencyHeat() {
|
||||
return isUsingEmergencyHeat;
|
||||
}
|
||||
|
||||
public Boolean isHasFan() {
|
||||
return hasFan;
|
||||
}
|
||||
|
||||
public Boolean isFanTimerActive() {
|
||||
return fanTimerActive;
|
||||
}
|
||||
|
||||
public Date getFanTimerTimeout() {
|
||||
return fanTimerTimeout;
|
||||
}
|
||||
|
||||
public Boolean isHasLeaf() {
|
||||
return hasLeaf;
|
||||
}
|
||||
|
||||
public Mode getPreviousHvacMode() {
|
||||
return previousHvacMode;
|
||||
}
|
||||
|
||||
public State getHvacState() {
|
||||
return hvacState;
|
||||
}
|
||||
|
||||
public Boolean isSunlightCorrectionEnabled() {
|
||||
return sunlightCorrectionEnabled;
|
||||
}
|
||||
|
||||
public Boolean isSunlightCorrectionActive() {
|
||||
return sunlightCorrectionActive;
|
||||
}
|
||||
|
||||
public Integer getFanTimerDuration() {
|
||||
return fanTimerDuration;
|
||||
}
|
||||
|
||||
public Integer getTimeToTarget() {
|
||||
return parseTimeToTarget(timeToTarget);
|
||||
}
|
||||
|
||||
/*
|
||||
* Turns the time to target string into a real value.
|
||||
*/
|
||||
static Integer parseTimeToTarget(String timeToTarget) {
|
||||
if (timeToTarget == null) {
|
||||
return null;
|
||||
} else if (timeToTarget.startsWith("~") || timeToTarget.startsWith("<") || timeToTarget.startsWith(">")) {
|
||||
return Integer.valueOf(timeToTarget.substring(1));
|
||||
}
|
||||
return Integer.valueOf(timeToTarget);
|
||||
}
|
||||
|
||||
public String getWhereName() {
|
||||
return whereName;
|
||||
}
|
||||
|
||||
public Double getAmbientTemperature() {
|
||||
if (getTemperatureUnit() == CELSIUS) {
|
||||
return ambientTemperatureC;
|
||||
} else if (getTemperatureUnit() == FAHRENHEIT) {
|
||||
return ambientTemperatureF;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getHumidity() {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
@SerializedName("heat")
|
||||
HEAT,
|
||||
@SerializedName("cool")
|
||||
COOL,
|
||||
@SerializedName("heat-cool")
|
||||
HEAT_COOL,
|
||||
@SerializedName("eco")
|
||||
ECO,
|
||||
@SerializedName("off")
|
||||
OFF
|
||||
}
|
||||
|
||||
public enum State {
|
||||
@SerializedName("heating")
|
||||
HEATING,
|
||||
@SerializedName("cooling")
|
||||
COOLING,
|
||||
@SerializedName("off")
|
||||
OFF
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Thermostat other = (Thermostat) obj;
|
||||
if (ambientTemperatureC == null) {
|
||||
if (other.ambientTemperatureC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ambientTemperatureC.equals(other.ambientTemperatureC)) {
|
||||
return false;
|
||||
}
|
||||
if (ambientTemperatureF == null) {
|
||||
if (other.ambientTemperatureF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ambientTemperatureF.equals(other.ambientTemperatureF)) {
|
||||
return false;
|
||||
}
|
||||
if (canCool == null) {
|
||||
if (other.canCool != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!canCool.equals(other.canCool)) {
|
||||
return false;
|
||||
}
|
||||
if (canHeat == null) {
|
||||
if (other.canHeat != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!canHeat.equals(other.canHeat)) {
|
||||
return false;
|
||||
}
|
||||
if (ecoTemperatureHighC == null) {
|
||||
if (other.ecoTemperatureHighC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ecoTemperatureHighC.equals(other.ecoTemperatureHighC)) {
|
||||
return false;
|
||||
}
|
||||
if (ecoTemperatureHighF == null) {
|
||||
if (other.ecoTemperatureHighF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ecoTemperatureHighF.equals(other.ecoTemperatureHighF)) {
|
||||
return false;
|
||||
}
|
||||
if (ecoTemperatureLowC == null) {
|
||||
if (other.ecoTemperatureLowC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ecoTemperatureLowC.equals(other.ecoTemperatureLowC)) {
|
||||
return false;
|
||||
}
|
||||
if (ecoTemperatureLowF == null) {
|
||||
if (other.ecoTemperatureLowF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!ecoTemperatureLowF.equals(other.ecoTemperatureLowF)) {
|
||||
return false;
|
||||
}
|
||||
if (fanTimerActive == null) {
|
||||
if (other.fanTimerActive != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!fanTimerActive.equals(other.fanTimerActive)) {
|
||||
return false;
|
||||
}
|
||||
if (fanTimerDuration == null) {
|
||||
if (other.fanTimerDuration != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!fanTimerDuration.equals(other.fanTimerDuration)) {
|
||||
return false;
|
||||
}
|
||||
if (fanTimerTimeout == null) {
|
||||
if (other.fanTimerTimeout != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!fanTimerTimeout.equals(other.fanTimerTimeout)) {
|
||||
return false;
|
||||
}
|
||||
if (hasFan == null) {
|
||||
if (other.hasFan != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!hasFan.equals(other.hasFan)) {
|
||||
return false;
|
||||
}
|
||||
if (hasLeaf == null) {
|
||||
if (other.hasLeaf != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!hasLeaf.equals(other.hasLeaf)) {
|
||||
return false;
|
||||
}
|
||||
if (humidity == null) {
|
||||
if (other.humidity != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!humidity.equals(other.humidity)) {
|
||||
return false;
|
||||
}
|
||||
if (hvacMode != other.hvacMode) {
|
||||
return false;
|
||||
}
|
||||
if (hvacState != other.hvacState) {
|
||||
return false;
|
||||
}
|
||||
if (isLocked == null) {
|
||||
if (other.isLocked != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isLocked.equals(other.isLocked)) {
|
||||
return false;
|
||||
}
|
||||
if (isUsingEmergencyHeat == null) {
|
||||
if (other.isUsingEmergencyHeat != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isUsingEmergencyHeat.equals(other.isUsingEmergencyHeat)) {
|
||||
return false;
|
||||
}
|
||||
if (lockedTempMaxC == null) {
|
||||
if (other.lockedTempMaxC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lockedTempMaxC.equals(other.lockedTempMaxC)) {
|
||||
return false;
|
||||
}
|
||||
if (lockedTempMaxF == null) {
|
||||
if (other.lockedTempMaxF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lockedTempMaxF.equals(other.lockedTempMaxF)) {
|
||||
return false;
|
||||
}
|
||||
if (lockedTempMinC == null) {
|
||||
if (other.lockedTempMinC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lockedTempMinC.equals(other.lockedTempMinC)) {
|
||||
return false;
|
||||
}
|
||||
if (lockedTempMinF == null) {
|
||||
if (other.lockedTempMinF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!lockedTempMinF.equals(other.lockedTempMinF)) {
|
||||
return false;
|
||||
}
|
||||
if (previousHvacMode != other.previousHvacMode) {
|
||||
return false;
|
||||
}
|
||||
if (sunlightCorrectionActive == null) {
|
||||
if (other.sunlightCorrectionActive != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!sunlightCorrectionActive.equals(other.sunlightCorrectionActive)) {
|
||||
return false;
|
||||
}
|
||||
if (sunlightCorrectionEnabled == null) {
|
||||
if (other.sunlightCorrectionEnabled != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!sunlightCorrectionEnabled.equals(other.sunlightCorrectionEnabled)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureC == null) {
|
||||
if (other.targetTemperatureC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureC.equals(other.targetTemperatureC)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureF == null) {
|
||||
if (other.targetTemperatureF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureF.equals(other.targetTemperatureF)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureHighC == null) {
|
||||
if (other.targetTemperatureHighC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureHighC.equals(other.targetTemperatureHighC)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureHighF == null) {
|
||||
if (other.targetTemperatureHighF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureHighF.equals(other.targetTemperatureHighF)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureLowC == null) {
|
||||
if (other.targetTemperatureLowC != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureLowC.equals(other.targetTemperatureLowC)) {
|
||||
return false;
|
||||
}
|
||||
if (targetTemperatureLowF == null) {
|
||||
if (other.targetTemperatureLowF != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!targetTemperatureLowF.equals(other.targetTemperatureLowF)) {
|
||||
return false;
|
||||
}
|
||||
if (temperatureScale == null) {
|
||||
if (other.temperatureScale != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!temperatureScale.equals(other.temperatureScale)) {
|
||||
return false;
|
||||
}
|
||||
if (timeToTarget == null) {
|
||||
if (other.timeToTarget != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!timeToTarget.equals(other.timeToTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (whereName == null) {
|
||||
if (other.whereName != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!whereName.equals(other.whereName)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
result = prime * result + ((ambientTemperatureC == null) ? 0 : ambientTemperatureC.hashCode());
|
||||
result = prime * result + ((ambientTemperatureF == null) ? 0 : ambientTemperatureF.hashCode());
|
||||
result = prime * result + ((canCool == null) ? 0 : canCool.hashCode());
|
||||
result = prime * result + ((canHeat == null) ? 0 : canHeat.hashCode());
|
||||
result = prime * result + ((ecoTemperatureHighC == null) ? 0 : ecoTemperatureHighC.hashCode());
|
||||
result = prime * result + ((ecoTemperatureHighF == null) ? 0 : ecoTemperatureHighF.hashCode());
|
||||
result = prime * result + ((ecoTemperatureLowC == null) ? 0 : ecoTemperatureLowC.hashCode());
|
||||
result = prime * result + ((ecoTemperatureLowF == null) ? 0 : ecoTemperatureLowF.hashCode());
|
||||
result = prime * result + ((fanTimerActive == null) ? 0 : fanTimerActive.hashCode());
|
||||
result = prime * result + ((fanTimerDuration == null) ? 0 : fanTimerDuration.hashCode());
|
||||
result = prime * result + ((fanTimerTimeout == null) ? 0 : fanTimerTimeout.hashCode());
|
||||
result = prime * result + ((hasFan == null) ? 0 : hasFan.hashCode());
|
||||
result = prime * result + ((hasLeaf == null) ? 0 : hasLeaf.hashCode());
|
||||
result = prime * result + ((humidity == null) ? 0 : humidity.hashCode());
|
||||
result = prime * result + ((hvacMode == null) ? 0 : hvacMode.hashCode());
|
||||
result = prime * result + ((hvacState == null) ? 0 : hvacState.hashCode());
|
||||
result = prime * result + ((isLocked == null) ? 0 : isLocked.hashCode());
|
||||
result = prime * result + ((isUsingEmergencyHeat == null) ? 0 : isUsingEmergencyHeat.hashCode());
|
||||
result = prime * result + ((lockedTempMaxC == null) ? 0 : lockedTempMaxC.hashCode());
|
||||
result = prime * result + ((lockedTempMaxF == null) ? 0 : lockedTempMaxF.hashCode());
|
||||
result = prime * result + ((lockedTempMinC == null) ? 0 : lockedTempMinC.hashCode());
|
||||
result = prime * result + ((lockedTempMinF == null) ? 0 : lockedTempMinF.hashCode());
|
||||
result = prime * result + ((previousHvacMode == null) ? 0 : previousHvacMode.hashCode());
|
||||
result = prime * result + ((sunlightCorrectionActive == null) ? 0 : sunlightCorrectionActive.hashCode());
|
||||
result = prime * result + ((sunlightCorrectionEnabled == null) ? 0 : sunlightCorrectionEnabled.hashCode());
|
||||
result = prime * result + ((targetTemperatureC == null) ? 0 : targetTemperatureC.hashCode());
|
||||
result = prime * result + ((targetTemperatureF == null) ? 0 : targetTemperatureF.hashCode());
|
||||
result = prime * result + ((targetTemperatureHighC == null) ? 0 : targetTemperatureHighC.hashCode());
|
||||
result = prime * result + ((targetTemperatureHighF == null) ? 0 : targetTemperatureHighF.hashCode());
|
||||
result = prime * result + ((targetTemperatureLowC == null) ? 0 : targetTemperatureLowC.hashCode());
|
||||
result = prime * result + ((targetTemperatureLowF == null) ? 0 : targetTemperatureLowF.hashCode());
|
||||
result = prime * result + ((temperatureScale == null) ? 0 : temperatureScale.hashCode());
|
||||
result = prime * result + ((timeToTarget == null) ? 0 : timeToTarget.hashCode());
|
||||
result = prime * result + ((whereName == null) ? 0 : whereName.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Thermostat [canCool=").append(canCool).append(", canHeat=").append(canHeat)
|
||||
.append(", isUsingEmergencyHeat=").append(isUsingEmergencyHeat).append(", hasFan=").append(hasFan)
|
||||
.append(", fanTimerActive=").append(fanTimerActive).append(", fanTimerTimeout=").append(fanTimerTimeout)
|
||||
.append(", hasLeaf=").append(hasLeaf).append(", temperatureScale=").append(temperatureScale)
|
||||
.append(", ambientTemperatureC=").append(ambientTemperatureC).append(", ambientTemperatureF=")
|
||||
.append(ambientTemperatureF).append(", humidity=").append(humidity).append(", targetTemperatureC=")
|
||||
.append(targetTemperatureC).append(", targetTemperatureF=").append(targetTemperatureF)
|
||||
.append(", targetTemperatureHighC=").append(targetTemperatureHighC).append(", targetTemperatureHighF=")
|
||||
.append(targetTemperatureHighF).append(", targetTemperatureLowC=").append(targetTemperatureLowC)
|
||||
.append(", targetTemperatureLowF=").append(targetTemperatureLowF).append(", hvacMode=").append(hvacMode)
|
||||
.append(", previousHvacMode=").append(previousHvacMode).append(", hvacState=").append(hvacState)
|
||||
.append(", ecoTemperatureHighC=").append(ecoTemperatureHighC).append(", ecoTemperatureHighF=")
|
||||
.append(ecoTemperatureHighF).append(", ecoTemperatureLowC=").append(ecoTemperatureLowC)
|
||||
.append(", ecoTemperatureLowF=").append(ecoTemperatureLowF).append(", isLocked=").append(isLocked)
|
||||
.append(", lockedTempMaxC=").append(lockedTempMaxC).append(", lockedTempMaxF=").append(lockedTempMaxF)
|
||||
.append(", lockedTempMinC=").append(lockedTempMinC).append(", lockedTempMinF=").append(lockedTempMinF)
|
||||
.append(", sunlightCorrectionEnabled=").append(sunlightCorrectionEnabled)
|
||||
.append(", sunlightCorrectionActive=").append(sunlightCorrectionActive).append(", fanTimerDuration=")
|
||||
.append(fanTimerDuration).append(", timeToTarget=").append(timeToTarget).append(", whereName=")
|
||||
.append(whereName).append(", getId()=").append(getId()).append(", getName()=").append(getName())
|
||||
.append(", getDeviceId()=").append(getDeviceId()).append(", getLastConnection()=")
|
||||
.append(getLastConnection()).append(", isOnline()=").append(isOnline()).append(", getNameLong()=")
|
||||
.append(getNameLong()).append(", getSoftwareVersion()=").append(getSoftwareVersion())
|
||||
.append(", getStructureId()=").append(getStructureId()).append(", getWhereId()=").append(getWhereId())
|
||||
.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Top level data for all the Nest stuff, this is the format the Nest data comes back from Nest in.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Add equals and hashCode methods
|
||||
*/
|
||||
public class TopLevelData {
|
||||
|
||||
private NestDevices devices;
|
||||
private NestMetadata metadata;
|
||||
private Map<String, Structure> structures;
|
||||
|
||||
public NestDevices getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public NestMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public Map<String, Structure> getStructures() {
|
||||
return structures;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TopLevelData other = (TopLevelData) obj;
|
||||
if (devices == null) {
|
||||
if (other.devices != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!devices.equals(other.devices)) {
|
||||
return false;
|
||||
}
|
||||
if (metadata == null) {
|
||||
if (other.metadata != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!metadata.equals(other.metadata)) {
|
||||
return false;
|
||||
}
|
||||
if (structures == null) {
|
||||
if (other.structures != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!structures.equals(other.structures)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((devices == null) ? 0 : devices.hashCode());
|
||||
result = prime * result + ((metadata == null) ? 0 : metadata.hashCode());
|
||||
result = prime * result + ((structures == null) ? 0 : structures.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("TopLevelData [devices=").append(devices).append(", metadata=").append(metadata)
|
||||
.append(", structures=").append(structures).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* The top level 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 {
|
||||
|
||||
private String path;
|
||||
private TopLevelData data;
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public TopLevelData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((data == null) ? 0 : data.hashCode());
|
||||
result = prime * result + ((path == null) ? 0 : path.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TopLevelStreamingData other = (TopLevelStreamingData) obj;
|
||||
if (data == null) {
|
||||
if (other.data != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!data.equals(other.data)) {
|
||||
return false;
|
||||
}
|
||||
if (path == null) {
|
||||
if (other.path != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!path.equals(other.path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("TopLevelStreamingData [path=").append(path).append(", data=").append(data).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.data;
|
||||
|
||||
/**
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Extract Where object from Structure
|
||||
* @author Wouter Born - Add equals, hashCode, toString methods
|
||||
*/
|
||||
public class Where {
|
||||
private String whereId;
|
||||
private String name;
|
||||
|
||||
public String getWhereId() {
|
||||
return whereId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Where other = (Where) obj;
|
||||
if (name == null) {
|
||||
if (other.name != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
if (whereId == null) {
|
||||
if (other.whereId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!whereId.equals(other.whereId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
result = prime * result + ((whereId == null) ? 0 : whereId.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Where [whereId=").append(whereId).append(", name=").append(name).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.exceptions;
|
||||
|
||||
/**
|
||||
* Will be thrown when the bridge was unable to resolve the Nest redirect URL.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Improve exception handling while sending data
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class FailedResolvingNestUrlException extends Exception {
|
||||
public FailedResolvingNestUrlException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FailedResolvingNestUrlException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FailedResolvingNestUrlException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.exceptions;
|
||||
|
||||
/**
|
||||
* Will be thrown when the bridge was unable to retrieve data.
|
||||
*
|
||||
* @author Martin van Wingerden - Initial contribution
|
||||
* @author Martin van Wingerden - Added more centralized handling of failure when retrieving data
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class FailedRetrievingNestDataException extends Exception {
|
||||
|
||||
public FailedRetrievingNestDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FailedRetrievingNestDataException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FailedRetrievingNestDataException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.exceptions;
|
||||
|
||||
/**
|
||||
* Will be thrown when the bridge was unable to send data.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Improve exception handling while sending data
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class FailedSendingNestDataException extends Exception {
|
||||
public FailedSendingNestDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FailedSendingNestDataException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FailedSendingNestDataException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.exceptions;
|
||||
|
||||
/**
|
||||
* Will be thrown when there is no valid access token and it was not possible to refresh it
|
||||
*
|
||||
* @author Martin van Wingerden - Initial contribution
|
||||
* @author Martin van Wingerden - Added more centralized handling of invalid access tokens
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class InvalidAccessTokenException extends Exception {
|
||||
public InvalidAccessTokenException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidAccessTokenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidAccessTokenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
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.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.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.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Deals with the structures on the Nest API, turning them into a thing in openHAB.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Martin van Wingerden - Splitted of NestBaseHandler
|
||||
* @author Wouter Born - Add generic update data type
|
||||
*
|
||||
* @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);
|
||||
|
||||
private @Nullable String deviceId;
|
||||
private Class<T> dataClass;
|
||||
|
||||
NestBaseHandler(Thing thing, Class<T> dataClass) {
|
||||
super(thing);
|
||||
this.dataClass = dataClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing handler for {}", getClass().getName());
|
||||
|
||||
NestBridgeHandler handler = getNestBridgeHandler();
|
||||
if (handler != null) {
|
||||
boolean success = handler.addThingDataListener(dataClass, getId(), this);
|
||||
logger.debug("Adding {} with ID '{}' as device data listener, result: {}", getClass().getSimpleName(),
|
||||
getId(), success);
|
||||
} else {
|
||||
logger.debug("Unable to add {} with ID '{}' as device data listener because bridge is null",
|
||||
getClass().getSimpleName(), getId());
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for refresh");
|
||||
|
||||
T lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null) {
|
||||
update(null, lastUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
NestBridgeHandler handler = getNestBridgeHandler();
|
||||
if (handler != null) {
|
||||
handler.removeThingDataListener(dataClass, getId(), this);
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable T getLastUpdate() {
|
||||
NestBridgeHandler handler = getNestBridgeHandler();
|
||||
if (handler != null) {
|
||||
return handler.getLastUpdate(dataClass, getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void addUpdateRequest(String updatePath, String field, Object value) {
|
||||
NestBridgeHandler handler = getNestBridgeHandler();
|
||||
if (handler != null) {
|
||||
// @formatter:off
|
||||
handler.addUpdateRequest(new NestUpdateRequest.Builder()
|
||||
.withBasePath(updatePath)
|
||||
.withIdentifier(getId())
|
||||
.withAdditionalValue(field, value)
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return getDeviceId();
|
||||
}
|
||||
|
||||
protected String getDeviceId() {
|
||||
String localDeviceId = deviceId;
|
||||
if (localDeviceId == null) {
|
||||
localDeviceId = getConfigAs(NestDeviceConfiguration.class).deviceId;
|
||||
deviceId = localDeviceId;
|
||||
}
|
||||
return localDeviceId;
|
||||
}
|
||||
|
||||
protected @Nullable NestBridgeHandler getNestBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
return bridge != null ? (NestBridgeHandler) bridge.getHandler() : null;
|
||||
}
|
||||
|
||||
protected abstract State getChannelState(ChannelUID channelUID, T data);
|
||||
|
||||
protected State getAsDateTimeTypeOrNull(@Nullable Date date) {
|
||||
if (date == null) {
|
||||
return UnDefType.NULL;
|
||||
}
|
||||
|
||||
long offsetMillis = TimeZone.getDefault().getOffset(date.getTime());
|
||||
Instant instant = date.toInstant().plusMillis(offsetMillis);
|
||||
return new DateTimeType(ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()));
|
||||
}
|
||||
|
||||
protected State getAsDecimalTypeOrNull(@Nullable Integer value) {
|
||||
return value == null ? UnDefType.NULL : new DecimalType(value);
|
||||
}
|
||||
|
||||
protected State getAsOnOffTypeOrNull(@Nullable Boolean value) {
|
||||
return value == null ? UnDefType.NULL : value ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
protected <U extends Quantity<U>> State getAsQuantityTypeOrNull(@Nullable Number value, Unit<U> unit) {
|
||||
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
protected State getAsStringTypeOrNull(@Nullable Object value) {
|
||||
return value == null ? UnDefType.NULL : new StringType(value.toString());
|
||||
}
|
||||
|
||||
protected State getAsStringTypeListOrNull(@Nullable Collection<?> values) {
|
||||
return values == null || values.isEmpty() ? UnDefType.NULL
|
||||
: new StringType(values.stream().map(v -> v.toString()).collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
protected boolean isNotHandling(NestIdentifiable 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewData(T data) {
|
||||
update(null, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdatedData(T oldData, T data) {
|
||||
update(oldData, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMissingData(String nestId) {
|
||||
thing.setStatusInfo(
|
||||
new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Missing from streaming updates"));
|
||||
}
|
||||
|
||||
protected abstract void update(T oldData, T data);
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.core.config.core.Configuration;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This bridge handler connects to Nest and handles all the 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.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Martin van Wingerden - Use listeners not only for discovery but for all data processing
|
||||
* @author Wouter Born - Improve exception and URL redirect handling
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener {
|
||||
|
||||
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class);
|
||||
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final SseEventSourceFactory eventSourceFactory;
|
||||
private final List<NestUpdateRequest> nestUpdateRequests = new CopyOnWriteArrayList<>();
|
||||
private final NestCompositeUpdateHandler updateHandler = new NestCompositeUpdateHandler(
|
||||
this::getPresentThingsNestIds);
|
||||
|
||||
private @NonNullByDefault({}) NestAuthorizer authorizer;
|
||||
private @NonNullByDefault({}) NestBridgeConfiguration config;
|
||||
|
||||
private @Nullable ScheduledFuture<?> initializeJob;
|
||||
private @Nullable ScheduledFuture<?> transmitJob;
|
||||
private @Nullable NestRedirectUrlSupplier redirectUrlSupplier;
|
||||
private @Nullable NestStreamingRestClient 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) {
|
||||
super(bridge);
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.eventSourceFactory = eventSourceFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the connection to Nest.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Nest bridge handler");
|
||||
|
||||
config = getConfigAs(NestBridgeConfiguration.class);
|
||||
authorizer = new NestAuthorizer(config);
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query");
|
||||
|
||||
initializeJob = scheduler.schedule(() -> {
|
||||
try {
|
||||
logger.debug("Product ID {}", config.productId);
|
||||
logger.debug("Product Secret {}", config.productSecret);
|
||||
logger.debug("Pincode {}", config.pincode);
|
||||
logger.debug("Access Token {}", getExistingOrNewAccessToken());
|
||||
redirectUrlSupplier = createRedirectUrlSupplier();
|
||||
restartStreamingUpdates();
|
||||
} catch (InvalidAccessTokenException e) {
|
||||
logger.debug("Invalid access token", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Token is invalid and could not be refreshed: " + e.getMessage());
|
||||
}
|
||||
}, 0, TimeUnit.SECONDS);
|
||||
|
||||
logger.debug("Finished initializing Nest bridge handler");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the handler.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Nest bridge disposed");
|
||||
stopStreamingUpdates();
|
||||
|
||||
ScheduledFuture<?> localInitializeJob = initializeJob;
|
||||
if (localInitializeJob != null && !localInitializeJob.isCancelled()) {
|
||||
localInitializeJob.cancel(true);
|
||||
initializeJob = null;
|
||||
}
|
||||
|
||||
ScheduledFuture<?> localTransmitJob = transmitJob;
|
||||
if (localTransmitJob != null && !localTransmitJob.isCancelled()) {
|
||||
localTransmitJob.cancel(true);
|
||||
transmitJob = null;
|
||||
}
|
||||
|
||||
this.authorizer = null;
|
||||
this.redirectUrlSupplier = null;
|
||||
this.streamingRestClient = null;
|
||||
}
|
||||
|
||||
public <T> boolean addThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
|
||||
return updateHandler.addListener(dataClass, listener);
|
||||
}
|
||||
|
||||
public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<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) {
|
||||
nestUpdateRequests.add(request);
|
||||
scheduleTransmitJobForPendingRequests();
|
||||
}
|
||||
|
||||
protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
|
||||
return new NestRedirectUrlSupplier(getHttpHeaders());
|
||||
}
|
||||
|
||||
private String getExistingOrNewAccessToken() throws InvalidAccessTokenException {
|
||||
String accessToken = config.accessToken;
|
||||
if (accessToken == null || accessToken.isEmpty()) {
|
||||
accessToken = authorizer.getNewAccessToken();
|
||||
config.accessToken = accessToken;
|
||||
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);
|
||||
updateConfiguration(configuration);
|
||||
logger.debug("Retrieved new access token: {}", config.accessToken);
|
||||
return accessToken;
|
||||
} else {
|
||||
logger.debug("Re-using access token from configuration: {}", accessToken);
|
||||
return accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
protected Properties getHttpHeaders() throws InvalidAccessTokenException {
|
||||
Properties httpHeaders = new Properties();
|
||||
httpHeaders.put("Authorization", "Bearer " + getExistingOrNewAccessToken());
|
||||
httpHeaders.put("Content-Type", JSON_CONTENT_TYPE);
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
|
||||
return updateHandler.getLastUpdate(dataClass, nestId);
|
||||
}
|
||||
|
||||
public <T> List<T> getLastUpdates(Class<T> dataClass) {
|
||||
return updateHandler.getLastUpdates(dataClass);
|
||||
}
|
||||
|
||||
private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException {
|
||||
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
|
||||
if (localRedirectUrlSupplier == null) {
|
||||
localRedirectUrlSupplier = createRedirectUrlSupplier();
|
||||
redirectUrlSupplier = localRedirectUrlSupplier;
|
||||
}
|
||||
return localRedirectUrlSupplier;
|
||||
}
|
||||
|
||||
private Set<String> getPresentThingsNestIds() {
|
||||
Set<String> nestIds = new HashSet<>();
|
||||
for (Thing thing : getThing().getThings()) {
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler != null && thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.GONE) {
|
||||
nestIds.add(((NestIdentifiable) handler).getId());
|
||||
}
|
||||
}
|
||||
return nestIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an incoming command update
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
logger.debug("Refresh command received");
|
||||
updateHandler.resendLastUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void jsonToPutUrl(NestUpdateRequest request)
|
||||
throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException {
|
||||
try {
|
||||
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
|
||||
if (localRedirectUrlSupplier == null) {
|
||||
throw new FailedResolvingNestUrlException("redirectUrlSupplier is null");
|
||||
}
|
||||
|
||||
String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath();
|
||||
logger.debug("Putting data to: {}", url);
|
||||
|
||||
String jsonContent = NestUtils.toJson(request.getValues());
|
||||
logger.debug("PUT content: {}", jsonContent);
|
||||
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8));
|
||||
String jsonResponse = HttpUtil.executeUrl("PUT", url, getHttpHeaders(), inputStream, JSON_CONTENT_TYPE,
|
||||
REQUEST_TIMEOUT);
|
||||
logger.debug("PUT response: {}", jsonResponse);
|
||||
|
||||
ErrorData error = NestUtils.fromJson(jsonResponse, ErrorData.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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthorizationRevoked(String token) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Authorization token revoked: " + token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected() {
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Streaming data connection established");
|
||||
scheduleTransmitJobForPendingRequests();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Streaming data disconnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String message) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTopLevelData(TopLevelData data) {
|
||||
updateHandler.handleUpdate(data);
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data");
|
||||
}
|
||||
|
||||
public <T> boolean removeThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
|
||||
return updateHandler.removeListener(dataClass, listener);
|
||||
}
|
||||
|
||||
public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
|
||||
return updateHandler.removeListener(dataClass, nestId, listener);
|
||||
}
|
||||
|
||||
private void restartStreamingUpdates() {
|
||||
synchronized (this) {
|
||||
stopStreamingUpdates();
|
||||
startStreamingUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTransmitJobForPendingRequests() {
|
||||
ScheduledFuture<?> localTransmitJob = transmitJob;
|
||||
if (!nestUpdateRequests.isEmpty() && (localTransmitJob == null || localTransmitJob.isDone())) {
|
||||
transmitJob = scheduler.schedule(this::transmitQueue, 0, SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void startStreamingUpdates() {
|
||||
synchronized (this) {
|
||||
try {
|
||||
NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient(
|
||||
getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory,
|
||||
getOrCreateRedirectUrlSupplier(), scheduler);
|
||||
localStreamingRestClient.addStreamingDataListener(this);
|
||||
localStreamingRestClient.start();
|
||||
|
||||
streamingRestClient = localStreamingRestClient;
|
||||
} catch (InvalidAccessTokenException e) {
|
||||
logger.debug("Invalid access token", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Token is invalid and could not be refreshed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopStreamingUpdates() {
|
||||
NestStreamingRestClient localStreamingRestClient = streamingRestClient;
|
||||
if (localStreamingRestClient != null) {
|
||||
synchronized (this) {
|
||||
localStreamingRestClient.stop();
|
||||
localStreamingRestClient.removeStreamingDataListener(this);
|
||||
streamingRestClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void transmitQueue() {
|
||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Not transmitting events because bridge is OFFLINE");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (!nestUpdateRequests.isEmpty()) {
|
||||
// nestUpdateRequests is a CopyOnWriteArrayList so its iterator does not support remove operations
|
||||
NestUpdateRequest request = nestUpdateRequests.get(0);
|
||||
jsonToPutUrl(request);
|
||||
nestUpdateRequests.remove(request);
|
||||
}
|
||||
} catch (InvalidAccessTokenException 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) {
|
||||
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) {
|
||||
logger.debug("Error sending data", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
|
||||
|
||||
NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
|
||||
if (localRedirectUrlSupplier != null) {
|
||||
localRedirectUrlSupplier.resetCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
|
||||
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.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
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.
|
||||
*
|
||||
* @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 NestCameraHandler(Thing thing) {
|
||||
super(thing, Camera.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State getChannelState(ChannelUID channelUID, Camera camera) {
|
||||
if (channelUID.getId().startsWith(CHANNEL_GROUP_CAMERA_PREFIX)) {
|
||||
return getCameraChannelState(channelUID, camera);
|
||||
} else if (channelUID.getId().startsWith(CHANNEL_GROUP_LAST_EVENT_PREFIX)) {
|
||||
return getLastEventChannelState(channelUID, camera);
|
||||
} else {
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
protected State getCameraChannelState(ChannelUID channelUID, Camera camera) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CAMERA_APP_URL:
|
||||
return getAsStringTypeOrNull(camera.getAppUrl());
|
||||
case CHANNEL_CAMERA_AUDIO_INPUT_ENABLED:
|
||||
return getAsOnOffTypeOrNull(camera.isAudioInputEnabled());
|
||||
case CHANNEL_CAMERA_LAST_ONLINE_CHANGE:
|
||||
return getAsDateTimeTypeOrNull(camera.getLastIsOnlineChange());
|
||||
case CHANNEL_CAMERA_PUBLIC_SHARE_ENABLED:
|
||||
return getAsOnOffTypeOrNull(camera.isPublicShareEnabled());
|
||||
case CHANNEL_CAMERA_PUBLIC_SHARE_URL:
|
||||
return getAsStringTypeOrNull(camera.getPublicShareUrl());
|
||||
case CHANNEL_CAMERA_SNAPSHOT_URL:
|
||||
return getAsStringTypeOrNull(camera.getSnapshotUrl());
|
||||
case CHANNEL_CAMERA_STREAMING:
|
||||
return getAsOnOffTypeOrNull(camera.isStreaming());
|
||||
case CHANNEL_CAMERA_VIDEO_HISTORY_ENABLED:
|
||||
return getAsOnOffTypeOrNull(camera.isVideoHistoryEnabled());
|
||||
case CHANNEL_CAMERA_WEB_URL:
|
||||
return getAsStringTypeOrNull(camera.getWebUrl());
|
||||
default:
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
protected State getLastEventChannelState(ChannelUID channelUID, Camera camera) {
|
||||
CameraEvent lastEvent = camera.getLastEvent();
|
||||
if (lastEvent == null) {
|
||||
return UnDefType.NULL;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_LAST_EVENT_ACTIVITY_ZONES:
|
||||
return getAsStringTypeListOrNull(lastEvent.getActivityZones());
|
||||
case CHANNEL_LAST_EVENT_ANIMATED_IMAGE_URL:
|
||||
return getAsStringTypeOrNull(lastEvent.getAnimatedImageUrl());
|
||||
case CHANNEL_LAST_EVENT_APP_URL:
|
||||
return getAsStringTypeOrNull(lastEvent.getAppUrl());
|
||||
case CHANNEL_LAST_EVENT_END_TIME:
|
||||
return getAsDateTimeTypeOrNull(lastEvent.getEndTime());
|
||||
case CHANNEL_LAST_EVENT_HAS_MOTION:
|
||||
return getAsOnOffTypeOrNull(lastEvent.isHasMotion());
|
||||
case CHANNEL_LAST_EVENT_HAS_PERSON:
|
||||
return getAsOnOffTypeOrNull(lastEvent.isHasPerson());
|
||||
case CHANNEL_LAST_EVENT_HAS_SOUND:
|
||||
return getAsOnOffTypeOrNull(lastEvent.isHasSound());
|
||||
case CHANNEL_LAST_EVENT_IMAGE_URL:
|
||||
return getAsStringTypeOrNull(lastEvent.getImageUrl());
|
||||
case CHANNEL_LAST_EVENT_START_TIME:
|
||||
return getAsDateTimeTypeOrNull(lastEvent.getStartTime());
|
||||
case CHANNEL_LAST_EVENT_URLS_EXPIRE_TIME:
|
||||
return getAsDateTimeTypeOrNull(lastEvent.getUrlsExpireTime());
|
||||
case CHANNEL_LAST_EVENT_WEB_URL:
|
||||
return getAsStringTypeOrNull(lastEvent.getWebUrl());
|
||||
default:
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (REFRESH.equals(command)) {
|
||||
Camera lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null) {
|
||||
updateState(channelUID, getChannelState(channelUID, lastUpdate));
|
||||
}
|
||||
} else if (CHANNEL_CAMERA_STREAMING.equals(channelUID.getId())) {
|
||||
// Change the mode.
|
||||
if (command instanceof OnOffType) {
|
||||
// Set the mode to be the cmd value.
|
||||
addUpdateRequest("is_streaming", command == OnOffType.ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addUpdateRequest(String field, Object value) {
|
||||
addUpdateRequest(NEST_CAMERA_UPDATE_PATH, field, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void update(Camera oldCamera, Camera camera) {
|
||||
logger.debug("Updating {}", getThing().getUID());
|
||||
|
||||
updateLinkedChannels(oldCamera, camera);
|
||||
updateProperty(PROPERTY_FIRMWARE_VERSION, camera.getSoftwareVersion());
|
||||
|
||||
ThingStatus newStatus = camera.isOnline() == null ? ThingStatus.UNKNOWN
|
||||
: camera.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
|
||||
if (newStatus != thing.getStatus()) {
|
||||
updateStatus(newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
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.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
|
||||
* 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 {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NestRedirectUrlSupplier.class);
|
||||
|
||||
protected String cachedUrl = "";
|
||||
|
||||
protected Properties httpHeaders;
|
||||
|
||||
public NestRedirectUrlSupplier(Properties httpHeaders) {
|
||||
this.httpHeaders = httpHeaders;
|
||||
}
|
||||
|
||||
public String getRedirectUrl() throws FailedResolvingNestUrlException {
|
||||
if (cachedUrl.isEmpty()) {
|
||||
cachedUrl = resolveRedirectUrl();
|
||||
}
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
public void resetCache() {
|
||||
cachedUrl = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the redirect URL for calls using the {@link NestBindingConstants#NEST_URL}.
|
||||
*
|
||||
* The Jetty client used by {@link HttpUtil} will not pass the Authorization header after a redirect resulting in
|
||||
* "401 Unauthorized error" issues.
|
||||
*
|
||||
* Note that this workaround currently does not use any configured proxy like {@link HttpUtil} does.
|
||||
*
|
||||
* @see https://developers.nest.com/documentation/cloud/how-to-handle-redirects
|
||||
*/
|
||||
private String resolveRedirectUrl() throws FailedResolvingNestUrlException {
|
||||
HttpClient httpClient = new HttpClient(new SslContextFactory());
|
||||
httpClient.setFollowRedirects(false);
|
||||
|
||||
Request request = httpClient.newRequest(NestBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30,
|
||||
TimeUnit.SECONDS);
|
||||
for (String httpHeaderKey : httpHeaders.stringPropertyNames()) {
|
||||
request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey));
|
||||
}
|
||||
|
||||
ContentResponse response;
|
||||
try {
|
||||
httpClient.start();
|
||||
response = request.send();
|
||||
httpClient.stop();
|
||||
} catch (Exception e) {
|
||||
throw new FailedResolvingNestUrlException("Failed to resolve redirect URL: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
int status = response.getStatus();
|
||||
String redirectUrl = response.getHeaders().get(HttpHeader.LOCATION);
|
||||
|
||||
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 "
|
||||
+ HttpStatus.TEMPORARY_REDIRECT_307 + " but was " + status);
|
||||
} else if (redirectUrl == null || redirectUrl.isEmpty()) {
|
||||
throw new FailedResolvingNestUrlException("Redirect URL is empty");
|
||||
}
|
||||
|
||||
redirectUrl = redirectUrl.endsWith("/") ? redirectUrl.substring(0, redirectUrl.length() - 1) : redirectUrl;
|
||||
logger.debug("Redirect URL: {}", redirectUrl);
|
||||
return redirectUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
|
||||
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.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The smoke detector handler, it handles the data from Nest 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 NestSmokeDetectorHandler(Thing thing) {
|
||||
super(thing, SmokeDetector.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State getChannelState(ChannelUID channelUID, SmokeDetector smokeDetector) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CO_ALARM_STATE:
|
||||
return getAsStringTypeOrNull(smokeDetector.getCoAlarmState());
|
||||
case CHANNEL_LAST_CONNECTION:
|
||||
return getAsDateTimeTypeOrNull(smokeDetector.getLastConnection());
|
||||
case CHANNEL_LAST_MANUAL_TEST_TIME:
|
||||
return getAsDateTimeTypeOrNull(smokeDetector.getLastManualTestTime());
|
||||
case CHANNEL_LOW_BATTERY:
|
||||
return getAsOnOffTypeOrNull(smokeDetector.getBatteryHealth() == null ? null
|
||||
: smokeDetector.getBatteryHealth() == BatteryHealth.REPLACE);
|
||||
case CHANNEL_MANUAL_TEST_ACTIVE:
|
||||
return getAsOnOffTypeOrNull(smokeDetector.isManualTestActive());
|
||||
case CHANNEL_SMOKE_ALARM_STATE:
|
||||
return getAsStringTypeOrNull(smokeDetector.getSmokeAlarmState());
|
||||
case CHANNEL_UI_COLOR_STATE:
|
||||
return getAsStringTypeOrNull(smokeDetector.getUiColorState());
|
||||
default:
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any incoming command requests.
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (REFRESH.equals(command)) {
|
||||
SmokeDetector lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null) {
|
||||
updateState(channelUID, getChannelState(channelUID, lastUpdate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void update(SmokeDetector oldSmokeDetector, SmokeDetector smokeDetector) {
|
||||
logger.debug("Updating {}", getThing().getUID());
|
||||
|
||||
updateLinkedChannels(oldSmokeDetector, smokeDetector);
|
||||
updateProperty(PROPERTY_FIRMWARE_VERSION, smokeDetector.getSoftwareVersion());
|
||||
|
||||
ThingStatus newStatus = smokeDetector.isOnline() == null ? ThingStatus.UNKNOWN
|
||||
: smokeDetector.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
|
||||
if (newStatus != thing.getStatus()) {
|
||||
updateStatus(newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
|
||||
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.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Deals with the structures on the Nest 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);
|
||||
|
||||
private @Nullable String structureId;
|
||||
|
||||
public NestStructureHandler(Thing thing) {
|
||||
super(thing, Structure.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State getChannelState(ChannelUID channelUID, Structure structure) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_AWAY:
|
||||
return getAsStringTypeOrNull(structure.getAway());
|
||||
case CHANNEL_CO_ALARM_STATE:
|
||||
return getAsStringTypeOrNull(structure.getCoAlarmState());
|
||||
case CHANNEL_COUNTRY_CODE:
|
||||
return getAsStringTypeOrNull(structure.getCountryCode());
|
||||
case CHANNEL_ETA_BEGIN:
|
||||
return getAsDateTimeTypeOrNull(structure.getEtaBegin());
|
||||
case CHANNEL_PEAK_PERIOD_END_TIME:
|
||||
return getAsDateTimeTypeOrNull(structure.getPeakPeriodEndTime());
|
||||
case CHANNEL_PEAK_PERIOD_START_TIME:
|
||||
return getAsDateTimeTypeOrNull(structure.getPeakPeriodStartTime());
|
||||
case CHANNEL_POSTAL_CODE:
|
||||
return getAsStringTypeOrNull(structure.getPostalCode());
|
||||
case CHANNEL_RUSH_HOUR_REWARDS_ENROLLMENT:
|
||||
return getAsOnOffTypeOrNull(structure.isRhrEnrollment());
|
||||
case CHANNEL_SECURITY_STATE:
|
||||
return getAsStringTypeOrNull(structure.getWwnSecurityState());
|
||||
case CHANNEL_SMOKE_ALARM_STATE:
|
||||
return getAsStringTypeOrNull(structure.getSmokeAlarmState());
|
||||
case CHANNEL_TIME_ZONE:
|
||||
return getAsStringTypeOrNull(structure.getTimeZone());
|
||||
default:
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return getStructureId();
|
||||
}
|
||||
|
||||
private String getStructureId() {
|
||||
String localStructureId = structureId;
|
||||
if (localStructureId == null) {
|
||||
localStructureId = getConfigAs(NestStructureConfiguration.class).structureId;
|
||||
structureId = localStructureId;
|
||||
}
|
||||
return localStructureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updating the details on this structure by sending the request all the way
|
||||
* to Nest.
|
||||
*
|
||||
* @param channelUID the channel to update
|
||||
* @param command the command to apply
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (REFRESH.equals(command)) {
|
||||
Structure lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null) {
|
||||
updateState(channelUID, getChannelState(channelUID, lastUpdate));
|
||||
}
|
||||
} else if (CHANNEL_AWAY.equals(channelUID.getId())) {
|
||||
// Change the home/away state.
|
||||
if (command instanceof StringType) {
|
||||
StringType cmd = (StringType) command;
|
||||
// Set the mode to be the cmd value.
|
||||
addUpdateRequest(NEST_STRUCTURE_UPDATE_PATH, "away", HomeAwayState.valueOf(cmd.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void update(Structure oldStructure, Structure structure) {
|
||||
logger.debug("Updating {}", getThing().getUID());
|
||||
|
||||
updateLinkedChannels(oldStructure, structure);
|
||||
|
||||
if (ThingStatus.ONLINE != thing.getStatus()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.handler;
|
||||
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
|
||||
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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
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.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NestThermostatHandler} 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 NestThermostatHandler(Thing thing) {
|
||||
super(thing, Thermostat.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State getChannelState(ChannelUID channelUID, Thermostat thermostat) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CAN_COOL:
|
||||
return getAsOnOffTypeOrNull(thermostat.isCanCool());
|
||||
case CHANNEL_CAN_HEAT:
|
||||
return getAsOnOffTypeOrNull(thermostat.isCanHeat());
|
||||
case CHANNEL_ECO_MAX_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getEcoTemperatureHigh(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_ECO_MIN_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getEcoTemperatureLow(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_FAN_TIMER_ACTIVE:
|
||||
return getAsOnOffTypeOrNull(thermostat.isFanTimerActive());
|
||||
case CHANNEL_FAN_TIMER_DURATION:
|
||||
return getAsQuantityTypeOrNull(thermostat.getFanTimerDuration(), SmartHomeUnits.MINUTE);
|
||||
case CHANNEL_FAN_TIMER_TIMEOUT:
|
||||
return getAsDateTimeTypeOrNull(thermostat.getFanTimerTimeout());
|
||||
case CHANNEL_HAS_FAN:
|
||||
return getAsOnOffTypeOrNull(thermostat.isHasFan());
|
||||
case CHANNEL_HAS_LEAF:
|
||||
return getAsOnOffTypeOrNull(thermostat.isHasLeaf());
|
||||
case CHANNEL_HUMIDITY:
|
||||
return getAsQuantityTypeOrNull(thermostat.getHumidity(), SmartHomeUnits.PERCENT);
|
||||
case CHANNEL_LAST_CONNECTION:
|
||||
return getAsDateTimeTypeOrNull(thermostat.getLastConnection());
|
||||
case CHANNEL_LOCKED:
|
||||
return getAsOnOffTypeOrNull(thermostat.isLocked());
|
||||
case CHANNEL_LOCKED_MAX_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getLockedTempMax(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_LOCKED_MIN_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getLockedTempMin(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_MAX_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getTargetTemperatureHigh(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_MIN_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getTargetTemperatureLow(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_MODE:
|
||||
return getAsStringTypeOrNull(thermostat.getMode());
|
||||
case CHANNEL_PREVIOUS_MODE:
|
||||
Mode previousMode = thermostat.getPreviousHvacMode() != null ? thermostat.getPreviousHvacMode()
|
||||
: thermostat.getMode();
|
||||
return getAsStringTypeOrNull(previousMode);
|
||||
case CHANNEL_STATE:
|
||||
return getAsStringTypeOrNull(thermostat.getHvacState());
|
||||
case CHANNEL_SET_POINT:
|
||||
return getAsQuantityTypeOrNull(thermostat.getTargetTemperature(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_SUNLIGHT_CORRECTION_ACTIVE:
|
||||
return getAsOnOffTypeOrNull(thermostat.isSunlightCorrectionActive());
|
||||
case CHANNEL_SUNLIGHT_CORRECTION_ENABLED:
|
||||
return getAsOnOffTypeOrNull(thermostat.isSunlightCorrectionEnabled());
|
||||
case CHANNEL_TEMPERATURE:
|
||||
return getAsQuantityTypeOrNull(thermostat.getAmbientTemperature(), thermostat.getTemperatureUnit());
|
||||
case CHANNEL_TIME_TO_TARGET:
|
||||
return getAsQuantityTypeOrNull(thermostat.getTimeToTarget(), SmartHomeUnits.MINUTE);
|
||||
case CHANNEL_USING_EMERGENCY_HEAT:
|
||||
return getAsOnOffTypeOrNull(thermostat.isUsingEmergencyHeat());
|
||||
default:
|
||||
logger.error("Unsupported channelId '{}'", channelUID.getId());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command to do things to the thermostat, this will change the
|
||||
* value of a channel by sending the request to Nest.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (REFRESH.equals(command)) {
|
||||
Thermostat lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null) {
|
||||
updateState(channelUID, getChannelState(channelUID, lastUpdate));
|
||||
}
|
||||
} else if (CHANNEL_FAN_TIMER_ACTIVE.equals(channelUID.getId())) {
|
||||
if (command instanceof OnOffType) {
|
||||
// Update fan timer active to the command value
|
||||
addUpdateRequest("fan_timer_active", command == OnOffType.ON);
|
||||
}
|
||||
} else if (CHANNEL_FAN_TIMER_DURATION.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType) {
|
||||
// Update fan timer duration to the command value
|
||||
QuantityType<Time> minuteQuantity = ((QuantityType<Time>) command).toUnit(SmartHomeUnits.MINUTE);
|
||||
if (minuteQuantity != null) {
|
||||
addUpdateRequest("fan_timer_duration", minuteQuantity.intValue());
|
||||
}
|
||||
}
|
||||
} else if (CHANNEL_MAX_SET_POINT.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType) {
|
||||
// Update maximum set point to the command value
|
||||
addTemperatureUpdateRequest("target_temperature_high_c", "target_temperature_high_f",
|
||||
(QuantityType<Temperature>) command);
|
||||
}
|
||||
} else if (CHANNEL_MIN_SET_POINT.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType) {
|
||||
// Update minimum set point to the command value
|
||||
addTemperatureUpdateRequest("target_temperature_low_c", "target_temperature_low_f",
|
||||
(QuantityType<Temperature>) command);
|
||||
}
|
||||
} else if (CHANNEL_MODE.equals(channelUID.getId())) {
|
||||
if (command instanceof StringType) {
|
||||
// Update the HVAC mode to the command value
|
||||
addUpdateRequest("hvac_mode", Mode.valueOf(((StringType) command).toString()));
|
||||
}
|
||||
} else if (CHANNEL_SET_POINT.equals(channelUID.getId())) {
|
||||
if (command instanceof QuantityType) {
|
||||
// Update set point to the command value
|
||||
addTemperatureUpdateRequest("target_temperature_c", "target_temperature_f",
|
||||
(QuantityType<Temperature>) command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addUpdateRequest(String field, Object value) {
|
||||
addUpdateRequest(NEST_THERMOSTAT_UPDATE_PATH, field, value);
|
||||
}
|
||||
|
||||
private void addTemperatureUpdateRequest(String celsiusField, String fahrenheitField,
|
||||
QuantityType<Temperature> quantity) {
|
||||
Unit<Temperature> unit = getTemperatureUnit(quantity.getUnit());
|
||||
BigDecimal value = quantityToRoundedTemperature(quantity, unit);
|
||||
if (value != null) {
|
||||
addUpdateRequest(NEST_THERMOSTAT_UPDATE_PATH, unit == CELSIUS ? celsiusField : fahrenheitField, value);
|
||||
}
|
||||
}
|
||||
|
||||
private Unit<Temperature> getTemperatureUnit(Unit<Temperature> fallbackUnit) {
|
||||
Thermostat lastUpdate = getLastUpdate();
|
||||
if (lastUpdate != null && lastUpdate.getTemperatureUnit() != null) {
|
||||
return lastUpdate.getTemperatureUnit();
|
||||
}
|
||||
|
||||
return fallbackUnit;
|
||||
}
|
||||
|
||||
private @Nullable BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity,
|
||||
Unit<Temperature> unit) throws IllegalArgumentException {
|
||||
QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
|
||||
if (temparatureQuantity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal value = temparatureQuantity.toBigDecimal();
|
||||
BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
|
||||
BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
|
||||
return divisor.multiply(increment);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void update(Thermostat oldThermostat, Thermostat thermostat) {
|
||||
logger.debug("Updating {}", getThing().getUID());
|
||||
|
||||
updateLinkedChannels(oldThermostat, thermostat);
|
||||
updateProperty(PROPERTY_FIRMWARE_VERSION, thermostat.getSoftwareVersion());
|
||||
|
||||
ThingStatus newStatus = thermostat.isOnline() == null ? ThingStatus.UNKNOWN
|
||||
: thermostat.isOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
|
||||
if (newStatus != thing.getStatus()) {
|
||||
updateStatus(newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.listener;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nest.internal.data.TopLevelData;
|
||||
import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
|
||||
|
||||
/**
|
||||
* Interface for listeners of events generated by the {@link NestStreamingRestClient}.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Replace polling with REST streaming
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface NestStreamingDataListener {
|
||||
|
||||
/**
|
||||
* Authorization has been revoked for a token.
|
||||
*/
|
||||
void onAuthorizationRevoked(String token);
|
||||
|
||||
/**
|
||||
* The client successfully established a connection.
|
||||
*/
|
||||
void onConnected();
|
||||
|
||||
/**
|
||||
* The client was disconnected.
|
||||
*/
|
||||
void onDisconnected();
|
||||
|
||||
/**
|
||||
* An error message was published.
|
||||
*/
|
||||
void onError(String message);
|
||||
|
||||
/**
|
||||
* Initial {@link TopLevelData} or an update is sent.
|
||||
*/
|
||||
void onNewTopLevelData(TopLevelData data);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.listener;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Used to track incoming data for Nest things.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface NestThingDataListener<T> {
|
||||
|
||||
/**
|
||||
* An initial value for the data was received or the value is send again due to a refresh.
|
||||
*
|
||||
* @param data the data
|
||||
*/
|
||||
void onNewData(T data);
|
||||
|
||||
/**
|
||||
* Existing data was updated to a new value.
|
||||
*
|
||||
* @param oldData the previous value
|
||||
* @param data the current value
|
||||
*/
|
||||
void onUpdatedData(T oldData, T data);
|
||||
|
||||
/**
|
||||
* A Nest thing which previously had data is missing. E.g. it was removed from the account.
|
||||
*
|
||||
* @param nestId identifies the Nest thing
|
||||
*/
|
||||
void onMissingData(String nestId);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.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.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.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
* @author Wouter Born - Improve exception handling
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestAuthorizer {
|
||||
private final Logger logger = LoggerFactory.getLogger(NestAuthorizer.class);
|
||||
|
||||
private final NestBridgeConfiguration config;
|
||||
|
||||
/**
|
||||
* Create the helper class for the Nest access token. Also creates the folder
|
||||
* to put the access token data in if it does not already exist.
|
||||
*
|
||||
* @param config The configuration to use for the token
|
||||
*/
|
||||
public NestAuthorizer(NestBridgeConfiguration 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
|
||||
*/
|
||||
public String getNewAccessToken() throws InvalidAccessTokenException {
|
||||
try {
|
||||
String pincode = config.pincode;
|
||||
if (pincode == null || pincode.isBlank()) {
|
||||
throw new InvalidAccessTokenException("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)
|
||||
.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);
|
||||
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)");
|
||||
}
|
||||
return accessToken;
|
||||
} catch (IOException e) {
|
||||
throw new InvalidAccessTokenException("Access token request failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.rest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.client.ClientRequestContext;
|
||||
import javax.ws.rs.client.ClientRequestFilter;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
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.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Replace polling with REST streaming
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestStreamingRequestFilter implements ClientRequestFilter {
|
||||
private final String accessToken;
|
||||
|
||||
public NestStreamingRequestFilter(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(@Nullable ClientRequestContext requestContext) throws IOException {
|
||||
if (requestContext != null) {
|
||||
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
|
||||
headers.putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
|
||||
headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.rest;
|
||||
|
||||
import static org.openhab.binding.nest.internal.NestBindingConstants.KEEP_ALIVE_MILLIS;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.sse.InboundSseEvent;
|
||||
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.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).
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
* @author Wouter Born - Replace polling with REST streaming
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestStreamingRestClient {
|
||||
|
||||
// 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;
|
||||
|
||||
public static final String AUTH_REVOKED = "auth_revoked";
|
||||
public static final String ERROR = "error";
|
||||
public static final String KEEP_ALIVE = "keep-alive";
|
||||
public static final String OPEN = "open";
|
||||
public static final String PUT = "put";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NestStreamingRestClient.class);
|
||||
|
||||
private final String accessToken;
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final SseEventSourceFactory eventSourceFactory;
|
||||
private final NestRedirectUrlSupplier redirectUrlSupplier;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private final Object startStopLock = new Object();
|
||||
private final List<NestStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
private @Nullable ScheduledFuture<?> checkConnectionJob;
|
||||
private boolean connected;
|
||||
private @Nullable SseEventSource eventSource;
|
||||
private long lastEventTimestamp;
|
||||
private @Nullable TopLevelData lastReceivedTopLevelData;
|
||||
|
||||
public NestStreamingRestClient(String accessToken, ClientBuilder clientBuilder,
|
||||
SseEventSourceFactory eventSourceFactory, NestRedirectUrlSupplier redirectUrlSupplier,
|
||||
ScheduledExecutorService scheduler) {
|
||||
this.accessToken = accessToken;
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.eventSourceFactory = eventSourceFactory;
|
||||
this.redirectUrlSupplier = redirectUrlSupplier;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
private SseEventSource createEventSource() throws FailedResolvingNestUrlException {
|
||||
Client client = clientBuilder.register(new NestStreamingRequestFilter(accessToken)).build();
|
||||
SseEventSource eventSource = eventSourceFactory.newSource(client.target(redirectUrlSupplier.getRedirectUrl()));
|
||||
eventSource.register(this::onEvent, this::onError);
|
||||
return eventSource;
|
||||
}
|
||||
|
||||
private void checkConnection() {
|
||||
long millisSinceLastEvent = System.currentTimeMillis() - lastEventTimestamp;
|
||||
if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
|
||||
logger.debug("Check: Disconnected from streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
|
||||
synchronized (startStopLock) {
|
||||
stopCheckConnectionJob(false);
|
||||
if (connected) {
|
||||
connected = false;
|
||||
listeners.forEach(listener -> listener.onDisconnected());
|
||||
}
|
||||
redirectUrlSupplier.resetCache();
|
||||
reopenEventSource();
|
||||
startCheckConnectionJob();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the existing EventSource and opens a new EventSource as workaround when the EventSource fails to reconnect
|
||||
* itself.
|
||||
*/
|
||||
private void reopenEventSource() {
|
||||
try {
|
||||
logger.debug("Reopening EventSource");
|
||||
closeEventSource(10, TimeUnit.SECONDS);
|
||||
|
||||
logger.debug("Opening new EventSource");
|
||||
SseEventSource localEventSource = createEventSource();
|
||||
localEventSource.open();
|
||||
|
||||
eventSource = localEventSource;
|
||||
} catch (FailedResolvingNestUrlException e) {
|
||||
logger.debug("Failed to resolve Nest redirect URL while opening new EventSource");
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
synchronized (startStopLock) {
|
||||
logger.debug("Opening EventSource and starting checkConnection job");
|
||||
reopenEventSource();
|
||||
startCheckConnectionJob();
|
||||
logger.debug("Started");
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
synchronized (startStopLock) {
|
||||
logger.debug("Closing EventSource and stopping checkConnection job");
|
||||
stopCheckConnectionJob(true);
|
||||
closeEventSource(0, TimeUnit.SECONDS);
|
||||
logger.debug("Stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private void closeEventSource(long timeout, TimeUnit timeoutUnit) {
|
||||
SseEventSource localEventSource = eventSource;
|
||||
if (localEventSource != null) {
|
||||
if (!localEventSource.isOpen()) {
|
||||
logger.debug("Existing EventSource is already closed");
|
||||
} else if (localEventSource.close(timeout, timeoutUnit)) {
|
||||
logger.debug("Succesfully closed existing EventSource");
|
||||
} else {
|
||||
logger.debug("Failed to close existing EventSource");
|
||||
}
|
||||
eventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startCheckConnectionJob() {
|
||||
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
|
||||
if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
|
||||
checkConnectionJob = scheduler.scheduleWithFixedDelay(this::checkConnection, CONNECTION_TIMEOUT_MILLIS,
|
||||
KEEP_ALIVE_MILLIS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopCheckConnectionJob(boolean mayInterruptIfRunning) {
|
||||
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
|
||||
if (localCheckConnectionJob != null && !localCheckConnectionJob.isCancelled()) {
|
||||
localCheckConnectionJob.cancel(mayInterruptIfRunning);
|
||||
checkConnectionJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addStreamingDataListener(NestStreamingDataListener listener) {
|
||||
return listeners.add(listener);
|
||||
}
|
||||
|
||||
public boolean removeStreamingDataListener(NestStreamingDataListener listener) {
|
||||
return listeners.remove(listener);
|
||||
}
|
||||
|
||||
public @Nullable TopLevelData getLastReceivedTopLevelData() {
|
||||
return lastReceivedTopLevelData;
|
||||
}
|
||||
|
||||
private void onEvent(InboundSseEvent inboundEvent) {
|
||||
try {
|
||||
lastEventTimestamp = System.currentTimeMillis();
|
||||
|
||||
String name = inboundEvent.getName();
|
||||
String data = inboundEvent.readData();
|
||||
|
||||
logger.debug("Received '{}' event, data: {}", name, data);
|
||||
|
||||
if (!connected) {
|
||||
logger.debug("Connected to streaming events");
|
||||
connected = true;
|
||||
listeners.forEach(listener -> listener.onConnected());
|
||||
}
|
||||
|
||||
if (AUTH_REVOKED.equals(name)) {
|
||||
logger.debug("API authorization has been revoked for access token: {}", data);
|
||||
listeners.forEach(listener -> listener.onAuthorizationRevoked(data));
|
||||
} else if (ERROR.equals(name)) {
|
||||
logger.warn("Error occurred: {}", data);
|
||||
listeners.forEach(listener -> listener.onError(data));
|
||||
} else if (KEEP_ALIVE.equals(name)) {
|
||||
logger.debug("Received message to keep connection alive");
|
||||
} else if (OPEN.equals(name)) {
|
||||
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();
|
||||
lastReceivedTopLevelData = topLevelData;
|
||||
listeners.forEach(listener -> listener.onNewTopLevelData(topLevelData));
|
||||
} else {
|
||||
logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// catch exceptions here otherwise they will be swallowed by the implementation
|
||||
logger.warn("An exception occurred while processing the inbound event", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError(Throwable error) {
|
||||
logger.debug("Error occurred while receiving events", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.rest;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Contains the data needed to do an update request back to Nest.
|
||||
*
|
||||
* @author David Bennett - Initial contribution
|
||||
*/
|
||||
public class NestUpdateRequest {
|
||||
private final String updatePath;
|
||||
private final Map<String, Object> values;
|
||||
|
||||
private NestUpdateRequest(Builder builder) {
|
||||
this.updatePath = builder.basePath + builder.identifier;
|
||||
this.values = builder.values;
|
||||
}
|
||||
|
||||
public String getUpdatePath() {
|
||||
return updatePath;
|
||||
}
|
||||
|
||||
public Map<String, Object> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String basePath;
|
||||
private String identifier;
|
||||
private Map<String, Object> values = new HashMap<>();
|
||||
|
||||
public Builder withBasePath(String basePath) {
|
||||
this.basePath = basePath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withIdentifier(String identifier) {
|
||||
this.identifier = identifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAdditionalValue(String field, Object value) {
|
||||
values.put(field, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public NestUpdateRequest build() {
|
||||
return new NestUpdateRequest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.update;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Handles all Nest data updates through delegation to the {@link NestUpdateHandler} for the respective data type.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestCompositeUpdateHandler {
|
||||
|
||||
private final Supplier<Set<String>> presentNestIdsSupplier;
|
||||
private final Map<Class<?>, @Nullable NestUpdateHandler<?>> updateHandlersMap = new ConcurrentHashMap<>();
|
||||
|
||||
public NestCompositeUpdateHandler(Supplier<Set<String>> presentNestIdsSupplier) {
|
||||
this.presentNestIdsSupplier = presentNestIdsSupplier;
|
||||
}
|
||||
|
||||
public <T> boolean addListener(Class<T> dataClass, NestThingDataListener<T> listener) {
|
||||
return getOrCreateUpdateHandler(dataClass).addListener(listener);
|
||||
}
|
||||
|
||||
public <T> boolean addListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
|
||||
return getOrCreateUpdateHandler(dataClass).addListener(nestId, listener);
|
||||
}
|
||||
|
||||
private Set<String> findMissingNestIds(Set<NestIdentifiable> updates) {
|
||||
Set<String> nestIds = updates.stream().map(u -> u.getId()).collect(Collectors.toSet());
|
||||
Set<String> missingNestIds = presentNestIdsSupplier.get();
|
||||
missingNestIds.removeAll(nestIds);
|
||||
return missingNestIds;
|
||||
}
|
||||
|
||||
public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
|
||||
return getOrCreateUpdateHandler(dataClass).getLastUpdate(nestId);
|
||||
}
|
||||
|
||||
public <T> List<T> getLastUpdates(Class<T> dataClass) {
|
||||
return getOrCreateUpdateHandler(dataClass).getLastUpdates();
|
||||
}
|
||||
|
||||
private Set<NestIdentifiable> getNestUpdates(TopLevelData data) {
|
||||
Set<NestIdentifiable> updates = new HashSet<>();
|
||||
if (data.getDevices() != null) {
|
||||
if (data.getDevices().getCameras() != null) {
|
||||
updates.addAll(data.getDevices().getCameras().values());
|
||||
}
|
||||
if (data.getDevices().getSmokeCoAlarms() != null) {
|
||||
updates.addAll(data.getDevices().getSmokeCoAlarms().values());
|
||||
}
|
||||
if (data.getDevices().getThermostats() != null) {
|
||||
updates.addAll(data.getDevices().getThermostats().values());
|
||||
}
|
||||
}
|
||||
if (data.getStructures() != null) {
|
||||
updates.addAll(data.getStructures().values());
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> NestUpdateHandler<T> getOrCreateUpdateHandler(Class<T> dataClass) {
|
||||
NestUpdateHandler<T> handler = (NestUpdateHandler<T>) updateHandlersMap.get(dataClass);
|
||||
if (handler == null) {
|
||||
handler = new NestUpdateHandler<>();
|
||||
updateHandlersMap.put(dataClass, handler);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleUpdate(TopLevelData data) {
|
||||
Set<NestIdentifiable> updates = getNestUpdates(data);
|
||||
updates.forEach(update -> {
|
||||
Class<NestIdentifiable> updateClass = (Class<NestIdentifiable>) update.getClass();
|
||||
getOrCreateUpdateHandler(updateClass).handleUpdate(updateClass, update.getId(), update);
|
||||
});
|
||||
|
||||
Set<String> missingNestIds = findMissingNestIds(updates);
|
||||
if (!missingNestIds.isEmpty()) {
|
||||
updateHandlersMap.values().forEach(handler -> {
|
||||
if (handler != null) {
|
||||
handler.handleMissingNestIds(missingNestIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public <T> boolean removeListener(Class<T> dataClass, NestThingDataListener<T> listener) {
|
||||
return getOrCreateUpdateHandler(dataClass).removeListener(listener);
|
||||
}
|
||||
|
||||
public <T> boolean removeListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
|
||||
return getOrCreateUpdateHandler(dataClass).removeListener(nestId, listener);
|
||||
}
|
||||
|
||||
public void resendLastUpdates() {
|
||||
updateHandlersMap.values().forEach(handler -> {
|
||||
if (handler != null) {
|
||||
handler.resendLastUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nest.internal.update;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nest.internal.listener.NestThingDataListener;
|
||||
|
||||
/**
|
||||
* Handles the updates of one type of data by notifying listeners of changes and storing the update value.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*
|
||||
* @param <T> the type of update data
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NestUpdateHandler<T> {
|
||||
|
||||
/**
|
||||
* The ID used for listeners that subscribe to any Nest update.
|
||||
*/
|
||||
private static final String ANY_ID = "*";
|
||||
|
||||
private final Map<String, @Nullable T> lastUpdates = new ConcurrentHashMap<>();
|
||||
private final Map<String, @Nullable Set<NestThingDataListener<T>>> listenersMap = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean addListener(NestThingDataListener<T> listener) {
|
||||
return addListener(ANY_ID, listener);
|
||||
}
|
||||
|
||||
public boolean addListener(String nestId, NestThingDataListener<T> listener) {
|
||||
return getOrCreateListeners(nestId).add(listener);
|
||||
}
|
||||
|
||||
public @Nullable T getLastUpdate(String nestId) {
|
||||
return lastUpdates.get(nestId);
|
||||
}
|
||||
|
||||
public List<T> getLastUpdates() {
|
||||
return new ArrayList<>(lastUpdates.values());
|
||||
}
|
||||
|
||||
private Set<NestThingDataListener<T>> getListeners(String nestId) {
|
||||
Set<NestThingDataListener<T>> listeners = new HashSet<>();
|
||||
if (listenersMap.get(nestId) != null) {
|
||||
listeners.addAll(listenersMap.get(nestId));
|
||||
}
|
||||
if (listenersMap.get(ANY_ID) != null) {
|
||||
listeners.addAll(listenersMap.get(ANY_ID));
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
private Set<NestThingDataListener<T>> getOrCreateListeners(String nestId) {
|
||||
Set<NestThingDataListener<T>> listeners = listenersMap.get(nestId);
|
||||
if (listeners == null) {
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
listenersMap.put(nestId, listeners);
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
public void handleMissingNestIds(Set<String> nestIds) {
|
||||
nestIds.forEach(nestId -> {
|
||||
lastUpdates.remove(nestId);
|
||||
getListeners(nestId).forEach(l -> l.onMissingData(nestId));
|
||||
});
|
||||
}
|
||||
|
||||
public void handleUpdate(Class<T> dataClass, String nestId, T update) {
|
||||
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);
|
||||
if (lastUpdate == null) {
|
||||
listeners.forEach(l -> l.onNewData(update));
|
||||
} else if (!lastUpdate.equals(update)) {
|
||||
listeners.forEach(l -> l.onUpdatedData(lastUpdate, update));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeListener(NestThingDataListener<T> listener) {
|
||||
return removeListener(ANY_ID, listener);
|
||||
}
|
||||
|
||||
public boolean removeListener(String nestId, NestThingDataListener<T> listener) {
|
||||
return getOrCreateListeners(nestId).remove(listener);
|
||||
}
|
||||
|
||||
public void resendLastUpdates() {
|
||||
lastUpdates.forEach((nestId, update) -> notifyListeners(nestId, null, update));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="nest" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Nest Binding</name>
|
||||
<description>Nest connects to the Nest cloud and allows control of the various Nest devices.</description>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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: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>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="productId" type="text" groupName="oauth">
|
||||
<label>Product ID</label>
|
||||
<description>The product ID from the Nest product page</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="productSecret" type="text" groupName="oauth">
|
||||
<label>Product Secret</label>
|
||||
<description>The product secret from the Nest product page</description>
|
||||
<required>true</required>
|
||||
</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">
|
||||
<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>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:nest:device">
|
||||
<parameter name="deviceId" type="text" required="true">
|
||||
<label>Device ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:nest:structure">
|
||||
<parameter name="structureId" type="text" required="true">
|
||||
<label>Structure ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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="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>
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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="camera">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Nest Cam</label>
|
||||
<description>A Nest Cam registered with your account</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="camera" typeId="Camera"/>
|
||||
<channel-group id="last_event" typeId="CameraEvent">
|
||||
<label>Last Event</label>
|
||||
<description>Information about the last camera event (requires Nest Aware subscription)</description>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,520 @@
|
||||
<?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">
|
||||
|
||||
<!-- Common -->
|
||||
<channel-type id="LastConnection" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Connection</label>
|
||||
<description>Timestamp of the last successful interaction with Nest</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<!-- Structure -->
|
||||
<channel-type id="Away">
|
||||
<item-type>String</item-type>
|
||||
<label>Away</label>
|
||||
<description>Away state of the structure</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="AWAY">Away</option>
|
||||
<option value="HOME">Home</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="CountryCode" 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">
|
||||
<item-type>String</item-type>
|
||||
<label>Postal Code</label>
|
||||
<description>Postal code of the structure</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="TimeZone">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>ETA</label>
|
||||
<description>
|
||||
Estimated time of arrival at home, will setup the heat to turn on and be warm
|
||||
by the time you arrive
|
||||
</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="RushHourRewardsEnrollment">
|
||||
<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">
|
||||
<item-type>String</item-type>
|
||||
<label>Security State</label>
|
||||
<description>Security state of the structure</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OK">ok</option>
|
||||
<option value="DETER">deter</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<!-- Camera -->
|
||||
<channel-group-type id="Camera">
|
||||
<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"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="AudioInputEnabled" 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">
|
||||
<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">
|
||||
<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">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Streaming</label>
|
||||
<description>If the camera is currently streaming</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="WebUrl">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="CameraEventHasSound" 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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<item-type>String</item-type>
|
||||
<label>Activity Zones</label>
|
||||
<description>Identifiers for activity zones that detected the event (comma separated)</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<!-- Smoke detector -->
|
||||
<channel-type id="UiColorState" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>UI Color State</label>
|
||||
<description>Current color state of the protect</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="GRAY">gray</option>
|
||||
<option value="GREEN">green</option>
|
||||
<option value="YELLOW">yellow</option>
|
||||
<option value="RED">red</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="CoAlarmState">
|
||||
<item-type>String</item-type>
|
||||
<label>CO Alarm State</label>
|
||||
<description>Carbon monoxide alarm state</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OK">ok</option>
|
||||
<option value="EMERGENCY">emergency</option>
|
||||
<option value="WARNING">warning</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="SmokeAlarmState">
|
||||
<item-type>String</item-type>
|
||||
<label>Smoke Alarm State</label>
|
||||
<description>Smoke alarm state</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OK">ok</option>
|
||||
<option value="EMERGENCY">emergency</option>
|
||||
<option value="WARNING">warning</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ManualTestActive" 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">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Manual Test Time</label>
|
||||
<description>Timestamp of the last successful manual test</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<!-- Thermostat -->
|
||||
<channel-type id="Temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Current temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="SetPoint">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Set Point</label>
|
||||
<description>The set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%.1f %unit%" step="0.5"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="MaxSetPoint">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Max Set Point</label>
|
||||
<description>The max set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%.1f %unit%" step="0.5"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="MinSetPoint">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Min Set Point</label>
|
||||
<description>The min set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%.1f %unit%" step="0.5"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="EcoMaxSetPoint" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Eco Max Set Point</label>
|
||||
<description>The eco range max set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="EcoMinSetPoint" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Eco Min Set Point</label>
|
||||
<description>The eco range min set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="LockedMaxSetPoint" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Locked Max Set Point</label>
|
||||
<description>The locked range max set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="LockedMinSetPoint" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Locked Min Set Point</label>
|
||||
<description>The locked range min set point temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Locked" 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">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>Current mode of the Nest thermostat</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="OFF">off</option>
|
||||
<option value="ECO">eco</option>
|
||||
<option value="HEAT">heating</option>
|
||||
<option value="COOL">cooling</option>
|
||||
<option value="HEAT_COOL">heat/cool</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="PreviousMode" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Previous Mode</label>
|
||||
<description>The previous mode of the Nest thermostat</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OFF">off</option>
|
||||
<option value="ECO">eco</option>
|
||||
<option value="HEAT">heating</option>
|
||||
<option value="COOL">cooling</option>
|
||||
<option value="HEAT_COOL">heat/cool</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="State" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>State</label>
|
||||
<description>The active state of the Nest thermostat</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="Humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Indicates the current relative humidity</description>
|
||||
<category>Humidity</category>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="TimeToTarget">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Fan Timer Duration</label>
|
||||
<description>Length of time that the fan is set to run</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="15">15 min</option>
|
||||
<option value="30">30 min</option>
|
||||
<option value="45">45 min</option>
|
||||
<option value="60">1 h</option>
|
||||
<option value="120">2 h</option>
|
||||
<option value="240">4 h</option>
|
||||
<option value="480">8 h</option>
|
||||
<option value="960">16 h</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="FanTimerTimeout" 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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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="smoke_detector">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="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="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"/>
|
||||
</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>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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="structure">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Nest Structure</label>
|
||||
<description>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</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"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Nest</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>structureId</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:nest:structure"/>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user