From 63d634c6318c4e3a0a23fa6489a040cf5d868bcf Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Tue, 3 Nov 2020 01:09:27 +0100 Subject: [PATCH] [nest] Add support for Smart Device Management (SDM) API * Reworks WWN implementation so that the thing types have a wwn_ prefix and the classes have a WWN prefix and reside in a 'wwn' package * Adds an SDM implementation which is also based on: https://github.com/bhigg-code/openhab-addons/tree/2.5.x/bundles/org.openhab.binding.nestdeviceaccess * Adds unit tests for (de)serialization of the SDM and Pub/Sub API requests and responses * Updates the binding documentation for the changes and additions Fixes #8664 Also-by: Brian Higginbotham Signed-off-by: Wouter Born --- bundles/org.openhab.binding.nest/README.md | 489 +++++++++++++----- .../nest/internal/NestHandlerFactory.java | 135 ----- .../discovery/NestDiscoveryService.java | 171 ------ .../internal/sdm/SDMBindingConstants.java | 92 ++++ .../internal/sdm/SDMThingHandlerFactory.java | 80 +++ .../nest/internal/sdm/api/PubSubAPI.java | 284 ++++++++++ .../binding/nest/internal/sdm/api/SDMAPI.java | 340 ++++++++++++ .../sdm/config/SDMAccountConfiguration.java | 59 +++ .../sdm/config/SDMDeviceConfiguration.java | 31 ++ .../sdm/discovery/SDMDiscoveryService.java | 146 ++++++ .../sdm/dto/PubSubRequestsResponses.java | 148 ++++++ .../nest/internal/sdm/dto/SDMCommands.java | 318 ++++++++++++ .../nest/internal/sdm/dto/SDMDevice.java | 46 ++ .../nest/internal/sdm/dto/SDMDeviceType.java | 38 ++ .../nest/internal/sdm/dto/SDMError.java | 31 ++ .../nest/internal/sdm/dto/SDMEvent.java | 128 +++++ .../nest/internal/sdm/dto/SDMGson.java | 76 +++ .../internal/sdm/dto/SDMIdentifiable.java | 29 ++ .../sdm/dto/SDMListDevicesResponse.java | 37 ++ .../sdm/dto/SDMListRoomsResponse.java | 37 ++ .../sdm/dto/SDMListStructuresResponse.java | 37 ++ .../internal/sdm/dto/SDMParentRelation.java | 30 ++ .../internal/sdm/dto/SDMResourceName.java | 101 ++++ .../nest/internal/sdm/dto/SDMRoom.java | 30 ++ .../nest/internal/sdm/dto/SDMStructure.java | 30 ++ .../nest/internal/sdm/dto/SDMTraits.java | 441 ++++++++++++++++ .../FailedSendingPubSubDataException.java | 38 ++ .../FailedSendingSDMDataException.java | 38 ++ .../InvalidPubSubAccessTokenException.java | 38 ++ ...validPubSubAuthorizationCodeException.java | 38 ++ .../InvalidSDMAccessTokenException.java | 38 ++ .../InvalidSDMAuthorizationCodeException.java | 38 ++ .../sdm/handler/SDMAccountHandler.java | 332 ++++++++++++ .../internal/sdm/handler/SDMBaseHandler.java | 319 ++++++++++++ .../sdm/handler/SDMCameraHandler.java | 204 ++++++++ .../sdm/handler/SDMThermostatHandler.java | 357 +++++++++++++ .../listener/PubSubSubscriptionListener.java | 32 ++ .../sdm/listener/SDMAPIRequestListener.java | 29 ++ .../sdm/listener/SDMEventListener.java | 27 + .../WWNBindingConstants.java} | 22 +- .../internal/wwn/WWNThingHandlerFactory.java | 85 +++ .../{NestUtils.java => wwn/WWNUtils.java} | 8 +- .../config/WWNAccountConfiguration.java} | 6 +- .../config/WWNDeviceConfiguration.java} | 6 +- .../config/WWNStructureConfiguration.java} | 6 +- .../wwn/discovery/WWNDiscoveryService.java | 176 +++++++ .../dto/BaseWWNDevice.java} | 8 +- .../dto/WWNAccessTokenData.java} | 8 +- .../dto/WWNActivityZone.java} | 8 +- .../Camera.java => wwn/dto/WWNCamera.java} | 18 +- .../dto/WWNCameraEvent.java} | 8 +- .../dto/WWNDevices.java} | 20 +- .../{data/ETA.java => wwn/dto/WWNETA.java} | 8 +- .../dto/WWNErrorData.java} | 8 +- .../dto/WWNIdentifiable.java} | 8 +- .../dto/WWNMetadata.java} | 8 +- .../dto/WWNSmokeDetector.java} | 15 +- .../dto/WWNStructure.java} | 27 +- .../dto/WWNThermostat.java} | 10 +- .../dto/WWNTopLevelData.java} | 20 +- .../dto/WWNTopLevelStreamingData.java} | 12 +- .../dto/WWNUpdateRequest.java} | 12 +- .../Where.java => wwn/dto/WWNWhere.java} | 6 +- .../FailedResolvingWWNUrlException.java} | 13 +- .../FailedRetrievingWWNDataException.java} | 13 +- .../FailedSendingWWNDataException.java} | 13 +- .../InvalidWWNAccessTokenException.java} | 13 +- .../handler/WWNAccountHandler.java} | 126 ++--- .../handler/WWNBaseHandler.java} | 78 +-- .../handler/WWNCameraHandler.java} | 32 +- .../handler/WWNRedirectUrlSupplier.java} | 28 +- .../handler/WWNSmokeDetectorHandler.java} | 25 +- .../handler/WWNStructureHandler.java} | 28 +- .../handler/WWNThermostatHandler.java} | 26 +- .../listener/WWNStreamingDataListener.java} | 14 +- .../listener/WWNThingDataListener.java} | 6 +- .../rest/WWNAuthorizer.java} | 50 +- .../rest/WWNStreamingRequestFilter.java} | 8 +- .../rest/WWNStreamingRestClient.java} | 46 +- .../update/WWNCompositeUpdateHandler.java} | 43 +- .../update/WWNUpdateHandler.java} | 33 +- .../resources/OH-INF/config/sdm-config.xml | 96 ++++ .../config/{config.xml => wwn-config.xml} | 29 +- .../resources/OH-INF/thing/sdm-account.xml | 13 + .../resources/OH-INF/thing/sdm-camera.xml | 27 + .../resources/OH-INF/thing/sdm-channels.xml | 231 +++++++++ .../resources/OH-INF/thing/sdm-display.xml | 27 + .../resources/OH-INF/thing/sdm-doorbell.xml | 28 + .../resources/OH-INF/thing/sdm-thermostat.xml | 35 ++ .../resources/OH-INF/thing/thermostat.xml | 51 -- .../thing/{bridge.xml => wwn-account.xml} | 9 +- .../thing/{camera.xml => wwn-camera.xml} | 13 +- .../thing/{channels.xml => wwn-channels.xml} | 163 +++--- ...ke-detector.xml => wwn-smoke-detector.xml} | 19 +- .../{structure.xml => wwn-structure.xml} | 28 +- .../resources/OH-INF/thing/wwn-thermostat.xml | 52 ++ .../sdm/dto/PubSubRequestsResponsesTest.java | 97 ++++ .../internal/sdm/dto/SDMCommandsTest.java | 166 ++++++ .../nest/internal/sdm/dto/SDMDataUtil.java | 67 +++ .../nest/internal/sdm/dto/SDMDeviceTest.java | 298 +++++++++++ .../nest/internal/sdm/dto/SDMErrorTest.java | 64 +++ .../nest/internal/sdm/dto/SDMEventTest.java | 161 ++++++ .../sdm/dto/SDMListDevicesResponseTest.java | 67 +++ .../sdm/dto/SDMListRoomsResponseTest.java | 64 +++ .../dto/SDMListStructuresResponseTest.java | 64 +++ .../internal/sdm/dto/SDMResourceNameTest.java | 81 +++ .../dto/acknowledge-subscription-request.json | 7 + .../sdm/dto/camera-device-response.json | 38 ++ .../sdm/dto/create-subscription-request.json | 4 + .../sdm/dto/display-device-response.json | 38 ++ .../sdm/dto/doorbell-device-response.json | 39 ++ .../extend-camera-rtsp-stream-request.json | 6 + .../extend-camera-rtsp-stream-response.json | 7 + .../sdm/dto/failed-precondition-error.json | 7 + .../dto/generate-camera-image-request.json | 6 + .../dto/generate-camera-image-response.json | 6 + .../generate-camera-rtsp-stream-request.json | 4 + .../generate-camera-rtsp-stream-response.json | 10 + .../sdm/dto/list-devices-response.json | 173 +++++++ .../internal/sdm/dto/list-rooms-response.json | 20 + .../sdm/dto/list-structures-response.json | 20 + .../internal/sdm/dto/not-found-error.json | 7 + .../sdm/dto/pull-subscription-request.json | 3 + .../sdm/dto/pull-subscription-response.json | 28 + .../sdm/dto/relation-created-event.json | 10 + .../sdm/dto/relation-deleted-event.json | 10 + .../sdm/dto/relation-updated-event.json | 10 + .../sdm/dto/resource-update-event.json | 38 ++ .../set-fan-timer-request-with-duration.json | 7 + ...et-fan-timer-request-without-duration.json | 6 + .../set-thermostat-cool-setpoint-request.json | 6 + .../dto/set-thermostat-eco-mode-request.json | 6 + .../set-thermostat-heat-setpoint-request.json | 6 + .../sdm/dto/set-thermostat-mode-request.json | 6 + ...set-thermostat-range-setpoint-request.json | 7 + .../dto/stop-camera-rtsp-stream-request.json | 6 + .../sdm/dto/thermostat-device-response.json | 54 ++ .../itest.bndrun | 1 + .../dto/WWNDataUtil.java} | 16 +- .../dto/WWNGsonParsingTest.java} | 56 +- .../wwn/handler/WWNAccountHandlerTest.java} | 20 +- .../wwn/handler/WWNCameraHandlerTest.java} | 21 +- .../handler/WWNSmokeDetectorHandlerTest.java} | 21 +- .../wwn/handler/WWNStructureHandlerTest.java} | 21 +- .../handler/WWNThermostatHandlerTest.java} | 21 +- .../wwn/handler/WWNThingHandlerOSGiTest.java} | 57 +- .../wwn/test/WWNTestAccountHandler.java} | 27 +- .../wwn/test/WWNTestApiServlet.java} | 14 +- .../wwn/test/WWNTestHandlerFactory.java} | 27 +- .../wwn/test/WWNTestServer.java} | 10 +- .../{data => wwn/dto}/access-token-data.json | 0 .../{data => wwn/dto}/camera-data.json | 0 .../{data => wwn/dto}/error-data.json | 0 .../dto}/smoke-detector-data.json | 0 .../{data => wwn/dto}/structure-data.json | 0 .../{data => wwn/dto}/thermostat-data.json | 0 .../{data => wwn/dto}/top-level-data.json | 0 .../dto}/top-level-streaming-data-empty.json | 0 .../top-level-streaming-data-incomplete.json | 0 .../dto}/top-level-streaming-data.json | 0 160 files changed, 7680 insertions(+), 1197 deletions(-) delete mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestHandlerFactory.java delete mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/discovery/NestDiscoveryService.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMBindingConstants.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMThingHandlerFactory.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/PubSubAPI.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/SDMAPI.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMAccountConfiguration.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMDeviceConfiguration.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/discovery/SDMDiscoveryService.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponses.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommands.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDevice.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceType.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMError.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMEvent.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMGson.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMIdentifiable.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponse.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponse.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponse.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMParentRelation.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceName.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMRoom.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMStructure.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMTraits.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingPubSubDataException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingSDMDataException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAccessTokenException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAuthorizationCodeException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAccessTokenException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAuthorizationCodeException.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMAccountHandler.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMBaseHandler.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMCameraHandler.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMThermostatHandler.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/PubSubSubscriptionListener.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMAPIRequestListener.java create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMEventListener.java rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{NestBindingConstants.java => wwn/WWNBindingConstants.java} (91%) create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNThingHandlerFactory.java rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{NestUtils.java => wwn/WWNUtils.java} (87%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{config/NestBridgeConfiguration.java => wwn/config/WWNAccountConfiguration.java} (88%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{config/NestDeviceConfiguration.java => wwn/config/WWNDeviceConfiguration.java} (85%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{config/NestStructureConfiguration.java => wwn/config/WWNStructureConfiguration.java} (84%) create mode 100644 bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/discovery/WWNDiscoveryService.java rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/BaseNestDevice.java => wwn/dto/BaseWWNDevice.java} (95%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/AccessTokenData.java => wwn/dto/WWNAccessTokenData.java} (89%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/ActivityZone.java => wwn/dto/WWNActivityZone.java} (90%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/Camera.java => wwn/dto/WWNCamera.java} (94%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/CameraEvent.java => wwn/dto/WWNCameraEvent.java} (97%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/NestDevices.java => wwn/dto/WWNDevices.java} (82%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/ETA.java => wwn/dto/WWNETA.java} (95%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/ErrorData.java => wwn/dto/WWNErrorData.java} (94%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/NestIdentifiable.java => wwn/dto/WWNIdentifiable.java} (67%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/NestMetadata.java => wwn/dto/WWNMetadata.java} (92%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/SmokeDetector.java => wwn/dto/WWNSmokeDetector.java} (93%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/Structure.java => wwn/dto/WWNStructure.java} (93%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/Thermostat.java => wwn/dto/WWNThermostat.java} (98%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/TopLevelData.java => wwn/dto/WWNTopLevelData.java} (82%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/TopLevelStreamingData.java => wwn/dto/WWNTopLevelStreamingData.java} (85%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{rest/NestUpdateRequest.java => wwn/dto/WWNUpdateRequest.java} (83%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/Where.java => wwn/dto/WWNWhere.java} (94%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{exceptions/FailedResolvingNestUrlException.java => wwn/exceptions/FailedResolvingWWNUrlException.java} (64%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{exceptions/FailedRetrievingNestDataException.java => wwn/exceptions/FailedRetrievingWWNDataException.java} (64%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{exceptions/FailedSendingNestDataException.java => wwn/exceptions/FailedSendingWWNDataException.java} (64%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{exceptions/InvalidAccessTokenException.java => wwn/exceptions/InvalidWWNAccessTokenException.java} (65%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestBridgeHandler.java => wwn/handler/WWNAccountHandler.java} (72%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestBaseHandler.java => wwn/handler/WWNBaseHandler.java} (67%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestCameraHandler.java => wwn/handler/WWNCameraHandler.java} (86%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestRedirectUrlSupplier.java => wwn/handler/WWNRedirectUrlSupplier.java} (72%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestSmokeDetectorHandler.java => wwn/handler/WWNSmokeDetectorHandler.java} (76%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestStructureHandler.java => wwn/handler/WWNStructureHandler.java} (80%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{handler/NestThermostatHandler.java => wwn/handler/WWNThermostatHandler.java} (91%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{listener/NestStreamingDataListener.java => wwn/listener/WWNStreamingDataListener.java} (69%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{listener/NestThingDataListener.java => wwn/listener/WWNThingDataListener.java} (88%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{rest/NestAuthorizer.java => wwn/rest/WWNAuthorizer.java} (51%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{rest/NestStreamingRequestFilter.java => wwn/rest/WWNStreamingRequestFilter.java} (86%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{rest/NestStreamingRestClient.java => wwn/rest/WWNStreamingRestClient.java} (82%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{update/NestCompositeUpdateHandler.java => wwn/update/WWNCompositeUpdateHandler.java} (69%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{update/NestUpdateHandler.java => wwn/update/WWNUpdateHandler.java} (70%) create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/sdm-config.xml rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/{config.xml => wwn-config.xml} (57%) create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-account.xml create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-camera.xml create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-channels.xml create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-display.xml create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-doorbell.xml create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-thermostat.xml delete mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/thermostat.xml rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/{bridge.xml => wwn-account.xml} (64%) rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/{camera.xml => wwn-camera.xml} (70%) rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/{channels.xml => wwn-channels.xml} (75%) rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/{smoke-detector.xml => wwn-smoke-detector.xml} (59%) rename bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/{structure.xml => wwn-structure.xml} (50%) create mode 100644 bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-thermostat.xml create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponsesTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommandsTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDataUtil.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMErrorTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMEventTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponseTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponseTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponseTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceNameTest.java create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/acknowledge-subscription-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/camera-device-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/create-subscription-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/display-device-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/doorbell-device-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/failed-precondition-error.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-devices-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-rooms-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-structures-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/not-found-error.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-response.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-created-event.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-deleted-event.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-updated-event.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/resource-update-event.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-with-duration.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-without-duration.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-cool-setpoint-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-eco-mode-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-heat-setpoint-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-mode-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-range-setpoint-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/stop-camera-rtsp-stream-request.json create mode 100644 bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/thermostat-device-response.json rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/{data/NestDataUtil.java => wwn/dto/WWNDataUtil.java} (89%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/{data/GsonParsingTest.java => wwn/dto/WWNGsonParsingTest.java} (80%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestBridgeHandlerTest.java => internal/wwn/handler/WWNAccountHandlerTest.java} (81%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestCameraHandlerTest.java => internal/wwn/handler/WWNCameraHandlerTest.java} (90%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestSmokeDetectorHandlerTest.java => internal/wwn/handler/WWNSmokeDetectorHandlerTest.java} (86%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestStructureHandlerTest.java => internal/wwn/handler/WWNStructureHandlerTest.java} (88%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestThermostatHandlerTest.java => internal/wwn/handler/WWNThermostatHandlerTest.java} (95%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{handler/NestThingHandlerOSGiTest.java => internal/wwn/handler/WWNThingHandlerOSGiTest.java} (87%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{test/NestTestBridgeHandler.java => internal/wwn/test/WWNTestAccountHandler.java} (63%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{test/NestTestApiServlet.java => internal/wwn/test/WWNTestApiServlet.java} (93%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{test/NestTestHandlerFactory.java => internal/wwn/test/WWNTestHandlerFactory.java} (79%) rename itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/{test/NestTestServer.java => internal/wwn/test/WWNTestServer.java} (88%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/access-token-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/camera-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/error-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/smoke-detector-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/structure-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/thermostat-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/top-level-data.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/top-level-streaming-data-empty.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/top-level-streaming-data-incomplete.json (100%) rename itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/{data => wwn/dto}/top-level-streaming-data.json (100%) diff --git a/bundles/org.openhab.binding.nest/README.md b/bundles/org.openhab.binding.nest/README.md index 627b2058f9d11..3c8cdad8fc57f 100644 --- a/bundles/org.openhab.binding.nest/README.md +++ b/bundles/org.openhab.binding.nest/README.md @@ -1,66 +1,243 @@ # Nest Binding -The Nest binding integrates devices by [Nest](https://nest.com) using the [Nest API](https://developers.nest.com/documentation/cloud/get-started) (REST). +The Nest binding integrates devices by [Nest](https://store.google.com/us/category/connected_home?) using the [Smart Device Management](https://developers.google.com/nest/device-access/api) (SDM) API and the Works with Nest (WWN) API. -Because the Nest API runs on Nest's servers a connection with the Internet is required for sending and receiving information. -The binding uses HTTPS to connect to the Nest API using ports 443 and 9553. Make sure outbound connections to these ports are not blocked by a firewall. +To be able to use the SDM API it is required to first [register](https://developers.google.com/nest/device-access/registration) and pay a US$5 non-refundable registration fee. -> Note: This binding can only be used with Nest devices if you have an existing Nest developer account signed up for the Works with Nest (WWN) program. -New integrations using the WWN program are no longer accepted because WWN is being retired. -To keep using this binding do **NOT** migrate your Nest Account to a Google Account. -For more information see [What's happening at Nest?](https://nest.com/whats-happening/). +It is also possible to use the older WWN API with this binding. +For this you need to have the account details of a previously registered WWN API account. +Another requirement is that you have not yet migrated your Nest account to a Google account (which is irreversible). +It is no longer possible to register new WWN API accounts because the WWN API runs in maintenance mode. +See also [What's happening at Nest?](https://nest.com/whats-happening/). + +Because the SDM and WWN APIs run on servers in the cloud, a connection with the Internet is required for sending and receiving information. +The binding uses HTTPS to connect to the APIs using port 443. +When using the WWN API, the binding also connects to servers on port 9553. +So make sure outbound connections to these ports are not blocked by a firewall. ## Supported Things The table below lists the Nest binding thing types: -| Things | Description | Thing Type | -|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------| -| Nest Account | An account for using the Nest REST API | account | -| Nest Cam (Indoor, IQ, Outdoor), Dropcam | A Nest Cam registered with your account | camera | -| Nest Protect | The smoke detector/Nest Protect for the account | smoke_detector | -| Structure | The Nest structure defines the house the account has setup on Nest. You will only have more than one structure if you have more than one house | structure | -| Nest Thermostat (E) | A Thermostat to control the various aspects of the house's HVAC system | thermostat | - -## 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. +| Things | Description | SDM Thing Type | WWN Thing Type | +|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------|--------------------| +| Nest Account (SDM, WWN) | An account for using the Nest (SDM/WWN) REST API | sdm_account | wwn_account | +| Nest Cam (Indoor, IQ, Outdoor), Dropcam | A Nest Cam registered with your account | sdm_camera | wwn_camera | +| Nest Hello Doorbell | A Nest Doorbell registered with your account | sdm_doorbell | wwn_camera | +| Nest Hub (Max) | A Nest Display registered with your account | sdm_display | wwn_camera | +| Nest Protect | The smoke detector/Nest Protect for the account | | wwn_smoke_detector | +| Nest Thermostat (E) | A Thermostat to control the various aspects of the house's HVAC system | sdm_thermostat | wwn_thermostat | +| Structure | The Nest structure defines the house the account has setup on Nest. You will only have more than one structure if you have more than one house | | wwn_structure | + +The SDM API currently does not support Nest Protect devices. +There are no structure Things when using the SDM API, because the SDM API does not support setting the Home/Away status like the WWN API does. + +To use one of the Nest APIs, add the corresponding Account Thing using the UI and configure the required parameters. +After configuring an Account Thing, you can use it to discover the connected devices which are then added the Inbox. + +## SDM Account Configuration + +### Google Account Requirement + +To be able to use the SDM API it is required that you use a Google Account with your Nest devices. +If you still use the WWN API, you can no longer use the WWN API after migrating to a Google Account. +So if you have not yet migrated your account, check that all the functionality you require is provided by the SDM API and SDM Things in the binding. +Most notably, there is no support for the Nest Protect in the SDM API and you cannot change your Home/Away status. +To migrate to a Google account, follow the migration steps in the [Nest accounts FAQ](https://support.google.com/googlenest/answer/9297676?co=GENIE.Platform%3DiOS&hl=en&oco=0#accountmigration&accountmigration1&#accountmigration2&#accountmigration3&zippy=%2Chow-do-i-migrate-my-account) + +### SDM Configuration Parameters + +These parameters configure which SDM project is accessed using the SDM API and configure the OAuth 2.0 client details used for accessing the project. + +First a SDM project needs to be created and configured: + +1. Register for device access by clicking the "Go to Device Access Console" button and follow the instructions on the [Device Access Registration](https://developers.google.com/nest/device-access/registration) page. +1. Create a new SDM project on the [Projects](https://console.nest.google.com/device-access/project-list) page + 1. Give your project a name so it is easily recognizable + 1. "Skip" entering the OAuth client ID for now + 1. If you want to download camera images using the binding, it is required to "Enable" events. + Enabling events also allows for faster thermostat state updates. + The binding only uses events when the Pub/Sub configuration parameters of the Nest SDM Account Thing are also configured. + 1. After clicking the "Create project" button, the SDM project details of the created project show +1. Copy and save the **Project ID** at the top of the page (e.g. `585de72e-968c-435c-b16a-31d1d3f76833`) somewhere + +Now an OAuth 2.0 client is created and configured for using the SDM API by the binding: + +1. Configure the "Publishing status" of your Google Cloud Platform to "Production" ([APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) so the OAuth 2.0 tokens do not expire after 2 weeks +1. Create a new client on the "Credentials" page ([APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials)): + 1. Click the "Create Credentials" button at the top of the page + 1. Choose "OAuth client ID" + 1. As "Application type" choose "TVs and Limited Input devices" + 1. Give it a name so you can remember what it is used for (e.g. `Nest Binding SDM`) + 1. Click "Create" to create the client + 1. Copy and save the generated **Client ID** (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`) and **Client Secret** (e.g. `726kcU-d1W4RXxEJA79oZ0oG`) somewhere +1. Configure the SDM project to use the created client: + 1. Go the the SDM [Projects](https://console.nest.google.com/device-access/project-list) page + 1. Click on your SDM Project to show its details + 1. Scroll to "Project Info > OAuth client ID" and open the options menu (3 stacked dots) at the end of the line + 1. Select the "Edit" option + 1. Copy/paste the saved OAuth 2.0 Client ID here (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`) + 1. Click the "Save" button at the end of the line to update the project + +Finally, an SDM Account Thing can be created to access the SDM project using the SDM API with the created client: + +1. Create a new "Nest SDM Account" Thing in openHAB +1. Copy/paste the saved SDM **Project ID** to SDM group parameter in the SDM Account Thing configuration parameters (e.g. `585de72e-968c-435c-b16a-31d1d3f76833`) +1. Copy/paste the saved OAuth 2.0 **Client ID** to SDM group parameter (e.g. `1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com`) +1. Copy/paste the saved OAuth 2.0 **Client Secret** to SDM group parameter (e.g. `726kcU-d1W4RXxEJA79oZ0oG`) +1. Create an authorization code for the binding: + 1. Replace the **Project ID** and **Client ID** in the URL below with your SDM Project ID and SDM OAuth 2.0 Client ID and open the URL in a new browser tab: + + `https://nestservices.google.com/partnerconnections/{{Project ID}}/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&access_type=offline&prompt=consent&client_id={{Client ID}}&scope=https://www.googleapis.com/auth/sdm.service` + + For the example values used so far this is: + + `https://nestservices.google.com/partnerconnections/585de72e-968c-435c-b16a-31d1d3f76833/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&access_type=offline&prompt=consent&client_id=1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com&scope=https://www.googleapis.com/auth/sdm.service` + 1. Enable all the permissions you want to use with the binding and click "Next" to continue + 1. Login using your Google account when prompted + 1. On the "Google hasn't verified this app" page, click on "Advanced" + 1. Then click on "Go to ... (advanced)" + 1. Now "Allow" the SDM permissions and confirm your choices again by clicking "Allow" + 1. Next the "Sign in" page will show the **Authorization Code** + 1. Copy/paste the **Authorization Code** to the SDM group parameter in the openHAB Nest SDM Account Thing configuration +1. All required SDM Account Thing configuration parameters have now been entered so create it by clicking "Create Thing". + +The SDM Account Thing should now be ONLINE and have as status description "Using periodic refresh". +It should also be possible to use the configured account to discover your Nest devices via the Inbox. + +You can monitor the SDM API using the Google Cloud Platform Console via [API & Services > Smart Device Management API](https://console.cloud.google.com/apis/api/smartdevicemanagement.googleapis.com/overview). + +If you've made it this far, it should be easy to edit the SDM Account Thing again and update it so it can also use SDM Pub/Sub events. :-) + +### Pub/Sub Configuration Parameters + +After configuring the SDM configuration parameters, a SDM Account Thing can be updated so it can listen to SDM events using Pub/Sub. +This is required if you want to download camera images using the binding or to get faster thermostat state updates. + +Enable Pub/Sub events in your SDM project: + +1. Open your SDM project details using the [Projects](https://console.nest.google.com/device-access/project-list) page +1. Scroll to "Project Info > Pub/Sub topic" and check if it is set to "Enabled" +1. If it is set to "Disabled", enable events: + 1. Open the options menu (3 stacked dots) at the end of the line + 1. Select the "Edit" option + 1. Check the "Enable events" option + 1. Click the "Save" button at the end of the line to update the project + +Lookup your Google Cloud Platform (GCP) Project ID: + +1. Open the [IAM & Admin > Settings](https://console.cloud.google.com/iam-admin/settings) +1. Copy and save the GCP **Project ID** (e.g. `openhab-12345`) + +Next an OAuth 2.0 client is created which is used to create a Pub/Sub subscription for listening to SDM events by the binding: + +1. Open the "Credentials" page ([APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials)): +1. Click the "Create Credentials" button at the top of the page +1. Choose "OAuth client ID" +1. As "Application type" choose "TVs and Limited Input devices" +1. Give it a name so you can remember what it is used for (e.g. `Nest Binding Pub/Sub`) +1. Click "Create" to create the client +1. Copy and save the generated **Client ID** (e.g. `1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com`) and **Client Secret** (e.g. `1-k78-XcHhp_gdZF-I6JaIHp`) somewhere + +Finally, the existing SDM Account Thing can be updated so it can subscribe to SDM events: + +1. Open the configuration details of your existing "Nest SDM Account" Thing in openHAB +1. Copy/paste the saved GCP **Project ID** to Pub/Sub group parameter (e.g. `openhab-123`) +1. Enter a name in **Subscription ID** that uniquely identifies the Pub/Sub subscription used by the binding + + > Must be 3-255 characters, start with a letter, and contain only the following characters: letters, numbers, dashes (-), periods (.), underscores (_), tildes (~), percents (%) or plus signs (+). Cannot start with goog. +1. Copy/paste the saved OAuth 2.0 **Client ID** to Pub/Sub group parameter (e.g. `1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com`) +1. Copy/paste the saved OAuth 2.0 **Client Secret** to Pub/Sub group parameter (e.g. `1-k78-XcHhp_gdZF-I6JaIHp`) +1. Create an authorization code for the binding: + 1. Replace the **Client ID** in the URL below with your Pub/Sub OAuth 2.0 Client ID and open the URL in a new browser tab: + + `https://accounts.google.com/o/oauth2/auth?client_id={{Client ID}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/pubsub&response_type=code` + + For the example client this is: + + `https://accounts.google.com/o/oauth2/auth?client_id=1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/pubsub&response_type=code` + 1. Login using your Google account when prompted + 1. On the "Google hasn't verified this app" page, click on "Advanced" + 1. Then click on "Go to ... (advanced)" + 1. Now "Allow" the Pub/Sub permissions and confirm your choices again by clicking "Allow" + 1. Next the "Sign in" page will show the **Authorization Code** + 1. Copy/paste the **Authorization Code** to the Pub/Sub group parameter in the openHAB Nest SDM Account Thing configuration +1. All required Pub/Sub Account Thing configuration parameters have now been entered so click "Save" to update the SDM Account Thing configuration. + +The SDM Account Thing should now be ONLINE and have as status description "Using periodic refresh and Pub/Sub". + +The created subscription can also be monitored using the Google Cloud Platform Console via [Pub/Sub > Subscriptions](https://console.cloud.google.com/cloudpubsub/subscription/list). + +## SDM Device Configuration + +| Configuration Parameter | Required | Default | Description | +|-------------------------|----------|---------|---------------------------------------------------------------------------| +| deviceId | X | | Identifies the device in the SDM API | +| refreshInterval | | 300 | This is refresh interval in seconds to update the Nest device information | + +Decreasing the `refreshInterval` may cause issues when you have a lot of devices connected because it may cause API rate limits to be exceeded. +You may want to decrease the `refreshInterval` for a Thermostat if Pub/Sub events have not been configured to provide state updating. + +## WWN Account Configuration + +To configure the binding to use the WWN API, add a new "Nest WWN Account" Thing in the UI and enter the **Product ID**, **Product Secret** and **Access Token** of an existing WWN account as configuration parameters. +It is no longer possible to register new WWN accounts with Nest because the WWN API runs in maintenance mode. ## Discovery -The binding will discover all Nest Things from your account when you add and configure a "Nest Account" Thing. -See the Authorization paragraph above for details on how to obtain the Product ID, Product Secret and Pincode configuration parameters. - -Once the binding has successfully authorized with the Nest API, it obtains an Access Token using the Pincode. -The configured Pincode is cleared because it can only be used once. -The obtained Access Token is saved as an advanced configuration parameter of the "Nest Account". - -You can reuse an Access Token for authorization but not the Pincode. -A new Pincode can again be generated via the "Authorization URL" (see Authorization paragraph). +The binding will discover all Nest Things from your account when you add and configure a Nest SDM or WWN Account Thing. ## Channels -### Account Channels - -The account Thing Type does not have any channels. - -### Camera Channels +### SDM/WWN Account Channels + +The account Thing Types do not have any channels. + +### SDM Camera/Display/Doorbell Channels + +The state of these channels is based on Pub/Sub events sent by the SDM API. +So make sure the Pub/Sub account details are properly configured in the `sdm_account`. + +| Channel Type ID | Item Type | Description | Read Write | +|----------------------------------|-----------|-----------------------------------------------------|:----------:| +| chime_event#image | Image | Static image based on a chime event | R | +| chime_event#timestamp | DateTime | The last time that the door chime was pressed | R | +| live_stream#current_token | String | Live stream current token value | R | +| live_stream#expiration_timestamp | DateTime | Live stream token expiration time | R | +| live_stream#extension_token | String | Live stream token extension value | R | +| live_stream#url | String | The RTSP video stream URL for the most recent event | R | +| motion_event#image | Image | Static image based on a motion event | R | +| motion_event#timestamp | DateTime | The last time that motion was detected | R | +| person_event#image | Image | Static image based on a person event | R | +| person_event#timestamp | DateTime | The last time that a person was detected | R | +| sound_event#image | Image | Static image based on a sound event | R | +| sound_event#timestamp | DateTime | The last time that a sound was detected | R | + +The `chime_event` group channels only exist for doorbell Things. +Each image channel has the `imageWidth` and `imageHeight` configuration parameters that can be used for configuring the image size in pixels. +The maximum camera resolution is listed as `maxImageResolution` property in the Thing properties. + +### SDM Thermostat Channels + +| Channel Type ID | Item Type | Description | Read Write | +|---------------------|----------------------|------------------------------------------------------------------------|:----------:| +| ambient_humidity | Number:Dimensionless | Lists the current ambient humidity percentage from the thermostat | R | +| ambient_temperature | Number:Temperature | Lists the current ambient temperature from the thermostat | R | +| current_eco_mode | String | Lists the current eco mode from the thermostat (OFF, MANUAL_ECO) | R/W | +| current_mode | String | Lists the current mode from the thermostat (OFF, HEAT, COOL, HEATCOOL) | R/W | +| fan_timer_mode | Switch | Lists the current fan timer mode | R/W | +| fan_timer_timeout | DateTime | Timestamp at which timer mode turns OFF | R/W | +| hvac_status | String | Provides the thermostat HVAC Status (OFF, HEATING, COOLING) | R | +| maximum_temperature | Number:Temperature | Lists the maximum temperature setting from the thermostat | R/W | +| minimum_temperature | Number:Temperature | Lists the target temperature setting from the thermostat | R/W | +| target_temperature | Number:Temperature | Lists the target temperature setting from the thermostat | R/W | +| temperature_cool | Number:Temperature | Lists the heat temperature Setting from the thermostat | R | +| temperature_heat | Number:Temperature | Lists the heat temperature setting from the thermostat | R | + +The `fan_timer_mode` channel has a `fanTimerDuration` configuration parameter that can be used for configuring how long the fan is ON before it is switched OFF (1s to 43200s). +Similarly, when a DateTime command is sent to the `fan_timer_timeout` channel, the fan timer is switched ON and runs until the timestamp in the command (min now+1s, max now+43200s). + +### WWN Camera Channels **Camera group channels** @@ -96,7 +273,7 @@ Information about the last camera event (requires Nest Aware subscription). | urls_expire_time | DateTime | Timestamp when the camera event URLs expire | R | | web_url | String | The web URL for the camera event, allows you to see the camera event in a web page | R | -### Smoke Detector Channels +### WWN Smoke Detector Channels | Channel Type ID | Item Type | Description | Read Write | |-----------------------|-----------|-----------------------------------------------------------------------------------|:----------:| @@ -108,7 +285,7 @@ Information about the last camera event (requires Nest Aware subscription). | smoke_alarm_state | String | The smoke alarm state of the Nest Protect (OK, EMERGENCY, WARNING) | R | | ui_color_state | String | The current color of the ring on the smoke detector (GRAY, GREEN, YELLOW, RED) | R | -### Structure Channels +### WWN Structure Channels | Channel Type ID | Item Type | Description | Read Write | |------------------------------|-----------|--------------------------------------------------------------------------------------------------------|:----------:| @@ -124,7 +301,7 @@ Information about the last camera event (requires Nest Aware subscription). | smoke_alarm_state | String | Smoke alarm state (OK, EMERGENCY, WARNING) | R | | time_zone | String | The time zone for the structure ([IANA time zone format](https://www.iana.org/time-zones)) | R | -### Thermostat Channels +### WWN Thermostat Channels | Channel Type ID | Item Type | Description | Read Write | |-----------------------------|----------------------|----------------------------------------------------------------------------------------|:----------:| @@ -165,94 +342,144 @@ The Nest API applies the following rounding: You can use the discovery functionality of the binding to obtain the deviceId and structureId values for defining Nest things in files. -Another way to get the deviceId and structureId values is by querying the Nest API yourself. First [obtain an Access Token](https://developers.nest.com/documentation/cloud/sample-code-auth) (or use the Access Token obtained by the binding). -Then use it with one of the [API Read Examples](https://developers.nest.com/documentation/cloud/how-to-read-data). - -### demo.things: +### sdm-demo.things ``` -Bridge nest:account:demo_account [ productId="8fdf9885-ca07-4252-1aa3-f3d5ca9589e0", productSecret="QITLR3iyUlWaj9dbvCxsCKp4f", accessToken="c.6rse1xtRk2UANErcY0XazaqPHgbvSSB6owOrbZrZ6IXrmqhsr9QTmcfaiLX1l0ULvlI5xLp01xmKeiojHqozLQbNM8yfITj1LSdK28zsUft1aKKH2mDlOeoqZKBdVIsxyZk4orH0AvKEZ5aY" ] { - camera fish_cam [ deviceId="qw0NNE8ruxA9AGJkTaFH3KeUiJaONWKiH9Gh3RwwhHClonIexTtufQ" ] - smoke_detector hallway_smoke [ deviceId="Tzvibaa3lLKnHpvpi9OQeCI_z5rfkBAV" ] - structure home [ structureId="20wKjydArmMV3kOluTA7JRcZg8HKBzTR-G_2nRXuIN1Bd6laGLOJQw" ] - thermostat living_thermostat [ deviceId="ZqAKzSv6TO6PjBnOCXf9LSI_z5rfkBAV" ] +Bridge nest:sdm_account:demo_sdm_account [ sdmProjectId="585de72e-968c-435c-b16a-31d1d3f76833", sdmClientId="1046297811237-3f5sj4ccfubit0fum027ral82jgffsd1.apps.googleusercontent.com", sdmClientSecret="726kcU-d1W4RXxEJA79oZ0oG", sdmAuthorizationCode="xkkY3qYtfZCzaXCcPxpOELUW8EhgiSMD3n9jmzJ3m0yerkQpVRdj5vqWRjMSIG", pubsubProjectId="openhab-12345", pubsubSubscriptionId="nest-sdm-events", pubsubClientId="1046297811237-lg27h26kln6r1nbg54jpg6nfjg6h4b3n.apps.googleusercontent.com", pubsubClientSecret="1-k78-XcHhp_gdZF-I6JaIHp", pubsubAuthorizationCode="tASfQq7gn6sfbUSbwRufbMI0BYDzh1d7MBG2G7vdZpbhjmZfwDp5MkeaX0iMxn" ] { + Thing sdm_camera fish_cam [ deviceId="AVPHwTQCAhersqmQ3IXwyqSX-XyuVZXoiNSNPeHdIMKgYpYZolNP4S9LS5QDF2LeuM3BQcpBh_fOEZYxkeH6eoQdWEELqi" ] { + Channels: + Image : motion_event#image [ imageHeight=1080 ] + Image : person_event#image [ imageWidth=1920 ] + Image : sound_event#image [ imageHeight=1080 ] + } + Thing sdm_doorbell front_door [ deviceId="AVPHws4JWeIzZlru3DSxXoKnIgPntKpzax7a1Zwms8H0-HaRet2pTdTCPOTBZ74YDzYqq7w6XpEPwOTkBXtf4KCJ4nq9hq" ] { + Channels: + Image : chime_event#image [ imageWidth=1920 ] + } + Thing sdm_display kitchen_hub [ deviceId="AVPHw64dWG5CcAJdDNzBbHWgu91l4v8WA4CsJqgtrvMS3QrbDnurB0_WzZEwpcWaw8Y9rLEQXW0avEwCjTd40Gmia6ussU" ] + Thing sdm_thermostat living_thermostat [ deviceId="AVPHwQum_bx9LmiRfv6jv5qPcKho0vHx2HqqMUvXP3TD-TTDCJebbzkegpRMozU5t7GSeTQIzxdH2LYDsZO8RClcGj7CCT", refreshInterval=180 ] { + Channels: + Image : fan_timer_mode [ fanTimerDuration=7200 ] + } } ``` -### demo.items: +### sdm-demo.items + +``` +/* SDM Doorbell */ +Image Doorbell_Chime_Image "Chime Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:chime_event#image" } +DateTime Doorbell_Chime_Timestamp "Chime Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:chime_event#timestamp" } +String Doorbell_Stream_Token "Stream Token [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#current_token" } +DateTime Doorbell_Stream_Timestamp "Stream Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#expiration_timestamp" } +String Doorbell_Stream_Ext_Token "Stream Extension Token [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#extension_token" } +String Doorbell_Stream_URL "Stream Extension URL [%s]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:live_stream#url" } +Image Doorbell_Motion_Image "Motion Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:motion_event#image" } +DateTime Doorbell_Motion_Timestamp "Motion Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:motion_event#timestamp" } +Image Doorbell_Person_Image "Person Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:person_event#image" } +DateTime Doorbell_Person_Timestamp "Person Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:person_event#timestamp" } +Image Doorbell_Sound_Image "Sound Image" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:sound_event#image" } +DateTime Doorbell_Sound_Timestamp "Sound Timestamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_doorbell:demo_sdm_account:front_door:sound_event#timestamp" } + +/* SDM Thermostat */ +Number:Dimensionless Thermostat_Amb_Humidity "Ambient Humidity [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:ambient_humidity" } +Number:Temperature Thermostat_Amb_Temperature "Ambient Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:ambient_temperature" } +String Thermostat_Current_Eco_Mode "Current Eco Mode [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:current_eco_mode" } +String Thermostat_Current_Mode "Current Mode [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:current_mode" } +Switch Thermostat_Fan_Timer_Mode "Fan Timer Mode" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:fan_timer_mode" } +DateTime Thermostat_Fan_Timer_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:fan_timer_timeout" } +String Thermostat_HVAC_Status "HVAC Status [%s]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:hvac_status" } +Number:Temperature Thermostat_Max_Temperature "Max Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:maximum_temperature" } +Number:Temperature Thermostat_Min_Temperature "Min Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:minimum_temperature" } +Number:Temperature Thermostat_Target_temperature "Target Temperature [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:target_temperature" } +Number:Temperature Thermostat_Temperature_Cool "Temperature Cool [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:temperature_cool" } +Number:Temperature Thermostat_Temperature_Heat "Temperature Heat [%.1f %unit%]" { channel="nest:sdm_thermostat:demo_sdm_account:living_thermostat:temperature_heat" } +``` + +### wwn-demo.things + +``` +Bridge nest:wwn_account:demo_wwn_account [ productId="8fdf9885-ca07-4252-1aa3-f3d5ca9589e0", productSecret="QITLR3iyUlWaj9dbvCxsCKp4f", accessToken="c.6rse1xtRk2UANErcY0XazaqPHgbvSSB6owOrbZrZ6IXrmqhsr9QTmcfaiLX1l0ULvlI5xLp01xmKeiojHqozLQbNM8yfITj1LSdK28zsUft1aKKH2mDlOeoqZKBdVIsxyZk4orH0AvKEZ5aY" ] { + Thing wwn_camera fish_cam [ deviceId="qw0NNE8ruxA9AGJkTaFH3KeUiJaONWKiH9Gh3RwwhHClonIexTtufQ" ] + Thing wwn_smoke_detector hallway_smoke [ deviceId="Tzvibaa3lLKnHpvpi9OQeCI_z5rfkBAV" ] + Thing wwn_structure home [ structureId="20wKjydArmMV3kOluTA7JRcZg8HKBzTR-G_2nRXuIN1Bd6laGLOJQw" ] + Thing wwn_thermostat living_thermostat [ deviceId="ZqAKzSv6TO6PjBnOCXf9LSI_z5rfkBAV" ] +} +``` +### wwn-demo.items ``` -/* 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" } +/* WWN Camera */ +String Cam_App_URL "App URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#app_url" } +Switch Cam_Audio_Input_Enabled "Audio Input Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#audio_input_enabled" } +DateTime Cam_Last_Online_Change "Last Online Change [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#last_online_change" } +String Cam_Snapshot_URL "Snapshot URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#snapshot_url" } +Switch Cam_Streaming "Streaming" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#streaming" } +Switch Cam_Public_Share_Enabled "Public Share Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#public_share_enabled" } +String Cam_Public_Share_URL "Public Share URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#public_share_url" } +Switch Cam_Video_History_Enabled "Video History Enabled" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#video_history_enabled" } +String Cam_Web_URL "Web URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:camera#web_url" } +String Cam_LE_Activity_Zones "Last Event Activity Zones [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#activity_zones" } +String Cam_LE_Animated_Image_URL "Last Event Animated Image URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#animated_image_url" } +String Cam_LE_App_URL "Last Event App URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#app_url" } +DateTime Cam_LE_End_Time "Last Event End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#end_time" } +Switch Cam_LE_Has_Motion "Last Event Has Motion" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_motion" } +Switch Cam_LE_Has_Person "Last Event Has Person" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_person" } +Switch Cam_LE_Has_Sound "Last Event Has Sound" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#has_sound" } +String Cam_LE_Image_URL "Last Event Image URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#image_url" } +DateTime Cam_LE_Start_Time "Last Event Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#start_time" } +DateTime Cam_LE_URLs_Expire_Time "Last Event URLs Expire Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#urls_expire_time" } +String Cam_LE_Web_URL "Last Event Web URL [%s]" { channel="nest:wwn_camera:demo_wwn_account:fish_cam:last_event#web_url" } + +/* WWN Smoke Detector */ +String Smoke_CO_Alarm "CO Alarm [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:co_alarm_state" } +Switch Smoke_Battery_Low "Battery Low" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:low_battery" } +Switch Smoke_Manual_Test "Manual Test" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:manual_test_active" } +DateTime Smoke_Last_Connection "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:last_connection" } +DateTime Smoke_Last_Manual_Test "Last Manual Test [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:last_manual_test_time" } +String Smoke_Smoke_Alarm "Smoke Alarm [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:smoke_alarm_state" } +String Smoke_UI_Color "UI Color [%s]" { channel="nest:wwn_smoke_detector:demo_wwn_account:hallway_smoke:ui_color_state" } + +/* WWN Thermostat */ +Switch Thermostat_Can_Cool "Can Cool" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:can_cool" } +Switch Thermostat_Can_Heat "Can Heat" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:can_heat" } +Number:Temperature Therm_EMaxSP "Eco Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:eco_max_set_point" } +Number:Temperature Therm_EMinSP "Eco Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:eco_min_set_point" } +Switch Thermostat_FT_Active "Fan Timer Active" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_active" } +Number:Time Thermostat_FT_Duration "Fan Timer Duration [%d %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_duration" } +DateTime Thermostat_FT_Timeout "Fan Timer Timeout [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:fan_timer_timeout" } +Switch Thermostat_Has_Fan "Has Fan" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:has_fan" } +Switch Thermostat_Has_Leaf "Has Leaf" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:has_leaf" } +Number:Dimensionless Therm_Hum "Humidity [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:humidity" } +DateTime Thermostat_Last_Conn "Last Connection [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:last_connection" } +Switch Thermostat_Locked "Locked" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked" } +Number:Temperature Therm_LMaxSP "Locked Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked_max_set_point" } +Number:Temperature Therm_LMinSP "Locked Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:locked_min_set_point" } +Number:Temperature Therm_Max_SP "Max Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:max_set_point" } +Number:Temperature Therm_Min_SP "Min Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:min_set_point" } +String Thermostat_Mode "Mode [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:mode" } +String Thermostat_Previous_Mode "Previous Mode [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:previous_mode" } +String Thermostat_State "State [%s]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:state" } +Number:Temperature Thermostat_SP "Set Point [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:set_point" } +Switch Thermostat_Sunlight_CA "Sunlight Correction Active" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:sunlight_correction_active" } +Switch Thermostat_Sunlight_CE "Sunlight Correction Enabled" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:sunlight_correction_enabled" } +Number:Temperature Therm_Temp "Temperature [%.1f %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:temperature" } +Number:Time Therm_Time_To_Target "Time To Target [%d %unit%]" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:time_to_target" } +Switch Thermostat_Using_Em_Heat "Using Emergency Heat" { channel="nest:wwn_thermostat:demo_wwn_account:living_thermostat:using_emergency_heat" } + +/* WWN Structure */ +String Home_Away "Away [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:away" } +String Home_Country_Code "Country Code [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:country_code" } +String Home_CO_Alarm_State "CO Alarm State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:co_alarm_state" } +DateTime Home_ETA "ETA [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:eta_begin" } +DateTime Home_PP_End_Time "PP End Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:peak_period_end_time" } +DateTime Home_PP_Start_Time "PP Start Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" { channel="nest:wwn_structure:demo_wwn_account:home:peak_period_start_time" } +String Home_Postal_Code "Postal Code [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:postal_code" } +Switch Home_Rush_Hour_Rewards "Rush Hour Rewards" { channel="nest:wwn_structure:demo_wwn_account:home:rush_hour_rewards_enrollment" } +String Home_Security_State "Security State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:security_state" } +String Home_Smoke_Alarm_State "Smoke Alarm State [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:smoke_alarm_state" } +String Home_Time_Zone "Time Zone [%s]" { channel="nest:wwn_structure:demo_wwn_account:home:time_zone" } ``` ## Attribution diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestHandlerFactory.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestHandlerFactory.java deleted file mode 100644 index d7cf754cfb9fb..0000000000000 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestHandlerFactory.java +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2010-2021 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.nest.internal; - -import static java.util.stream.Collectors.toSet; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; - -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import javax.ws.rs.client.ClientBuilder; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.discovery.NestDiscoveryService; -import org.openhab.binding.nest.internal.handler.NestBridgeHandler; -import org.openhab.binding.nest.internal.handler.NestCameraHandler; -import org.openhab.binding.nest.internal.handler.NestSmokeDetectorHandler; -import org.openhab.binding.nest.internal.handler.NestStructureHandler; -import org.openhab.binding.nest.internal.handler.NestThermostatHandler; -import org.openhab.core.config.discovery.DiscoveryService; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.ThingUID; -import org.openhab.core.thing.binding.BaseThingHandlerFactory; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.jaxrs.client.SseEventSourceFactory; - -/** - * The {@link NestHandlerFactory} is responsible for creating things and thing - * handlers. It also sets up the discovery service to track things from the bridge - * when the bridge is created. - * - * @author David Bennett - Initial contribution - */ -@NonNullByDefault -@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest") -public class NestHandlerFactory extends BaseThingHandlerFactory { - private static final Set 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> 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); - } -} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/discovery/NestDiscoveryService.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/discovery/NestDiscoveryService.java deleted file mode 100644 index 14ca64a2e402e..0000000000000 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/discovery/NestDiscoveryService.java +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) 2010-2021 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.nest.internal.discovery; - -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nest.internal.config.NestDeviceConfiguration; -import org.openhab.binding.nest.internal.config.NestStructureConfiguration; -import org.openhab.binding.nest.internal.data.BaseNestDevice; -import org.openhab.binding.nest.internal.data.Camera; -import org.openhab.binding.nest.internal.data.SmokeDetector; -import org.openhab.binding.nest.internal.data.Structure; -import org.openhab.binding.nest.internal.data.Thermostat; -import org.openhab.binding.nest.internal.handler.NestBridgeHandler; -import org.openhab.binding.nest.internal.listener.NestThingDataListener; -import org.openhab.core.config.discovery.AbstractDiscoveryService; -import org.openhab.core.config.discovery.DiscoveryResultBuilder; -import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.ThingUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This service connects to the Nest bridge and creates the correct discovery results for Nest devices - * as they are found through the API. - * - * @author David Bennett - Initial contribution - * @author Wouter Born - Add representation properties - */ -@NonNullByDefault -public class NestDiscoveryService extends AbstractDiscoveryService { - - private static final Set 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 cameraDiscoveryDataListener = new DiscoveryDataListener<>(Camera.class, - THING_TYPE_CAMERA, this::addDeviceDiscoveryResult); - private final DiscoveryDataListener smokeDetectorDiscoveryDataListener = new DiscoveryDataListener<>( - SmokeDetector.class, THING_TYPE_SMOKE_DETECTOR, this::addDeviceDiscoveryResult); - private final DiscoveryDataListener structureDiscoveryDataListener = new DiscoveryDataListener<>( - Structure.class, THING_TYPE_STRUCTURE, this::addStructureDiscoveryResult); - private final DiscoveryDataListener thermostatDiscoveryDataListener = new DiscoveryDataListener<>( - Thermostat.class, THING_TYPE_THERMOSTAT, this::addDeviceDiscoveryResult); - - @SuppressWarnings("rawtypes") - private final List discoveryDataListeners = Stream.of(cameraDiscoveryDataListener, - smokeDetectorDiscoveryDataListener, structureDiscoveryDataListener, thermostatDiscoveryDataListener) - .collect(Collectors.toList()); - - private final NestBridgeHandler bridge; - - private static class DiscoveryDataListener implements NestThingDataListener { - private Class dataClass; - private ThingTypeUID thingTypeUID; - private BiConsumer onDiscovered; - - private DiscoveryDataListener(Class dataClass, ThingTypeUID thingTypeUID, - BiConsumer 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 void addDiscoveryResultsFromLastUpdates(Class dataClass, ThingTypeUID thingTypeUID, - BiConsumer onDiscovered) { - List 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 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 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 - } -} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMBindingConstants.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMBindingConstants.java new file mode 100644 index 0000000000000..d6e92c6a2a9a9 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMBindingConstants.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm; + +import static java.util.Map.entry; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nest.internal.sdm.dto.SDMDeviceType; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SDMBindingConstants} class defines common constants, which are used for the SDM implementation in the + * binding. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMBindingConstants { + + private static final String BINDING_ID = "nest"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "sdm_account"); + public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "sdm_camera"); + public static final ThingTypeUID THING_TYPE_DISPLAY = new ThingTypeUID(BINDING_ID, "sdm_display"); + public static final ThingTypeUID THING_TYPE_DOORBELL = new ThingTypeUID(BINDING_ID, "sdm_doorbell"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "sdm_thermostat"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_CAMERA, + THING_TYPE_DISPLAY, THING_TYPE_DOORBELL, THING_TYPE_THERMOSTAT); + + // Maps SDM device types to Thing Types UIDs + public static final Map SDM_THING_TYPE_MAPPING = Map.ofEntries( + entry(SDMDeviceType.CAMERA, THING_TYPE_CAMERA), // + entry(SDMDeviceType.DISPLAY, THING_TYPE_DISPLAY), // + entry(SDMDeviceType.DOORBELL, THING_TYPE_DOORBELL), // + entry(SDMDeviceType.THERMOSTAT, THING_TYPE_THERMOSTAT)); + + // List of all Channel ids + public static final String CHANNEL_CHIME_EVENT_IMAGE = "chime_event#image"; + public static final String CHANNEL_CHIME_EVENT_TIMESTAMP = "chime_event#timestamp"; + public static final String CHANNEL_LIVE_STREAM_URL = "live_stream#url"; + public static final String CHANNEL_LIVE_STREAM_CURRENT_TOKEN = "live_stream#current_token"; + public static final String CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP = "live_stream#expiration_timestamp"; + public static final String CHANNEL_LIVE_STREAM_EXTENSION_TOKEN = "live_stream#extension_token"; + public static final String CHANNEL_MOTION_EVENT_IMAGE = "motion_event#image"; + public static final String CHANNEL_MOTION_EVENT_TIMESTAMP = "motion_event#timestamp"; + public static final String CHANNEL_PERSON_EVENT_IMAGE = "person_event#image"; + public static final String CHANNEL_PERSON_EVENT_TIMESTAMP = "person_event#timestamp"; + public static final String CHANNEL_SOUND_EVENT_IMAGE = "sound_event#image"; + public static final String CHANNEL_SOUND_EVENT_TIMESTAMP = "sound_event#timestamp"; + + public static final String CHANNEL_AMBIENT_HUMIDITY = "ambient_humidity"; + public static final String CHANNEL_AMBIENT_TEMPERATURE = "ambient_temperature"; + public static final String CHANNEL_CURRENT_ECO_MODE = "current_eco_mode"; + public static final String CHANNEL_CURRENT_MODE = "current_mode"; + public static final String CHANNEL_FAN_TIMER_MODE = "fan_timer_mode"; + public static final String CHANNEL_FAN_TIMER_TIMEOUT = "fan_timer_timeout"; + public static final String CHANNEL_HVAC_STATUS = "hvac_status"; + public static final String CHANNEL_MAXIMUM_TEMPERATURE = "maximum_temperature"; + public static final String CHANNEL_MINIMUM_TEMPERATURE = "minimum_temperature"; + public static final String CHANNEL_TARGET_TEMPERATURE = "target_temperature"; + + // List of all configuration property IDs + public static final String CONFIG_PROPERTY_FAN_TIMER_DURATION = "fanTimerDuration"; + public static final String CONFIG_PROPERTY_IMAGE_HEIGHT = "imageHeight"; + public static final String CONFIG_PROPERTY_IMAGE_WIDTH = "imageWidth"; + + // List of all property IDs + public static final String PROPERTY_AUDIO_CODECS = "audioCodecs"; + public static final String PROPERTY_CUSTOM_NAME = "customName"; + public static final String PROPERTY_MAX_IMAGE_RESOLUTION = "maxImageResolution"; + public static final String PROPERTY_MAX_VIDEO_RESOLUTION = "maxVideoResolution"; + public static final String PROPERTY_SUPPORTED_PROTOCOLS = "supportedProtocols"; + public static final String PROPERTY_ROOM = "room"; + public static final String PROPERTY_TEMPERATURE_SCALE = "temperatureScale"; + public static final String PROPERTY_VIDEO_CODECS = "videoCodecs"; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMThingHandlerFactory.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMThingHandlerFactory.java new file mode 100644 index 0000000000000..8cb7395761ff4 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMThingHandlerFactory.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm; + +import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.handler.SDMAccountHandler; +import org.openhab.binding.nest.internal.sdm.handler.SDMCameraHandler; +import org.openhab.binding.nest.internal.sdm.handler.SDMThermostatHandler; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SDMThingHandlerFactory} is responsible for creating SDM thing handlers. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest") +@NonNullByDefault +public class SDMThingHandlerFactory extends BaseThingHandlerFactory { + + private HttpClientFactory httpClientFactory; + private OAuthFactory oAuthFactory; + private final TimeZoneProvider timeZoneProvider; + + @Activate + public SDMThingHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference OAuthFactory oAuthFactory, final @Reference TimeZoneProvider timeZoneProvider) { + this.httpClientFactory = httpClientFactory; + this.oAuthFactory = oAuthFactory; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { + return new SDMAccountHandler((Bridge) thing, httpClientFactory, oAuthFactory); + } else if (thingTypeUID.equals(THING_TYPE_CAMERA)) { + return new SDMCameraHandler(thing, timeZoneProvider); + } else if (thingTypeUID.equals(THING_TYPE_DISPLAY)) { + return new SDMCameraHandler(thing, timeZoneProvider); + } else if (thingTypeUID.equals(THING_TYPE_DOORBELL)) { + return new SDMCameraHandler(thing, timeZoneProvider); + } else if (thingTypeUID.equals(THING_TYPE_THERMOSTAT)) { + return new SDMThermostatHandler(thing, timeZoneProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/PubSubAPI.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/PubSubAPI.java new file mode 100644 index 0000000000000..50aa7b639b2a3 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/PubSubAPI.java @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.api; + +import static org.eclipse.jetty.http.HttpHeader.*; +import static org.eclipse.jetty.http.HttpMethod.POST; +import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON; + +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubAcknowledgeRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubCreateRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullResponse; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingPubSubDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAccessTokenException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAuthorizationCodeException; +import org.openhab.binding.nest.internal.sdm.listener.PubSubSubscriptionListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthException; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PubSubAPI} implements a subset of the Pub/Sub REST API which allows for subscribing to SDM events. + * + * @author Wouter Born - Initial contribution + * + * @see https://cloud.google.com/pubsub/docs/reference/rest + * @see https://developers.google.com/nest/device-access/api/events + */ +@NonNullByDefault +public class PubSubAPI { + + private class Subscriber implements Runnable { + + private final String subscriptionId; + + Subscriber(String subscriptionId) { + this.subscriptionId = subscriptionId; + } + + @Override + public void run() { + if (!subscriptionListeners.containsKey(subscriptionId)) { + logger.debug("Stop receiving subscription '{}' messages since there are no listeners", subscriptionId); + return; + } + + try { + String messages = pullSubscriptionMessages(subscriptionId); + + PubSubPullResponse pullResponse = GSON.fromJson(messages, PubSubPullResponse.class); + + if (pullResponse != null && pullResponse.receivedMessages != null) { + logger.debug("Subscription '{}' has {} new message(s)", subscriptionId, + pullResponse.receivedMessages.size()); + forEachListener((listener) -> pullResponse.receivedMessages + .forEach((message) -> listener.onMessage(message.message))); + List ackIds = pullResponse.receivedMessages.stream().map(message -> message.ackId) + .collect(Collectors.toList()); + acknowledgeSubscriptionMessages(subscriptionId, ackIds); + } else { + forEachListener((listener) -> listener.onNoNewMessages()); + } + + scheduler.submit(this); + } catch (FailedSendingPubSubDataException e) { + logger.debug("Expected exception while pulling message for '{}' subscription", subscriptionId, e); + Throwable cause = e.getCause(); + if (!(cause instanceof InterruptedException)) { + forEachListener((listener) -> listener.onError(e)); + scheduler.schedule(this, RETRY_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS); + } + } catch (InvalidPubSubAccessTokenException e) { + logger.warn("Cannot pull messages for '{}' subscription (access token invalid)", subscriptionId, e); + forEachListener((listener) -> listener.onError(e)); + } catch (Exception e) { + logger.warn("Unexpected exception while pulling message for '{}' subscription", subscriptionId, e); + forEachListener((listener) -> listener.onError(e)); + scheduler.schedule(this, RETRY_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS); + } + } + + private void forEachListener(Consumer consumer) { + Set listeners = subscriptionListeners.get(subscriptionId); + if (listeners != null) { + listeners.forEach(consumer::accept); + } else { + logger.debug("Subscription '{}' has no listeners", subscriptionId); + } + } + } + + private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth"; + private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; + private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"; + + private static final String PUBSUB_HANDLE_FORMAT = "%s.pubsub"; + private static final String PUBSUB_SCOPE = "https://www.googleapis.com/auth/pubsub"; + + private static final String PUBSUB_URL_PREFIX = "https://pubsub.googleapis.com/v1/"; + private static final int PUBSUB_PULL_MAX_MESSAGES = 10; + + private static final String APPLICATION_JSON = "application/json"; + private static final String BEARER = "Bearer "; + + private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1); + private static final Duration RETRY_TIMEOUT = Duration.ofSeconds(30); + + private final Logger logger = LoggerFactory.getLogger(PubSubAPI.class); + + private final HttpClient httpClient; + private final OAuthClientService oAuthService; + private final String projectId; + private final ScheduledThreadPoolExecutor scheduler; + private final Map> subscriptionListeners = new HashMap<>(); + + public PubSubAPI(HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory, String ownerId, String projectId, + String clientId, String clientSecret) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.projectId = projectId; + this.oAuthService = oAuthFactory.createOAuthClientService(String.format(PUBSUB_HANDLE_FORMAT, ownerId), + TOKEN_URL, AUTH_URL, clientId, clientSecret, PUBSUB_SCOPE, false); + scheduler = new ScheduledThreadPoolExecutor(3, new NamedThreadFactory(ownerId, true)); + } + + public void dispose() { + subscriptionListeners.clear(); + scheduler.shutdownNow(); + } + + public void authorizeClient(String authorizationCode) throws InvalidPubSubAuthorizationCodeException, IOException { + try { + oAuthService.getAccessTokenResponseByAuthorizationCode(authorizationCode, REDIRECT_URI); + } catch (OAuthException | OAuthResponseException e) { + throw new InvalidPubSubAuthorizationCodeException( + "Failed to authorize Pub/Sub client. Check the authorization code or generate a new one.", e); + } + } + + public void checkAccessTokenValidity() throws InvalidPubSubAccessTokenException, IOException { + getAuthorizationHeader(); + } + + private String acknowledgeSubscriptionMessages(String subscriptionId, List ackIds) + throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException { + logger.debug("Acknowleding {} message(s) for '{}' subscription", ackIds.size(), subscriptionId); + String url = getSubscriptionUrl(subscriptionId) + ":acknowledge"; + String requestContent = GSON.toJson(new PubSubAcknowledgeRequest(ackIds)); + return postJson(url, requestContent); + } + + public void addSubscriptionListener(String subscriptionId, PubSubSubscriptionListener listener) { + synchronized (subscriptionListeners) { + Set listeners = subscriptionListeners.get(subscriptionId); + if (listeners == null) { + listeners = new HashSet<>(); + subscriptionListeners.put(subscriptionId, listeners); + } + listeners.add(listener); + if (listeners.size() == 1) { + scheduler.submit(new Subscriber(subscriptionId)); + } + } + } + + public void removeSubscriptionListener(String subscriptionId, PubSubSubscriptionListener listener) { + synchronized (subscriptionListeners) { + Set listeners = subscriptionListeners.get(subscriptionId); + if (listeners != null) { + listeners.remove(listener); + if (listeners.isEmpty()) { + subscriptionListeners.remove(subscriptionId); + scheduler.getQueue().removeIf((runnable) -> runnable instanceof Subscriber + && ((Subscriber) runnable).subscriptionId.equals(subscriptionId)); + } + } + } + } + + public void createSubscription(String subscriptionId, String topicName) + throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException { + logger.debug("Creating '{}' subscription", subscriptionId); + String url = getSubscriptionUrl(subscriptionId); + String requestContent = GSON.toJson(new PubSubCreateRequest(topicName, true)); + putJson(url, requestContent); + } + + private String getAuthorizationHeader() throws InvalidPubSubAccessTokenException, IOException { + try { + AccessTokenResponse response = oAuthService.getAccessTokenResponse(); + if (response == null || response.getAccessToken() == null || response.getAccessToken().isEmpty()) { + throw new InvalidPubSubAccessTokenException( + "No Pub/Sub access token. Client may not have been authorized."); + } + return BEARER + response.getAccessToken(); + } catch (OAuthException | OAuthResponseException e) { + throw new InvalidPubSubAccessTokenException( + "Error fetching Pub/Sub access token. Check the authorization code or generate a new one.", e); + } + } + + private String getSubscriptionUrl(String subscriptionId) { + return PUBSUB_URL_PREFIX + "projects/" + projectId + "/subscriptions/" + subscriptionId; + } + + private String postJson(String url, String requestContent) + throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException { + try { + logger.debug("Posting JSON to: {}", url); + String response = httpClient.newRequest(url) // + .method(POST) // + .header(ACCEPT, APPLICATION_JSON) // + .header(AUTHORIZATION, getAuthorizationHeader()) // + .content(new StringContentProvider(requestContent), APPLICATION_JSON) // + .timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) // + .send() // + .getContentAsString(); + logger.debug("Response: {}", response); + return response; + } catch (ExecutionException | InterruptedException | IOException | TimeoutException e) { + throw new FailedSendingPubSubDataException("Failed to send JSON POST request", e); + } + } + + private String pullSubscriptionMessages(String subscriptionId) + throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException { + logger.debug("Pulling messages for '{}' subscription", subscriptionId); + String url = getSubscriptionUrl(subscriptionId) + ":pull"; + String requestContent = GSON.toJson(new PubSubPullRequest(PUBSUB_PULL_MAX_MESSAGES)); + return postJson(url, requestContent); + } + + private String putJson(String url, String requestContent) + throws FailedSendingPubSubDataException, InvalidPubSubAccessTokenException { + try { + logger.debug("Putting JSON to: {}", url); + String response = httpClient.newRequest(url) // + .method(HttpMethod.PUT) // + .header(ACCEPT, APPLICATION_JSON) // + .header(AUTHORIZATION, getAuthorizationHeader()) // + .content(new StringContentProvider(requestContent), APPLICATION_JSON) // + .timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) // + .send() // + .getContentAsString(); + logger.debug("Response: {}", response); + return response; + } catch (ExecutionException | InterruptedException | IOException | TimeoutException e) { + throw new FailedSendingPubSubDataException("Failed to send JSON PUT request", e); + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/SDMAPI.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/SDMAPI.java new file mode 100644 index 0000000000000..1f2eb6d72d1f7 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/api/SDMAPI.java @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.api; + +import static org.eclipse.jetty.http.HttpHeader.*; +import static org.eclipse.jetty.http.HttpMethod.*; +import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMDevice; +import org.openhab.binding.nest.internal.sdm.dto.SDMError; +import org.openhab.binding.nest.internal.sdm.dto.SDMError.SDMErrorDetails; +import org.openhab.binding.nest.internal.sdm.dto.SDMListDevicesResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMListRoomsResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMListStructuresResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMRoom; +import org.openhab.binding.nest.internal.sdm.dto.SDMStructure; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAuthorizationCodeException; +import org.openhab.binding.nest.internal.sdm.listener.SDMAPIRequestListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthException; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMAPI} implements the SDM REST API which allows for querying Nest device, structure and room information + * as well as executing device commands. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest + */ +@NonNullByDefault +public class SDMAPI { + + private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth"; + private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; + private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"; + + private static final String SDM_HANDLE_FORMAT = "%s.sdm"; + private static final String SDM_SCOPE = "https://www.googleapis.com/auth/sdm.service"; + + private static final String SDM_URL_PREFIX = "https://smartdevicemanagement.googleapis.com/v1/enterprises/"; + + private static final String APPLICATION_JSON = "application/json"; + private static final String BEARER = "Bearer "; + private static final String IMAGE_JPEG = "image/jpeg"; + + private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1); + + private final Logger logger = LoggerFactory.getLogger(SDMAPI.class); + + private final HttpClient httpClient; + private final OAuthClientService oAuthService; + private final String projectId; + + private final Set requestListeners = ConcurrentHashMap.newKeySet(); + + public SDMAPI(HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory, String ownerId, String projectId, + String clientId, String clientSecret) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.oAuthService = oAuthFactory.createOAuthClientService(String.format(SDM_HANDLE_FORMAT, ownerId), TOKEN_URL, + AUTH_URL, clientId, clientSecret, SDM_SCOPE, false); + this.projectId = projectId; + } + + public void dispose() { + requestListeners.clear(); + } + + public void authorizeClient(String authorizationCode) throws InvalidSDMAuthorizationCodeException, IOException { + try { + oAuthService.getAccessTokenResponseByAuthorizationCode(authorizationCode, REDIRECT_URI); + } catch (OAuthException | OAuthResponseException e) { + throw new InvalidSDMAuthorizationCodeException( + "Failed to authorize SDM client. Check the authorization code or generate a new one.", e); + } + } + + public void checkAccessTokenValidity() throws InvalidSDMAccessTokenException, IOException { + getAuthorizationHeader(); + } + + public void addRequestListener(SDMAPIRequestListener listener) { + requestListeners.add(listener); + } + + public void removeRequestListener(SDMAPIRequestListener listener) { + requestListeners.remove(listener); + } + + public @Nullable T executeDeviceCommand(String deviceId, + SDMCommandRequest request) throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Executing device command for: {}", deviceId); + String requestContent = GSON.toJson(request); + String responseContent = postJson(getDeviceUrl(deviceId) + ":executeCommand", requestContent); + return GSON.fromJson(responseContent, request.getResponseClass()); + } + + private String getAuthorizationHeader() throws InvalidSDMAccessTokenException, IOException { + try { + AccessTokenResponse response = oAuthService.getAccessTokenResponse(); + if (response == null || response.getAccessToken() == null || response.getAccessToken().isEmpty()) { + throw new InvalidSDMAccessTokenException("No SDM access token. Client may not have been authorized."); + } + return BEARER + response.getAccessToken(); + } catch (OAuthException | OAuthResponseException e) { + throw new InvalidSDMAccessTokenException( + "Error fetching SDM access token. Check the authorization code or generate a new one.", e); + } + } + + public byte[] getCameraImage(String url, String token, @Nullable BigDecimal imageWidth, + @Nullable BigDecimal imageHeight) throws FailedSendingSDMDataException { + try { + logger.debug("Getting camera image from: {}", url); + + Request request = httpClient.newRequest(url) // + .method(GET) // + .header(ACCEPT, IMAGE_JPEG) // + .header(AUTHORIZATION, token) // + .timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS); + + if (imageWidth != null) { + request = request.param("width", Long.toString(imageWidth.longValue())); + } else if (imageHeight != null) { + request = request.param("height", Long.toString(imageHeight.longValue())); + } + + ContentResponse contentResponse = request.send(); + logResponseErrors(contentResponse); + logger.debug("Retrieved camera image from: {}", url); + requestListeners.forEach(listener -> listener.onSuccess()); + return contentResponse.getContent(); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.debug("Failed to get camera image", e); + FailedSendingSDMDataException exception = new FailedSendingSDMDataException("Failed to get camera image", + e); + requestListeners.forEach(listener -> listener.onError(exception)); + throw exception; + } + } + + public @Nullable SDMDevice getDevice(String deviceId) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Getting device: {}", deviceId); + return GSON.fromJson(getJson(getDeviceUrl(deviceId)), SDMDevice.class); + } + + public @Nullable SDMStructure getStructure(String structureId) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Getting structure: {}", structureId); + return GSON.fromJson(getJson(getStructureUrl(structureId)), SDMStructure.class); + } + + public @Nullable SDMRoom getRoom(String structureId, String roomId) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Getting structure {} room: {}", structureId, roomId); + return GSON.fromJson(getJson(getRoomUrl(structureId, roomId)), SDMRoom.class); + } + + private String getProjectUrl() { + return SDM_URL_PREFIX + projectId; + } + + private String getDevicesUrl() { + return getProjectUrl() + "/devices"; + } + + private String getDevicesUrl(String pageToken) { + return getDevicesUrl() + "?pageToken=" + pageToken; + } + + private String getDeviceUrl(String deviceId) { + return getDevicesUrl() + "/" + deviceId; + } + + private String getStructuresUrl() { + return getProjectUrl() + "/structures"; + } + + private String getStructuresUrl(String pageToken) { + return getStructuresUrl() + "?pageToken=" + pageToken; + } + + private String getStructureUrl(String structureId) { + return getStructuresUrl() + "/" + structureId; + } + + private String getRoomsUrl(String structureId) { + return getStructureUrl(structureId) + "/rooms"; + } + + private String getRoomsUrl(String structureId, String pageToken) { + return getRoomsUrl(structureId) + "?pageToken=" + pageToken; + } + + private String getRoomUrl(String structureId, String roomId) { + return getRoomsUrl(structureId) + "/" + roomId; + } + + public List listDevices() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Listing devices"); + SDMListDevicesResponse response = GSON.fromJson(getJson(getDevicesUrl()), SDMListDevicesResponse.class); + List result = response == null ? List.of() : response.devices; + while (response != null && !response.nextPageToken.isEmpty()) { + response = GSON.fromJson(getJson(getDevicesUrl(response.nextPageToken)), SDMListDevicesResponse.class); + if (response != null) { + result.addAll(response.devices); + } + } + return result; + } + + public List listStructures() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Listing structures"); + SDMListStructuresResponse response = GSON.fromJson(getJson(getStructuresUrl()), + SDMListStructuresResponse.class); + List result = response == null ? List.of() : response.structures; + while (response != null && !response.nextPageToken.isEmpty()) { + response = GSON.fromJson(getJson(getStructuresUrl(response.nextPageToken)), + SDMListStructuresResponse.class); + if (response != null) { + result.addAll(response.structures); + } + } + return result; + } + + public List listRooms(String structureId) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("Listing rooms for structure: {}", structureId); + SDMListRoomsResponse response = GSON.fromJson(getJson(getRoomsUrl(structureId)), SDMListRoomsResponse.class); + List result = response == null ? List.of() : response.rooms; + while (response != null && !response.nextPageToken.isEmpty()) { + response = GSON.fromJson(getJson(getRoomsUrl(structureId, response.nextPageToken)), + SDMListRoomsResponse.class); + if (response != null) { + result.addAll(response.rooms); + } + } + return result; + } + + private void logResponseErrors(ContentResponse contentResponse) { + if (contentResponse.getStatus() >= 400) { + logger.debug("SDM API error: {}", contentResponse.getContentAsString()); + + SDMError error = GSON.fromJson(contentResponse.getContentAsString(), SDMError.class); + SDMErrorDetails details = error == null ? null : error.error; + + if (details != null && !details.message.isBlank()) { + logger.warn("SDM API error: {}", details.message); + } else { + logger.warn("SDM API error: {} (HTTP {})", contentResponse.getReason(), contentResponse.getStatus()); + } + } + } + + private String getJson(String url) throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + try { + logger.debug("Getting JSON from: {}", url); + ContentResponse contentResponse = httpClient.newRequest(url) // + .method(GET) // + .header(ACCEPT, APPLICATION_JSON) // + .header(AUTHORIZATION, getAuthorizationHeader()) // + .timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) // + .send(); + logResponseErrors(contentResponse); + String response = contentResponse.getContentAsString(); + logger.debug("Response: {}", response); + requestListeners.forEach(listener -> listener.onSuccess()); + return response; + } catch (ExecutionException | InterruptedException | IOException | TimeoutException e) { + logger.debug("Failed to send JSON GET request", e); + FailedSendingSDMDataException exception = new FailedSendingSDMDataException( + "Failed to send JSON GET request", e); + requestListeners.forEach(listener -> listener.onError(exception)); + throw exception; + } + } + + private String postJson(String url, String requestContent) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + try { + logger.debug("Posting JSON to: {}", url); + ContentResponse contentResponse = httpClient.newRequest(url) // + .method(POST) // + .header(ACCEPT, APPLICATION_JSON) // + .header(AUTHORIZATION, getAuthorizationHeader()) // + .content(new StringContentProvider(requestContent), APPLICATION_JSON) // + .timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) // + .send(); + logResponseErrors(contentResponse); + String response = contentResponse.getContentAsString(); + logger.debug("Response: {}", response); + requestListeners.forEach(listener -> listener.onSuccess()); + return response; + } catch (ExecutionException | InterruptedException | IOException | TimeoutException e) { + logger.debug("Failed to send JSON POST request", e); + FailedSendingSDMDataException exception = new FailedSendingSDMDataException( + "Failed to send JSON POST request", e); + requestListeners.forEach(listener -> listener.onError(exception)); + throw exception; + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMAccountConfiguration.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMAccountConfiguration.java new file mode 100644 index 0000000000000..a95a0104a5ad1 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMAccountConfiguration.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.config; + +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SDMAccountConfiguration} contains the configuration parameter values for the SDM and Pub/Sub APIs. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMAccountConfiguration { + + public static final String PUBSUB_AUTHORIZATION_CODE = "pubsubAuthorizationCode"; + public String pubsubAuthorizationCode = ""; + + public static final String PUBSUB_CLIENT_ID = "pubsubClientId"; + public String pubsubClientId = ""; + + public static final String PUBSUB_CLIENT_SECRET = "pubsubClientSecret"; + public String pubsubClientSecret = ""; + + public static final String PUBSUB_PROJECT_ID = "pubsubProjectId"; + public String pubsubProjectId = ""; + + public static final String PUBSUB_SUBSCRIPTION_ID = "pubsubSubscriptionId"; + public String pubsubSubscriptionId = ""; + + public static final String SDM_AUTHORIZATION_CODE = "sdmAuthorizationCode"; + public String sdmAuthorizationCode = ""; + + public static final String SDM_CLIENT_ID = "sdmClientId"; + public String sdmClientId = ""; + + public static final String SDM_CLIENT_SECRET = "sdmClientSecret"; + public String sdmClientSecret = ""; + + public static final String SDM_PRODUCT_ID = "sdmProductId"; + public String sdmProjectId = ""; + + public boolean usePubSub() { + return Stream.of(pubsubProjectId, pubsubSubscriptionId, pubsubClientId, pubsubClientSecret) + .noneMatch(String::isBlank); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMDeviceConfiguration.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMDeviceConfiguration.java new file mode 100644 index 0000000000000..f5afbe9acd839 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/config/SDMDeviceConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SDMDeviceConfiguration} contains the configuration parameter values for a SDM device. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMDeviceConfiguration { + + public static final String DEVICE_ID = "deviceId"; + public String deviceId = ""; + + public static final String REFRESH_INTERVAL = "refreshInterval"; + public int refreshInterval = 300; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/discovery/SDMDiscoveryService.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/discovery/SDMDiscoveryService.java new file mode 100644 index 0000000000000..4a3313a00a98b --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/discovery/SDMDiscoveryService.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.discovery; + +import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Future; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.config.SDMDeviceConfiguration; +import org.openhab.binding.nest.internal.sdm.dto.SDMDevice; +import org.openhab.binding.nest.internal.sdm.dto.SDMDeviceType; +import org.openhab.binding.nest.internal.sdm.dto.SDMParentRelation; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.binding.nest.internal.sdm.handler.SDMAccountHandler; +import org.openhab.binding.nest.internal.sdm.handler.SDMBaseHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMDiscoveryService} is discovers devices using the SDM API list devices method. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/list + */ +@NonNullByDefault +public class SDMDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(SDMDiscoveryService.class); + private @NonNullByDefault({}) SDMAccountHandler accountHandler; + private @Nullable Future discoveryJob; + + public SDMDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, 30, false); + } + + protected void activate(ComponentContext context) { + } + + @Override + public void deactivate() { + cancelDiscoveryJob(); + super.deactivate(); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return accountHandler; + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof SDMAccountHandler) { + accountHandler = (SDMAccountHandler) handler; + } + } + + @Override + protected void startScan() { + cancelDiscoveryJob(); + discoveryJob = scheduler.submit(this::discoverDevices); + } + + @Override + protected synchronized void stopScan() { + cancelDiscoveryJob(); + super.stopScan(); + } + + private void cancelDiscoveryJob() { + Future localDiscoveryJob = discoveryJob; + if (localDiscoveryJob != null) { + localDiscoveryJob.cancel(true); + } + } + + private void discoverDevices() { + ThingUID bridgeUID = accountHandler.getThing().getUID(); + logger.debug("Starting discovery scan for {}", bridgeUID); + try { + accountHandler.getAPI().listDevices().forEach(device -> addDeviceDiscoveryResult(bridgeUID, device)); + } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) { + logger.debug("Exception during discovery scan for {}", bridgeUID, e); + } + logger.debug("Finished discovery scan for {}", bridgeUID); + } + + private void addDeviceDiscoveryResult(ThingUID bridgeUID, SDMDevice device) { + SDMDeviceType type = device.type; + ThingTypeUID thingTypeUID = type == null ? null : SDM_THING_TYPE_MAPPING.get(type); + if (type == null || thingTypeUID == null) { + logger.debug("Ignoring unsupported device type: {}", type); + return; + } + + String deviceId = device.name.deviceId; + ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceId); + + thingDiscovered(DiscoveryResultBuilder.create(thingUID) // + .withThingType(thingTypeUID) // + .withLabel(getDeviceLabel(device, type)) // + .withBridge(bridgeUID) // + .withProperty(SDMDeviceConfiguration.DEVICE_ID, deviceId) // + .withProperties(new HashMap<>(SDMBaseHandler.getDeviceProperties(device))) // + .withRepresentationProperty(SDMDeviceConfiguration.DEVICE_ID) // + .build() // + ); + } + + private String getDeviceLabel(SDMDevice device, SDMDeviceType type) { + String label = device.traits.deviceInfo.customName; + if (!label.isBlank()) { + return label; + } + + List parentRelations = device.parentRelations; + String displayName = !parentRelations.isEmpty() ? parentRelations.get(0).displayName : ""; + String typeLabel = type.toLabel(); + + return displayName.isBlank() ? String.format("Nest %s", typeLabel) + : String.format("Nest %s %s", displayName, typeLabel); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponses.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponses.java new file mode 100644 index 0000000000000..da1e215c242b9 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponses.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * The {@link PubSubRequestsResponses} provides classes used for mapping Pub/Sub REST API requests and responses. + * Only the subset of requests/responses and fields that are used by the binding are implemented. + * + * @author Wouter Born - Initial contribution + * + * @see https://cloud.google.com/pubsub/docs/reference/rest + */ +public class PubSubRequestsResponses { + + // Method: projects.subscriptions.acknowledge + + /** + * Acknowledges the messages associated with the ackIds in the AcknowledgeRequest. The Pub/Sub system can remove the + * relevant messages from the subscription. + * + * Acknowledging a message whose ack deadline has expired may succeed, but such a message may be redelivered later. + * Acknowledging a message more than once will not result in an error. + */ + public static class PubSubAcknowledgeRequest { + + public List ackIds; + + public PubSubAcknowledgeRequest(List ackIds) { + this.ackIds = ackIds; + } + } + + // Method: projects.subscriptions.create + + /** + * Creates a subscription to a given topic. See the resource name rules. If the subscription already exists, returns + * ALREADY_EXISTS. If the corresponding topic doesn't exist, returns NOT_FOUND. + * + * If the name is not provided in the request, the server will assign a random name for this subscription on the + * same project as the topic, conforming to the resource name format. The generated name is populated in the + * returned Subscription object. Note that for REST API requests, you must specify a name in the request. + */ + public static class PubSubCreateRequest { + + public String topic; + public boolean enableMessageOrdering; + + /** + * @param topic The name of the topic from which this subscription is receiving messages. Format is + * projects/{project}/topics/{topic}. + * @param enableMessageOrdering If true, messages published with the same orderingKey in the message will be + * delivered to the subscribers in the order in which they are received by the Pub/Sub system. + * Otherwise, they may be delivered in any order. + */ + public PubSubCreateRequest(String topic, boolean enableMessageOrdering) { + this.topic = topic; + this.enableMessageOrdering = enableMessageOrdering; + } + } + + // Method: projects.subscriptions.pull + + /** + * Pulls messages from the server. The server may return UNAVAILABLE if there are too many concurrent pull requests + * pending for the given subscription. + * + * A {@link PubSubPullResponse} is returned when successful. + */ + public static class PubSubPullRequest { + + public int maxMessages; + + /** + * @param maxMessages The maximum number of messages to return for this request. Must be a positive integer. The + * Pub/Sub system may return fewer than the number specified. + */ + public PubSubPullRequest(int maxMessages) { + this.maxMessages = maxMessages; + } + } + + /** + * A message that is published by publishers and consumed by subscribers. + */ + public static class PubSubMessage { + /** + * The message data field. A base64-encoded string. + */ + public String data; + + /** + * ID of this message, assigned by the server when the message is published. Guaranteed to be unique within the + * topic. This value may be read by a subscriber that receives a PubsubMessage via a + * subscriptions.pull call or a push delivery. It must not be populated by the publisher in a + * topics.publish call. + */ + public String messageId; + + /** + * The time at which the message was published, populated by the server when it receives the topics.publish + * call. It must not be populated by the publisher in a topics publish call. + * + * A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. + * Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + */ + public ZonedDateTime publishTime; + } + + /** + * A message and its corresponding acknowledgment ID. + */ + public static class PubSubReceivedMessage { + /** + * This ID can be used to acknowledge the received message. + */ + public String ackId; + + /** + * The message. + */ + public PubSubMessage message; + } + + /** + * Response to a {@link PubSubPullRequest}. + */ + public class PubSubPullResponse { + /** + * Received Pub/Sub messages. The list will be empty if there are no more messages available in the backlog. For + * JSON, the response can be entirely empty. The Pub/Sub system may return fewer than the maxMessages requested + * even if there are more messages available in the backlog. + */ + public List receivedMessages; + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommands.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommands.java new file mode 100644 index 0000000000000..0cd3ee49493d4 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommands.java @@ -0,0 +1,318 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static java.util.Map.entry; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode; + +/** + * The {@link SDMCommands} provides classes used for mapping all SDM REST API device command requests and responses. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/executeCommand + */ +public class SDMCommands { + + /** + * Command request parent. + */ + public abstract static class SDMCommandRequest { + private final String command; + private final Map params = new LinkedHashMap<>(); + + @SafeVarargs + private SDMCommandRequest(String command, Entry... params) { + this.command = command; + for (Entry param : params) { + this.params.put(param.getKey(), param.getValue()); + } + } + + public String getCommand() { + return command; + } + + public Map getParams() { + return params; + } + + @SuppressWarnings("unchecked") + public Class getResponseClass() { + return (Class) SDMCommandResponse.class; + } + } + + /** + * Command response parent. This class is also used for responses without additional data. + */ + public static class SDMCommandResponse { + } + + // CameraEventImage trait commands + + /** + * Generates a download URL for the image related to a camera event. + */ + public static class SDMGenerateCameraImageRequest extends SDMCommandRequest { + + /** + * Event images expire 30 seconds after the event is published. Make sure to download the image prior to + * expiration. + */ + public static final Duration EVENT_IMAGE_VALIDITY = Duration.ofSeconds(30); + + /** + * @param eventId ID of the camera event to request a related image for. + */ + public SDMGenerateCameraImageRequest(String eventId) { + super("sdm.devices.commands.CameraEventImage.GenerateImage", entry("eventId", eventId)); + } + + @Override + public Class getResponseClass() { + return SDMGenerateCameraImageResponse.class; + } + } + + public static class SDMGenerateCameraImageResults { + /** + * The URL to download the camera image from. + */ + public String url; + + /** + * Token to use in the HTTP Authorization header when downloading the camera image. + */ + public String token; + } + + public static class SDMGenerateCameraImageResponse extends SDMCommandResponse { + public SDMGenerateCameraImageResults results; + } + + // CameraLiveStream trait commands + + /** + * Request a token to access a camera RTSP live stream URL. + */ + public static class SDMGenerateCameraRtspStreamRequest + extends SDMCommandRequest { + public SDMGenerateCameraRtspStreamRequest() { + super("sdm.devices.commands.CameraLiveStream.GenerateRtspStream"); + } + + @Override + public Class getResponseClass() { + return SDMGenerateCameraRtspStreamResponse.class; + } + } + + /** + * Camera RTSP live stream URLs. + */ + public static class SDMCameraRtspStreamUrls { + public String rtspUrl; + } + + public static class SDMGenerateCameraRtspStreamResults { + /** + * Camera RTSP live stream URLs. + */ + public SDMCameraRtspStreamUrls streamUrls; + + /** + * Token to use to extend the {@link #streamToken} for an RTSP live stream. + */ + public String streamExtensionToken; + + /** + * Token to use to access an RTSP live stream. + */ + public String streamToken; + + /** + * Time at which both {@link #streamExtensionToken} and {@link #streamToken} expire. + */ + public ZonedDateTime expiresAt; + } + + public static class SDMGenerateCameraRtspStreamResponse extends SDMCommandResponse { + public SDMGenerateCameraRtspStreamResults results; + } + + /** + * Request a new RTSP live stream URL access token to replace a valid RTSP access token before it expires. This is + * also used to replace a valid RTSP token from a previous ExtendRtspStream command request. + */ + public static class SDMExtendCameraRtspStreamRequest extends SDMCommandRequest { + /** + * @param streamExtensionToken Token to use to request an extension to the RTSP streaming token. + */ + public SDMExtendCameraRtspStreamRequest(String streamExtensionToken) { + super("sdm.devices.commands.CameraLiveStream.ExtendRtspStream", + entry("streamExtensionToken", streamExtensionToken)); + } + + @Override + public Class getResponseClass() { + return SDMExtendCameraRtspStreamResponse.class; + } + } + + public static class SDMExtendCameraRtspStreamResults { + /** + * Token to use to view an existing RTSP live stream and to request an extension to the streaming token. + */ + public String streamExtensionToken; + + /** + * New token to use to access an existing RTSP live stream. + */ + public String streamToken; + + /** + * Time at which both {@link #streamExtensionToken} and {@link #streamToken} expire. + */ + public ZonedDateTime expiresAt; + } + + public static class SDMExtendCameraRtspStreamResponse extends SDMCommandResponse { + public SDMExtendCameraRtspStreamResults results; + } + + /** + * Invalidates a valid RTSP access token and stops the RTSP live stream tied to that access token. + */ + public static class SDMStopCameraRtspStreamRequest extends SDMCommandRequest { + /** + * @param streamExtensionToken Token to use to invalidate an existing RTSP live stream. + */ + public SDMStopCameraRtspStreamRequest(String streamExtensionToken) { + super("sdm.devices.commands.CameraLiveStream.StopRtspStream", + entry("streamExtensionToken", streamExtensionToken)); + } + } + + // Fan trait commands + + /** + * Change the fan timer. + */ + public static class SDMSetFanTimerRequest extends SDMCommandRequest { + public SDMSetFanTimerRequest(SDMFanTimerMode timerMode) { + super("sdm.devices.commands.Fan.SetTimer", entry("timerMode", timerMode.name())); + } + + /** + * @param duration Specifies the length of time in seconds that the timer is set to run. + * Range: "1s" to "43200s" + * Default: "900s" + */ + public SDMSetFanTimerRequest(SDMFanTimerMode timerMode, Duration duration) { + super("sdm.devices.commands.Fan.SetTimer", entry("timerMode", timerMode.name()), + entry("duration", String.valueOf(duration.toSeconds()) + "s")); + } + } + + // ThermostatEco trait commands + + /** + * Change the thermostat Eco mode. + * + * To change the thermostat mode to HEAT, COOL, or HEATCOOL, use the {@link SDMSetThermostatModeRequest}. + *
+ *
+ * This command impacts other traits, based on the current status of, or changes to, the Eco mode: + *
    + *
  • If Eco mode is OFF, the thermostat mode will default to the last standard mode (HEAT, COOL, HEATCOOL, or OFF) + * that was active.
  • + *
  • If Eco mode is MANUAL_ECO: + *
      + *
    • Commands for the ThermostatTemperatureSetpoint trait are rejected.
    • + *
    • Temperature setpoints are not returned by the ThermostatTemperatureSetpoint trait.
    • + *
    + *
  • + *
+ * + * Some thermostat models do not support changing the Eco mode when the thermostat mode is OFF, according to the + * ThermostatMode trait. The thermostat mode must be changed to HEAT, COOL, or HEATCOOL prior to changing the Eco + * mode. + */ + public static class SDMSetThermostatEcoModeRequest extends SDMCommandRequest { + public SDMSetThermostatEcoModeRequest(SDMThermostatEcoMode mode) { + super("sdm.devices.commands.ThermostatEco.SetMode", entry("mode", mode.name())); + } + } + + // ThermostatMode trait commands + + /** + * Change the thermostat mode. + */ + public static class SDMSetThermostatModeRequest extends SDMCommandRequest { + public SDMSetThermostatModeRequest(SDMThermostatMode mode) { + super("sdm.devices.commands.ThermostatMode.SetMode", entry("mode", mode.name())); + } + } + + // ThermostatTemperatureSetpoint trait commands + + /** + * Sets the target temperature when the thermostat is in COOL mode. + */ + public static class SDMSetThermostatCoolSetpointRequest extends SDMCommandRequest { + /** + * @param temperature the target temperature in degrees Celsius + */ + public SDMSetThermostatCoolSetpointRequest(BigDecimal temperature) { + super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool", entry("coolCelsius", temperature)); + } + } + + /** + * Sets the target temperature when the thermostat is in HEAT mode. + */ + public static class SDMSetThermostatHeatSetpointRequest extends SDMCommandRequest { + /** + * @param temperature the target temperature in degrees Celsius + */ + public SDMSetThermostatHeatSetpointRequest(BigDecimal temperature) { + super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat", entry("heatCelsius", temperature)); + } + } + + /** + * Sets the minimum and maximum temperatures when the thermostat is in HEATCOOL mode. + */ + public static class SDMSetThermostatRangeSetpointRequest extends SDMCommandRequest { + /** + * @param minTemperature the minimum target temperature in degrees Celsius + * @param maxTemperature the maximum target temperature in degrees Celsius + */ + public SDMSetThermostatRangeSetpointRequest(BigDecimal minTemperature, BigDecimal maxTemperature) { + super("sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange", entry("heatCelsius", minTemperature), + entry("coolCelsius", maxTemperature)); + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDevice.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDevice.java new file mode 100644 index 0000000000000..20cadf04563ac --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDevice.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * An instance of enterprise managed device in the property. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMDevice { + /** + * The resource name of the device. + */ + public SDMResourceName name = SDMResourceName.NAMELESS; + + /** + * Type of the device for general display purposes. + */ + public @Nullable SDMDeviceType type; + + /** + * Device traits. + */ + public SDMTraits traits = new SDMTraits(); + + /** + * Assignee details of the device. + */ + public List parentRelations = List.of(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceType.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceType.java new file mode 100644 index 0000000000000..f224a76817bef --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceType.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * Type of the SDM device. + * + * @author Wouter Born - Initial contribution + */ +public enum SDMDeviceType { + @SerializedName("sdm.devices.types.CAMERA") + CAMERA, + + @SerializedName("sdm.devices.types.DISPLAY") + DISPLAY, + + @SerializedName("sdm.devices.types.DOORBELL") + DOORBELL, + + @SerializedName("sdm.devices.types.THERMOSTAT") + THERMOSTAT; + + public String toLabel() { + return name().charAt(0) + name().toLowerCase().substring(1); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMError.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMError.java new file mode 100644 index 0000000000000..5e55218402611 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMError.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +/** + * An error response of the SDM API. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/errors/api + */ +public class SDMError { + + public static class SDMErrorDetails { + public int code; + public String message; + public String status; + } + + public SDMErrorDetails error; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMEvent.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMEvent.java new file mode 100644 index 0000000000000..e39956d04f3be --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMEvent.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SDMEvent} is used for mapping the SDM event data received from the SDM API in messages pulled from a + * Pub/Sub topic. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/api/events + */ +public class SDMEvent { + + /** + * An object that details information about the relation update. + */ + public static class SDMRelationUpdate { + public SDMRelationUpdateType type; + + /** + * The resource that the object now has a relation with. + */ + public SDMResourceName subject; + + /** + * The resource that triggered the event. + */ + public SDMResourceName object; + } + + public enum SDMRelationUpdateType { + CREATED, + DELETED, + UPDATED + } + + /** + * An object that details information about the resource update. + */ + public static class SDMResourceUpdate { + public SDMResourceName name; + public SDMTraits traits; + public SDMResourceUpdateEvents events; + } + + public static class SDMDeviceEvent { + public String eventId; + public String eventSessionId; + } + + public static class SDMResourceUpdateEvents extends SDMTraits { + @SerializedName("sdm.devices.events.CameraMotion.Motion") + public SDMDeviceEvent cameraMotionEvent; + + @SerializedName("sdm.devices.events.CameraPerson.Person") + public SDMDeviceEvent cameraPersonEvent; + + @SerializedName("sdm.devices.events.CameraSound.Sound") + public SDMDeviceEvent cameraSoundEvent; + + @SerializedName("sdm.devices.events.DoorbellChime.Chime") + public SDMDeviceEvent doorbellChimeEvent; + + public Stream eventStream() { + return Stream.of(cameraMotionEvent, cameraPersonEvent, cameraSoundEvent, doorbellChimeEvent) + .filter(Objects::nonNull); + } + + public List eventList() { + return eventStream().collect(Collectors.toList()); + } + + public Set eventSet() { + return eventStream().collect(Collectors.toSet()); + } + } + + /** + * The unique identifier for the event. + */ + public String eventId; + + /** + * An object that details information about the relation update. + */ + public SDMRelationUpdate relationUpdate; + + /** + * An object that indicates resources that might have similar updates to this event. + * The resource of the event itself (from the resourceUpdate object) will always be present in this object. + */ + public List resourceGroup; + + /** + * An object that details information about the resource update. + */ + public SDMResourceUpdate resourceUpdate; + + /** + * The time when the event occurred. + */ + public ZonedDateTime timestamp; + + /** + * A unique, obfuscated identifier that represents the user. + */ + public String userId; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMGson.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMGson.java new file mode 100644 index 0000000000000..dff2290e4247f --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMGson.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.lang.reflect.Type; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * The {@link SDMGson} class provides a {@link Gson} instance configured for (de)serializing all SDM and Pub/Sub data + * from/to JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMGson { + + public static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(SDMResourceName.class, new SDMResourceNameConverter()) // + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeConverter()) // + .create(); + + private static class SDMResourceNameConverter + implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(SDMResourceName src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + + @Override + public @Nullable SDMResourceName deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return new SDMResourceName(json.getAsString()); + } + } + + private static class ZonedDateTimeConverter + implements JsonSerializer, JsonDeserializer { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_DATE_TIME; + + @Override + public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(FORMATTER.format(src)); + } + + @Override + public @Nullable ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return ZonedDateTime.parse(json.getAsString()); + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMIdentifiable.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMIdentifiable.java new file mode 100644 index 0000000000000..eea2519437132 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMIdentifiable.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Interface for uniquely identifiable SDM objects (device, structure). + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public interface SDMIdentifiable { + + /** + * Returns the identifier that uniquely identifies the SDM object (deviceId or structureId). + */ + String getId(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponse.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponse.java new file mode 100644 index 0000000000000..15e0c7d7c40ea --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponse.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Lists devices managed by the enterprise. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.devices/list + */ +@NonNullByDefault +public class SDMListDevicesResponse { + /** + * The list of devices. + */ + public List devices = List.of(); + + /** + * The pagination token to retrieve the next page of results. + */ + public String nextPageToken = ""; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponse.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponse.java new file mode 100644 index 0000000000000..67cba4de57dcd --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponse.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Lists rooms managed by the enterprise. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.structures.rooms/list + */ +@NonNullByDefault +public class SDMListRoomsResponse { + /** + * The list of rooms. + */ + public List rooms = List.of(); + + /** + * The pagination token to retrieve the next page of results. + */ + public String nextPageToken = ""; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponse.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponse.java new file mode 100644 index 0000000000000..fde68ab957dd5 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponse.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Lists structures managed by the enterprise. + * + * @author Wouter Born - Initial contribution + * + * @see https://developers.google.com/nest/device-access/reference/rest/v1/enterprises.structures/list + */ +@NonNullByDefault +public class SDMListStructuresResponse { + /** + * The list of structures. + */ + public List structures = List.of(); + + /** + * The pagination token to retrieve the next page of results. + */ + public String nextPageToken = ""; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMParentRelation.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMParentRelation.java new file mode 100644 index 0000000000000..d5183771a35db --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMParentRelation.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +/** + * Represents device relationships, for instance, structure/room to which the device is assigned to. + * + * @author Wouter Born - Initial contribution + */ +public class SDMParentRelation { + /** + * The name of the relation. + */ + public SDMResourceName parent; + + /** + * The custom name of the relation. + */ + public String displayName; +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceName.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceName.java new file mode 100644 index 0000000000000..c835a1663d693 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceName.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A resource name uniquely identifies a structure, room or device. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMResourceName { + + public enum SDMResourceNameType { + DEVICE, + ROOM, + STRUCTURE, + UNKNOWN + } + + private static final Pattern PATTERN = Pattern + .compile("^enterprises/([^/]+)(/devices/([^/]+)|/structures/([^/]+)(/rooms/([^/]+))?)$"); + + public static final SDMResourceName NAMELESS = new SDMResourceName(""); + + public final String name; + public final String projectId; + public final String deviceId; + public final String structureId; + public final String roomId; + public final SDMResourceNameType type; + + public SDMResourceName(String name) { + this.name = name; + + Matcher matcher = PATTERN.matcher(name); + if (matcher.matches()) { + projectId = matcher.group(1); + deviceId = matcher.group(3) == null ? "" : matcher.group(3); + structureId = matcher.group(4) == null ? "" : matcher.group(4); + roomId = matcher.group(6) == null ? "" : matcher.group(6); + + if (!deviceId.isEmpty()) { + type = SDMResourceNameType.DEVICE; + } else if (!roomId.isEmpty()) { + type = SDMResourceNameType.ROOM; + } else if (!structureId.isEmpty()) { + type = SDMResourceNameType.STRUCTURE; + } else { + type = SDMResourceNameType.UNKNOWN; + } + } else { + projectId = ""; + deviceId = ""; + structureId = ""; + roomId = ""; + type = SDMResourceNameType.UNKNOWN; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + return prime * result + name.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return name.equals(((SDMResourceName) obj).name); + } + + @Override + public String toString() { + return name; + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMRoom.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMRoom.java new file mode 100644 index 0000000000000..1db6cbf03361f --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMRoom.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +/** + * An instance of enterprise managed room in a structure. + * + * @author Wouter Born - Initial contribution + */ +public class SDMRoom { + /** + * The resource name of the room. + */ + public SDMResourceName name = SDMResourceName.NAMELESS; + + /** + * Room traits. + */ + public SDMTraits traits = new SDMTraits(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMStructure.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMStructure.java new file mode 100644 index 0000000000000..f5c1d4090d4ee --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMStructure.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +/** + * An instance of an enterprise managed structure. + * + * @author Wouter Born - Initial contribution + */ +public class SDMStructure { + /** + * The resource name of the structure. + */ + public SDMResourceName name = SDMResourceName.NAMELESS; + + /** + * Structure traits. + */ + public SDMTraits traits = new SDMTraits(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMTraits.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMTraits.java new file mode 100644 index 0000000000000..cde8cab2faaab --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMTraits.java @@ -0,0 +1,441 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.gson.annotations.SerializedName; + +/** + * The common SDM traits that are used in the {@link SDMDevice} and {@link SDMEvent} types. + * + * @author Wouter Born - Initial contribution + */ +public class SDMTraits { + + /** + * This trait belongs to any device that supports generation of images from events. + */ + public static class SDMCameraEventImageTrait extends SDMCameraTrait { + } + + /** + * This trait belongs to any device that supports taking images. + */ + public static class SDMCameraImageTrait extends SDMCameraTrait { + /** + * Maximum image resolution that is supported. + */ + public SDMResolution maxImageResolution; + } + + /** + * This trait belongs to any device that supports live streaming. + */ + public static class SDMCameraLiveStreamTrait extends SDMCameraTrait { + /** + * Maximum resolution of the video live stream. + */ + public SDMResolution maxVideoResolution; + + /** + * Video codecs supported for the live stream. + */ + public List videoCodecs; + + /** + * Audio codecs supported for the live stream. + */ + public List audioCodecs; + + /** + * Protocols supported for the live stream. + */ + public List supportedProtocols; + } + + /** + * This trait belongs to any device that supports motion detection events. + */ + public static class SDMCameraMotionTrait extends SDMCameraTrait { + } + + /** + * This trait belongs to any device that supports person detection events. + */ + public static class SDMCameraPersonTrait extends SDMCameraTrait { + } + + /** + * This trait belongs to any device that supports sound detection events. + */ + public static class SDMCameraSoundTrait extends SDMCameraTrait { + } + + public static class SDMCameraTrait extends SDMTrait { + } + + public enum SDMConnectivityStatus { + OFFLINE, + ONLINE + } + + /** + * This trait belongs to any device that has connectivity information. + */ + public static class SDMConnectivityTrait extends SDMDeviceTrait { + /** + * Device connectivity status. + */ + public SDMConnectivityStatus status; + } + + /** + * This trait belongs to any device for device-related information. + */ + public static class SDMDeviceInfoTrait extends SDMDeviceTrait { + /** + * Custom name of the device. Corresponds to the Label value for a device in the Nest App. + */ + public String customName; + } + + /** + * This trait belongs to any device for device-related settings information. + */ + public static class SDMDeviceSettingsTrait extends SDMDeviceTrait { + /** + * Format of the degrees displayed on a Google Nest Thermostat. + */ + public SDMTemperatureScale temperatureScale; + } + + public static class SDMDeviceTrait extends SDMTrait { + } + + /** + * This trait belongs to any device that supports a doorbell chime and related press events. + */ + public static class SDMDoorbellChimeTrait extends SDMDoorbellTrait { + } + + public static class SDMDoorbellTrait extends SDMTrait { + } + + public enum SDMThermostatEcoMode { + MANUAL_ECO, + OFF + } + + /** + * This trait belongs to any device that has the system ability to control the fan. + */ + public static class SDMFanTrait extends SDMDeviceTrait { + /** + * Current timer mode. + */ + public SDMFanTimerMode timerMode; + + /** + * Timestamp, in RFC 3339 format, at which timer mode will turn to OFF. + */ + public ZonedDateTime timerTimeout; + } + + /** + * This trait belongs to any device that has a sensor to measure humidity. + */ + public static class SDMHumidityTrait extends SDMDeviceTrait { + /** + * Percent humidity, measured at the device. + */ + public BigDecimal ambientHumidityPercent; + } + + public enum SDMHvacStatus { + OFF, + HEATING, + COOLING + } + + public static class SDMResolution { + /** + * Maximum image resolution width. + */ + public int width; + + /** + * Maximum image resolution height. + */ + public int height; + } + + /** + * This trait belongs to any room for room-related information. + */ + public static class SDMRoomInfoTrait extends SDMStructureTrait { + /** + * Custom name of the room. Corresponds to the name in the Google Home App. + */ + public String customName; + } + + /** + * This trait belongs to any structure for structure-related information. + */ + public static class SDMStructureInfoTrait extends SDMStructureTrait { + /** + * Custom name of the structure. Corresponds to the name in the Google Home App. + */ + public String customName; + } + + public static class SDMStructureTrait extends SDMTrait { + } + + public enum SDMTemperatureScale { + CELSIUS, + FAHRENHEIT; + } + + /** + * This trait belongs to any device that has a sensor to measure temperature. + */ + public static class SDMTemperatureTrait extends SDMDeviceTrait { + /** + * Temperature in degrees Celsius, measured at the device. + */ + public BigDecimal ambientTemperatureCelsius; + } + + /** + * This trait belongs to device types of THERMOSTAT that support ECO modes. + */ + public static class SDMThermostatEcoTrait extends SDMThermostatTrait { + /** + * List of supported Eco modes. + */ + public List availableModes; + + /** + * The current Eco mode of the thermostat. + */ + public SDMThermostatEcoMode mode; + + /** + * Lowest temperature in Celsius at which the thermostat begins heating in Eco mode. + */ + public BigDecimal heatCelsius; + + /** + * Highest temperature in Celsius at which the thermostat begins cooling in Eco mode. + */ + public BigDecimal coolCelsius; + } + + /** + * This trait belongs to device types of THERMOSTAT that can report HVAC details. + */ + public static class SDMThermostatHvacTrait extends SDMThermostatTrait { + /** + * Current HVAC status of the thermostat. + */ + public SDMHvacStatus status; + } + + public enum SDMThermostatMode { + HEAT, + COOL, + HEATCOOL, + OFF + } + + /** + * This trait belongs to device types of THERMOSTAT that support different thermostat modes. + */ + public static class SDMThermostatModeTrait extends SDMThermostatTrait { + /** + * List of supported thermostat modes. + */ + public List availableModes; + + /** + * The current thermostat mode. + */ + public SDMThermostatMode mode; + } + + /** + * This trait belongs to device types of THERMOSTAT that support setting target temperature and temperature range. + */ + public static class SDMThermostatTemperatureSetpointTrait extends SDMThermostatTrait { + /** + * Target temperature in Celsius for thermostat HEAT and HEATCOOL modes. + */ + public BigDecimal heatCelsius; + + /** + * Target temperature in Celsius for thermostat COOL and HEATCOOL modes. + */ + public BigDecimal coolCelsius; + } + + public static class SDMThermostatTrait extends SDMTrait { + } + + public enum SDMFanTimerMode { + ON, + OFF + } + + public static class SDMTrait { + } + + @SerializedName("sdm.devices.traits.CameraEventImage") + public SDMCameraEventImageTrait cameraEventImage; + + @SerializedName("sdm.devices.traits.CameraImage") + public SDMCameraImageTrait cameraImage; + + @SerializedName("sdm.devices.traits.CameraLiveStream") + public SDMCameraLiveStreamTrait cameraLiveStream; + + @SerializedName("sdm.devices.traits.CameraMotion") + public SDMCameraMotionTrait cameraMotion; + + @SerializedName("sdm.devices.traits.CameraPerson") + public SDMCameraPersonTrait cameraPerson; + + @SerializedName("sdm.devices.traits.CameraSound") + public SDMCameraSoundTrait cameraSound; + + @SerializedName("sdm.devices.traits.Connectivity") + public SDMConnectivityTrait connectivity; + + @SerializedName("sdm.devices.traits.DoorbellChime") + public SDMDoorbellChimeTrait doorbellChime; + + @SerializedName("sdm.devices.traits.Fan") + public SDMFanTrait fan; + + @SerializedName("sdm.devices.traits.Humidity") + public SDMHumidityTrait humidity; + + @SerializedName("sdm.devices.traits.Info") + public SDMDeviceInfoTrait deviceInfo; + + @SerializedName("sdm.devices.traits.Settings") + public SDMDeviceSettingsTrait deviceSettings; + + @SerializedName("sdm.devices.traits.Temperature") + public SDMTemperatureTrait temperature; + + @SerializedName("sdm.devices.traits.ThermostatEco") + public SDMThermostatEcoTrait thermostatEco; + + @SerializedName("sdm.devices.traits.ThermostatHvac") + public SDMThermostatHvacTrait thermostatHvac; + + @SerializedName("sdm.devices.traits.ThermostatMode") + public SDMThermostatModeTrait thermostatMode; + + @SerializedName("sdm.devices.traits.ThermostatTemperatureSetpoint") + public SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint; + + @SerializedName("sdm.structures.traits.Info") + public SDMStructureInfoTrait structureInfo; + + @SerializedName("sdm.structures.traits.RoomInfo") + public SDMRoomInfoTrait roomInfo; + + public Stream traitStream() { + return Stream.of(cameraEventImage, cameraImage, cameraLiveStream, cameraMotion, cameraPerson, cameraSound, + connectivity, doorbellChime, fan, humidity, deviceInfo, deviceSettings, temperature, thermostatEco, + thermostatHvac, thermostatMode, thermostatTemperatureSetpoint, structureInfo, roomInfo) + .filter(Objects::nonNull); + } + + public List traitList() { + return traitStream().collect(Collectors.toList()); + } + + public Set traitSet() { + return traitStream().collect(Collectors.toSet()); + } + + public void updateTraits(SDMTraits other) { + if (other.cameraEventImage != null) { + cameraEventImage = other.cameraEventImage; + } + if (other.cameraImage != null) { + cameraImage = other.cameraImage; + } + if (other.cameraLiveStream != null) { + cameraLiveStream = other.cameraLiveStream; + } + if (other.cameraMotion != null) { + cameraMotion = other.cameraMotion; + } + if (other.cameraPerson != null) { + cameraPerson = other.cameraPerson; + } + if (other.cameraSound != null) { + cameraSound = other.cameraSound; + } + if (other.connectivity != null) { + connectivity = other.connectivity; + } + if (other.doorbellChime != null) { + doorbellChime = other.doorbellChime; + } + if (other.fan != null) { + fan = other.fan; + } + if (other.humidity != null) { + humidity = other.humidity; + } + if (other.deviceInfo != null) { + deviceInfo = other.deviceInfo; + } + if (other.deviceSettings != null) { + deviceSettings = other.deviceSettings; + } + if (other.temperature != null) { + temperature = other.temperature; + } + if (other.thermostatEco != null) { + thermostatEco = other.thermostatEco; + } + if (other.thermostatHvac != null) { + thermostatHvac = other.thermostatHvac; + } + if (other.thermostatMode != null) { + thermostatMode = other.thermostatMode; + } + if (other.thermostatTemperatureSetpoint != null) { + thermostatTemperatureSetpoint = other.thermostatTemperatureSetpoint; + } + if (other.structureInfo != null) { + structureInfo = other.structureInfo; + } + if (other.roomInfo != null) { + roomInfo = other.roomInfo; + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingPubSubDataException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingPubSubDataException.java new file mode 100644 index 0000000000000..79878aeb7e28b --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingPubSubDataException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An error occurred while sending data to the Pub/Sub REST API. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class FailedSendingPubSubDataException extends Exception { + + private static final long serialVersionUID = 8615651337708366903L; + + public FailedSendingPubSubDataException(String message) { + super(message); + } + + public FailedSendingPubSubDataException(String message, Throwable cause) { + super(message, cause); + } + + public FailedSendingPubSubDataException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingSDMDataException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingSDMDataException.java new file mode 100644 index 0000000000000..57c068fb0db00 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/FailedSendingSDMDataException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An error occurred while sending data to the SDM REST API. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class FailedSendingSDMDataException extends Exception { + + private static final long serialVersionUID = 5377279669017810297L; + + public FailedSendingSDMDataException(String message) { + super(message); + } + + public FailedSendingSDMDataException(String message, Throwable cause) { + super(message, cause); + } + + public FailedSendingSDMDataException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAccessTokenException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAccessTokenException.java new file mode 100644 index 0000000000000..069db817875bd --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAccessTokenException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The OAuth 2.0 access token used with the Pub/Sub REST API is invalid and could not be refreshed. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class InvalidPubSubAccessTokenException extends Exception { + + private static final long serialVersionUID = -2065751473657555846L; + + public InvalidPubSubAccessTokenException(Exception cause) { + super(cause); + } + + public InvalidPubSubAccessTokenException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidPubSubAccessTokenException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAuthorizationCodeException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAuthorizationCodeException.java new file mode 100644 index 0000000000000..bf5352c0f2560 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidPubSubAuthorizationCodeException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An authorization code is invalid and cannot be used to obtain the OAuth 2.0 tokens used with the Pub/Sub REST API. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class InvalidPubSubAuthorizationCodeException extends Exception { + + private static final long serialVersionUID = 8422005071870179414L; + + public InvalidPubSubAuthorizationCodeException(Exception cause) { + super(cause); + } + + public InvalidPubSubAuthorizationCodeException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidPubSubAuthorizationCodeException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAccessTokenException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAccessTokenException.java new file mode 100644 index 0000000000000..8faab61c77049 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAccessTokenException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The OAuth 2.0 access token used with the SDM REST API is invalid and could not be refreshed. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class InvalidSDMAccessTokenException extends Exception { + + private static final long serialVersionUID = 6149230876422099759L; + + public InvalidSDMAccessTokenException(Exception cause) { + super(cause); + } + + public InvalidSDMAccessTokenException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidSDMAccessTokenException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAuthorizationCodeException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAuthorizationCodeException.java new file mode 100644 index 0000000000000..2e44e2d2a66c0 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/exception/InvalidSDMAuthorizationCodeException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An authorization code is invalid and cannot be used to obtain the OAuth 2.0 tokens used with the SDM API. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class InvalidSDMAuthorizationCodeException extends Exception { + + private static final long serialVersionUID = -8900246112957957403L; + + public InvalidSDMAuthorizationCodeException(Exception cause) { + super(cause); + } + + public InvalidSDMAuthorizationCodeException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidSDMAuthorizationCodeException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMAccountHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMAccountHandler.java new file mode 100644 index 0000000000000..3cb9c1db2df4e --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMAccountHandler.java @@ -0,0 +1,332 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.handler; + +import static java.util.function.Predicate.not; +import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON; + +import java.io.IOException; +import java.time.Duration; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.api.PubSubAPI; +import org.openhab.binding.nest.internal.sdm.api.SDMAPI; +import org.openhab.binding.nest.internal.sdm.config.SDMAccountConfiguration; +import org.openhab.binding.nest.internal.sdm.discovery.SDMDiscoveryService; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingPubSubDataException; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAccessTokenException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAuthorizationCodeException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAuthorizationCodeException; +import org.openhab.binding.nest.internal.sdm.listener.PubSubSubscriptionListener; +import org.openhab.binding.nest.internal.sdm.listener.SDMAPIRequestListener; +import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMAccountHandler} provides the {@link SDMAPI} instance used by the device handlers. + * The {@link SDMAPI} is used by device handlers for periodically refreshing device data and sending device commands. + * When Pub/Sub is properly configured, the account handler also sends received {@link SDMEvent}s from the + * {@link PubSubAPI} to the subscribed {@link SDMEventListener}s. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMAccountHandler extends BaseBridgeHandler { + + private static final String PUBSUB_TOPIC_NAME_PREFIX = "projects/sdm-prod/topics/enterprise-"; + + private final Logger logger = LoggerFactory.getLogger(SDMAccountHandler.class); + + private HttpClientFactory httpClientFactory; + private OAuthFactory oAuthFactory; + + private @NonNullByDefault({}) SDMAccountConfiguration config; + private @Nullable Future initializeFuture; + + private @Nullable PubSubAPI pubSubAPI; + private @Nullable Exception pubSubException; + + private @Nullable SDMAPI sdmAPI; + private @Nullable Exception sdmException; + private @Nullable Future sdmCheckFuture; + private final Duration sdmCheckDelay = Duration.ofMinutes(1); + + private final Map listeners = new ConcurrentHashMap<>(); + + private final SDMAPIRequestListener requestListener = new SDMAPIRequestListener() { + @Override + public void onError(Exception exception) { + sdmException = exception; + logger.debug("SDM exception occurred"); + updateThingStatus(); + + Future future = sdmCheckFuture; + if (future == null || future.isDone()) { + sdmCheckFuture = scheduler.scheduleWithFixedDelay(() -> { + SDMAPI localSDMAPI = sdmAPI; + if (localSDMAPI != null) { + try { + logger.debug("Checking SDM API"); + localSDMAPI.listDevices(); + } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) { + logger.debug("SDM API check failed"); + } + } + }, sdmCheckDelay.toNanos(), sdmCheckDelay.toNanos(), TimeUnit.NANOSECONDS); + logger.debug("Scheduled SDM API check job"); + } + } + + @Override + public void onSuccess() { + if (sdmException != null) { + sdmException = null; + logger.debug("SDM exception cleared"); + updateThingStatus(); + } + + Future future = sdmCheckFuture; + if (future != null) { + future.cancel(true); + sdmCheckFuture = null; + logger.debug("Cancelled SDM API check job"); + } + } + }; + + private final PubSubSubscriptionListener subscriptionListener = new PubSubSubscriptionListener() { + @Override + public void onError(Exception exception) { + pubSubException = exception; + logger.debug("Pub/Sub exception occurred"); + updateThingStatus(); + } + + @Override + public void onMessage(PubSubMessage message) { + if (pubSubException != null) { + pubSubException = null; + logger.debug("Pub/Sub exception cleared"); + updateThingStatus(); + } + handlePubSubMessage(message); + } + + @Override + public void onNoNewMessages() { + if (pubSubException != null) { + pubSubException = null; + logger.debug("Pub/Sub exception cleared"); + updateThingStatus(); + } + } + }; + + public SDMAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory) { + super(bridge); + this.httpClientFactory = httpClientFactory; + this.oAuthFactory = oAuthFactory; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + config = getConfigAs(SDMAccountConfiguration.class); + + updateStatus(ThingStatus.UNKNOWN); + + initializeFuture = scheduler.submit(() -> { + sdmAPI = initializeSDMAPI(); + if (config.usePubSub()) { + pubSubAPI = initializePubSubAPI(); + } + updateThingStatus(); + }); + } + + private @Nullable SDMAPI initializeSDMAPI() { + SDMAPI sdmAPI = new SDMAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(), + config.sdmProjectId, config.sdmClientId, config.sdmClientSecret); + sdmException = null; + + try { + if (!config.sdmAuthorizationCode.isBlank()) { + sdmAPI.authorizeClient(config.sdmAuthorizationCode); + + Configuration configuration = editConfiguration(); + configuration.put(SDMAccountConfiguration.SDM_AUTHORIZATION_CODE, ""); + updateConfiguration(configuration); + } + + sdmAPI.checkAccessTokenValidity(); + sdmAPI.addRequestListener(requestListener); + + return sdmAPI; + } catch (InvalidSDMAccessTokenException | InvalidSDMAuthorizationCodeException | IOException e) { + sdmException = e; + return null; + } + } + + private @Nullable PubSubAPI initializePubSubAPI() { + PubSubAPI pubSubAPI = new PubSubAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(), + config.pubsubProjectId, config.pubsubClientId, config.pubsubClientSecret); + pubSubException = null; + + try { + if (!config.pubsubAuthorizationCode.isBlank()) { + pubSubAPI.authorizeClient(config.pubsubAuthorizationCode); + + Configuration configuration = editConfiguration(); + configuration.put(SDMAccountConfiguration.PUBSUB_AUTHORIZATION_CODE, ""); + updateConfiguration(configuration); + } + + pubSubAPI.checkAccessTokenValidity(); + pubSubAPI.createSubscription(config.pubsubSubscriptionId, PUBSUB_TOPIC_NAME_PREFIX + config.sdmProjectId); + pubSubAPI.addSubscriptionListener(config.pubsubSubscriptionId, subscriptionListener); + + return pubSubAPI; + } catch (FailedSendingPubSubDataException | InvalidPubSubAccessTokenException + | InvalidPubSubAuthorizationCodeException | IOException e) { + pubSubException = e; + return null; + } + } + + @Override + public void dispose() { + Future localFuture = initializeFuture; + if (localFuture != null) { + localFuture.cancel(true); + initializeFuture = null; + } + + localFuture = sdmCheckFuture; + if (localFuture != null) { + localFuture.cancel(true); + sdmCheckFuture = null; + } + + PubSubAPI localPubSubAPI = pubSubAPI; + if (localPubSubAPI != null) { + localPubSubAPI.dispose(); + pubSubAPI = null; + } + + SDMAPI localSDMAPI = sdmAPI; + if (localSDMAPI != null) { + localSDMAPI.dispose(); + sdmAPI = null; + } + } + + @Override + public Collection> getServices() { + return List.of(SDMDiscoveryService.class); + } + + public void addThingDataListener(String deviceId, SDMEventListener listener) { + listeners.put(deviceId, listener); + } + + public void removeThingDataListener(String deviceId, SDMEventListener listener) { + listeners.remove(deviceId, listener); + } + + public @Nullable SDMAPI getAPI() { + return sdmAPI; + } + + private void handlePubSubMessage(PubSubMessage message) { + String messageId = message.messageId; + String json = new String(Base64.getDecoder().decode(message.data)); + + logger.debug("Handling messageId={} with content:", messageId); + logger.debug("{}", json); + + SDMEvent event = GSON.fromJson(json, SDMEvent.class); + if (event == null) { + logger.debug("Ignoring messageId={} (empty)", messageId); + return; + } + + SDMResourceUpdate resourceUpdate = event.resourceUpdate; + if (resourceUpdate == null) { + logger.debug("Ignoring messageId={} (no resource update)", messageId); + return; + } + + String deviceId = resourceUpdate.name.deviceId; + SDMEventListener listener = listeners.get(deviceId); + if (listener != null) { + logger.debug("Sending messageId={} to listener with deviceId={}", messageId, deviceId); + listener.onEvent(event); + } else { + logger.debug("No listener for messageId={} with deviceId={}", messageId, deviceId); + } + } + + private void updateThingStatus() { + Exception e = sdmException != null ? sdmException : pubSubException; + if (e != null) { + if (e instanceof InvalidSDMAccessTokenException || e instanceof InvalidSDMAuthorizationCodeException + || e instanceof InvalidPubSubAccessTokenException + || e instanceof InvalidPubSubAuthorizationCodeException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } else { + Throwable cause = e.getCause(); + String description = Stream + .of(Objects.requireNonNullElse(e.getMessage(), ""), + cause == null ? "" : Objects.requireNonNullElse(cause.getMessage(), "")) + .filter(not(String::isBlank)) // + .collect(Collectors.joining(": ")); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description); + } + } else { + String description = config.usePubSub() ? "Using periodic refresh and Pub/Sub" : "Using periodic refresh"; + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, description); + } + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMBaseHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMBaseHandler.java new file mode 100644 index 0000000000000..8879e4302111b --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMBaseHandler.java @@ -0,0 +1,319 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.handler; + +import static org.openhab.core.thing.ThingStatus.*; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.SDMBindingConstants; +import org.openhab.binding.nest.internal.sdm.api.SDMAPI; +import org.openhab.binding.nest.internal.sdm.config.SDMDeviceConfiguration; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMDevice; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate; +import org.openhab.binding.nest.internal.sdm.dto.SDMIdentifiable; +import org.openhab.binding.nest.internal.sdm.dto.SDMParentRelation; +import org.openhab.binding.nest.internal.sdm.dto.SDMResourceName.SDMResourceNameType; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraImageTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraLiveStreamTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityStatus; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceInfoTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMResolution; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMBaseHandler} provides the common functionality of all SDM device thing handlers. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public abstract class SDMBaseHandler extends BaseThingHandler implements SDMIdentifiable, SDMEventListener { + + private final Logger logger = LoggerFactory.getLogger(SDMBaseHandler.class); + + protected @NonNullByDefault({}) SDMDeviceConfiguration config; + protected SDMDevice device = new SDMDevice(); + protected String deviceId = ""; + protected @Nullable ZonedDateTime lastRefreshDateTime; + protected @Nullable ScheduledFuture refreshJob; + protected final TimeZoneProvider timeZoneProvider; + + public SDMBaseHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + super(thing); + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + updateBridgeStatus(); + } + + /** + * Updates the thing state based on that of the bridge. + */ + protected void updateBridgeStatus() { + Bridge bridge = getBridge(); + ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null; + if (bridge == null) { + disableRefresh(); + updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured"); + } else if (bridgeStatus == ONLINE && thing.getStatus() != ONLINE) { + enableRefresh(); + } else if (bridgeStatus == OFFLINE) { + disableRefresh(); + updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } else if (bridgeStatus == UNKNOWN) { + disableRefresh(); + updateStatus(UNKNOWN); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + delayedRefresh(); + } + } + + @Override + public void initialize() { + logger.debug("Initializing handler for {}", thing.getUID()); + config = getConfigAs(SDMDeviceConfiguration.class); + deviceId = config.deviceId; + updateStatus(ThingStatus.UNKNOWN); + updateBridgeStatus(); + } + + @Override + public void dispose() { + disableRefresh(); + } + + @Override + public String getId() { + return deviceId; + } + + protected @Nullable SDMAccountHandler getAccountHandler() { + Bridge bridge = getBridge(); + return bridge != null ? (SDMAccountHandler) bridge.getHandler() : null; + } + + protected @Nullable SDMAPI getAPI() { + SDMAccountHandler accountHandler = getAccountHandler(); + return accountHandler != null ? accountHandler.getAPI() : null; + } + + protected @Nullable SDMDevice getDeviceInfo() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + SDMAPI api = getAPI(); + return api == null ? null : api.getDevice(deviceId); + } + + protected @Nullable T executeDeviceCommand(SDMCommandRequest request) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + SDMAPI api = getAPI(); + return api == null ? null : api.executeDeviceCommand(deviceId, request); + } + + protected @Nullable SDMTraits getTraitsForUpdate(SDMEvent event) { + SDMResourceUpdate resourceUpdate = event.resourceUpdate; + if (resourceUpdate == null) { + return null; + } + + SDMTraits traits = resourceUpdate.traits; + if (traits == null) { + return null; + } + + ZonedDateTime localRefreshDateTime = lastRefreshDateTime; + if (localRefreshDateTime == null || event.timestamp.isBefore(localRefreshDateTime)) { + return null; + } + + return traits; + } + + @Override + public void onEvent(SDMEvent event) { + SDMTraits traits = getTraitsForUpdate(event); + if (traits != null) { + logger.debug("Updating traits using resource update traits in event"); + device.traits.updateTraits(traits); + } + } + + protected void refreshDevice() { + try { + SDMDevice localDevice = getDeviceInfo(); + if (localDevice == null) { + logger.debug("Cannot refresh device (empty response or handler has no bridge)"); + return; + } + + this.device = localDevice; + this.lastRefreshDateTime = ZonedDateTime.now(); + + Map properties = editProperties(); + properties.putAll(getDeviceProperties(localDevice)); + updateProperties(properties); + + updateStateWithTraits(localDevice.traits); + } catch (InvalidSDMAccessTokenException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (FailedSendingSDMDataException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + protected void updateStateWithTraits(SDMTraits traits) { + SDMConnectivityTrait connectivity = traits.connectivity; + if (connectivity == null && device.traits.connectivity != null) { + logger.debug("Skipping partial update for device with connectivity trait"); + return; + } + + ThingStatus thingStatus = connectivity == null || connectivity.status == null + || connectivity.status == SDMConnectivityStatus.ONLINE ? ThingStatus.ONLINE : ThingStatus.OFFLINE; + + if (thing.getStatus() != thingStatus) { + updateStatus(thingStatus); + } + } + + protected void enableRefresh() { + scheduleRefreshJob(); + SDMAccountHandler handler = getAccountHandler(); + if (handler != null) { + handler.addThingDataListener(getId(), this); + } + } + + protected void disableRefresh() { + cancelRefreshJob(); + SDMAccountHandler handler = getAccountHandler(); + if (handler != null) { + handler.removeThingDataListener(getId(), this); + } + } + + protected void cancelRefreshJob() { + ScheduledFuture localRefreshJob = refreshJob; + if (localRefreshJob != null && !localRefreshJob.isCancelled()) { + localRefreshJob.cancel(true); + } + } + + protected void scheduleRefreshJob() { + ScheduledFuture localRefreshJob = refreshJob; + if (localRefreshJob == null || localRefreshJob.isCancelled()) { + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 0, config.refreshInterval, + TimeUnit.SECONDS); + } + } + + protected void delayedRefresh() { + cancelRefreshJob(); + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 3, config.refreshInterval, TimeUnit.SECONDS); + } + + public static Map getDeviceProperties(SDMDevice device) { + Map properties = new HashMap<>(); + + SDMTraits traits = device.traits; + + SDMDeviceInfoTrait deviceInfo = traits.deviceInfo; + if (deviceInfo != null && !deviceInfo.customName.isBlank()) { + properties.put(SDMBindingConstants.PROPERTY_CUSTOM_NAME, deviceInfo.customName); + } + + List parentRelations = device.parentRelations; + for (SDMParentRelation parentRelation : parentRelations) { + if (parentRelation.parent.type == SDMResourceNameType.ROOM && !parentRelation.displayName.isBlank()) { + properties.put(SDMBindingConstants.PROPERTY_ROOM, parentRelation.displayName); + break; + } + } + + SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings; + if (deviceSettings != null) { + properties.put(SDMBindingConstants.PROPERTY_TEMPERATURE_SCALE, deviceSettings.temperatureScale.name()); + } + + SDMCameraImageTrait cameraImage = traits.cameraImage; + if (cameraImage != null) { + SDMResolution resolution = cameraImage.maxImageResolution; + properties.put(SDMBindingConstants.PROPERTY_MAX_IMAGE_RESOLUTION, + String.format("%sx%s", resolution.width, resolution.height)); + } + + SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream; + if (cameraLiveStream != null) { + List audioCodecs = cameraLiveStream.audioCodecs; + if (audioCodecs != null) { + properties.put(SDMBindingConstants.PROPERTY_AUDIO_CODECS, + audioCodecs.stream().collect(Collectors.joining(", "))); + } + + SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution; + if (maxVideoResolution != null) { + SDMResolution resolution = maxVideoResolution; + properties.put(SDMBindingConstants.PROPERTY_MAX_VIDEO_RESOLUTION, + String.format("%sx%s", resolution.width, resolution.height)); + } + + List supportedProtocols = cameraLiveStream.supportedProtocols; + if (supportedProtocols != null) { + properties.put(SDMBindingConstants.PROPERTY_SUPPORTED_PROTOCOLS, + supportedProtocols.stream().collect(Collectors.joining(", "))); + } + + List videoCodecs = cameraLiveStream.videoCodecs; + if (videoCodecs != null) { + properties.put(SDMBindingConstants.PROPERTY_VIDEO_CODECS, + videoCodecs.stream().collect(Collectors.joining(", "))); + } + } + + return properties; + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMCameraHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMCameraHandler.java new file mode 100644 index 0000000000000..758b77da61437 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMCameraHandler.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.handler; + +import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*; +import static org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest.EVENT_IMAGE_VALIDITY; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.SDMBindingConstants; +import org.openhab.binding.nest.internal.sdm.api.SDMAPI; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResults; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResults; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMDeviceEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdateEvents; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMCameraHandler} handles state updates of SDM devices with a camera. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMCameraHandler extends SDMBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(SDMCameraHandler.class); + + private @Nullable ZonedDateTime lastChimeEventTimestamp; + private @Nullable ZonedDateTime lastMotionEventTimestamp; + private @Nullable ZonedDateTime lastPersonEventTimestamp; + private @Nullable ZonedDateTime lastSoundEventTimestamp; + + public SDMCameraHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + super(thing, timeZoneProvider); + } + + private void updateLiveStreamChannels() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + boolean channelLinked = Stream.of(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP, + CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, CHANNEL_LIVE_STREAM_URL).anyMatch(this::isLinked); + if (!channelLinked) { + logger.debug("Not updating live stream channels (channels are not linked)"); + return; + } + + logger.debug("Updating live stream channels"); + + SDMGenerateCameraRtspStreamResponse response = executeDeviceCommand(new SDMGenerateCameraRtspStreamRequest()); + if (response == null) { + logger.debug("Cannot update live stream channels (empty response)"); + return; + } + + SDMGenerateCameraRtspStreamResults results = response.results; + if (results != null) { + updateState(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, new StringType(results.streamToken)); + updateState(CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP, + new DateTimeType(results.expiresAt.withZoneSameInstant(timeZoneProvider.getTimeZone()))); + updateState(CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, new StringType(results.streamExtensionToken)); + updateState(CHANNEL_LIVE_STREAM_URL, new StringType(results.streamUrls.rtspUrl)); + } + } + + @Override + public void onEvent(SDMEvent event) { + super.onEvent(event); + + SDMResourceUpdate resourceUpdate = event.resourceUpdate; + if (resourceUpdate == null) { + logger.debug("Skipping event without resource update"); + return; + } + + SDMResourceUpdateEvents events = resourceUpdate.events; + if (events == null) { + logger.debug("Skipping resource update without events"); + return; + } + + try { + SDMDeviceEvent deviceEvent = events.cameraMotionEvent; + if (deviceEvent != null) { + lastMotionEventTimestamp = updateImageChannelsForEvent(CHANNEL_MOTION_EVENT_TIMESTAMP, + CHANNEL_MOTION_EVENT_IMAGE, lastMotionEventTimestamp, event.timestamp, deviceEvent); + } + + deviceEvent = events.cameraPersonEvent; + if (deviceEvent != null) { + lastPersonEventTimestamp = updateImageChannelsForEvent(CHANNEL_PERSON_EVENT_TIMESTAMP, + CHANNEL_PERSON_EVENT_IMAGE, lastPersonEventTimestamp, event.timestamp, deviceEvent); + } + + deviceEvent = events.cameraSoundEvent; + if (deviceEvent != null) { + lastSoundEventTimestamp = updateImageChannelsForEvent(CHANNEL_SOUND_EVENT_TIMESTAMP, + CHANNEL_SOUND_EVENT_IMAGE, lastSoundEventTimestamp, event.timestamp, deviceEvent); + } + + deviceEvent = events.doorbellChimeEvent; + if (deviceEvent != null) { + lastChimeEventTimestamp = updateImageChannelsForEvent(CHANNEL_CHIME_EVENT_TIMESTAMP, + CHANNEL_CHIME_EVENT_IMAGE, lastChimeEventTimestamp, event.timestamp, deviceEvent); + } + } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) { + logger.warn("Handling SDM event failed for {}", thing.getUID(), e); + } + } + + private @Nullable ZonedDateTime updateImageChannelsForEvent(String timeChannelName, String imageChannelName, + @Nullable ZonedDateTime lastEventTimestamp, ZonedDateTime eventTimestamp, SDMDeviceEvent event) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + boolean newerEvent = lastEventTimestamp == null || lastEventTimestamp.isBefore(eventTimestamp); + if (!newerEvent) { + logger.debug("Skipping {} channel update (more recent event already occurred)", imageChannelName); + return lastEventTimestamp; + } + + if (!isLinked(imageChannelName)) { + logger.debug("Not downloading image for {} channel update (channel is not linked)", imageChannelName); + } else if (Duration.between(eventTimestamp, ZonedDateTime.now()).compareTo(EVENT_IMAGE_VALIDITY) > 0) { + logger.debug("Cannot download image for {} channel update (event image has expired)", imageChannelName); + updateState(timeChannelName, UnDefType.NULL); + } else { + BigDecimal imageWidth = null; + BigDecimal imageHeight = null; + + Channel channel = getThing().getChannel(imageChannelName); + if (channel != null) { + Configuration configuration = channel.getConfiguration(); + imageWidth = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_WIDTH); + imageHeight = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_HEIGHT); + } + + updateState(imageChannelName, getCameraImage(event.eventId, imageWidth, imageHeight)); + } + + updateState(timeChannelName, + new DateTimeType(eventTimestamp.withZoneSameInstant(timeZoneProvider.getTimeZone()))); + + logger.debug("Updated {} channel and {} with image of event at {}", imageChannelName, timeChannelName, + eventTimestamp); + + updateLiveStreamChannels(); + + return eventTimestamp; + } + + private State getCameraImage(String eventId, @Nullable BigDecimal imageWidth, @Nullable BigDecimal imageHeight) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + SDMGenerateCameraImageResponse response = executeDeviceCommand(new SDMGenerateCameraImageRequest(eventId)); + if (response == null) { + logger.debug("Cannot get image for camera event (empty response)"); + return UnDefType.NULL; + } + + SDMGenerateCameraImageResults results = response.results; + if (results == null) { + logger.debug("Cannot get image for camera event (no results)"); + return UnDefType.NULL; + } + + SDMAPI api = getAPI(); + if (api == null) { + logger.debug("Cannot get image for camera event (handler has no bridge)"); + return UnDefType.NULL; + } + + byte[] imageBytes = api.getCameraImage(results.url, results.token, imageWidth, imageHeight); + return new RawType(imageBytes, "image/jpeg"); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMThermostatHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMThermostatHandler.java new file mode 100644 index 0000000000000..69fea783bbfb3 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMThermostatHandler.java @@ -0,0 +1,357 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.handler; + +import static org.openhab.binding.nest.internal.sdm.SDMBindingConstants.*; +import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT; +import static org.openhab.core.library.unit.SIUnits.CELSIUS; +import static org.openhab.core.library.unit.Units.PERCENT; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.ZonedDateTime; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.sdm.SDMBindingConstants; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetFanTimerRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatCoolSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatEcoModeRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatHeatSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatModeRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatRangeSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHumidityTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatHvacTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatModeTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatTemperatureSetpointTrait; +import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException; +import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDMThermostatHandler} handles state updates and commands for SDM thermostat devices. + * + * @author Brian Higginbotham - Initial contribution + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMThermostatHandler extends SDMBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(SDMThermostatHandler.class); + + public SDMThermostatHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + super(thing, timeZoneProvider); + } + + @SuppressWarnings("unchecked") + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + if (command instanceof RefreshType) { + delayedRefresh(); + } else if (CHANNEL_CURRENT_ECO_MODE.equals(channelUID.getId())) { + if (command instanceof StringType) { + SDMThermostatEcoMode mode = SDMThermostatEcoMode.valueOf(command.toString()); + executeDeviceCommand(new SDMSetThermostatEcoModeRequest(mode)); + delayedRefresh(); + } + } else if (CHANNEL_CURRENT_MODE.equals(channelUID.getId())) { + if (command instanceof StringType) { + SDMThermostatMode mode = SDMThermostatMode.valueOf(command.toString()); + executeDeviceCommand(new SDMSetThermostatModeRequest(mode)); + delayedRefresh(); + } + } else if (CHANNEL_FAN_TIMER_MODE.equals(channelUID.getId())) { + if (command instanceof OnOffType) { + if ((OnOffType) command == OnOffType.ON) { + executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, getFanTimerDuration())); + } else { + executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.OFF)); + } + delayedRefresh(); + } + } else if (CHANNEL_FAN_TIMER_TIMEOUT.equals(channelUID.getId())) { + if (command instanceof DateTimeType) { + Duration duration = Duration.between(ZonedDateTime.now(), + ((DateTimeType) command).getZonedDateTime()); + executeDeviceCommand(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, duration)); + delayedRefresh(); + } + } else if (CHANNEL_MAXIMUM_TEMPERATURE.equals(channelUID.getId())) { + if (command instanceof QuantityType) { + BigDecimal minTemperature = getMinTemperature(); + if (minTemperature != null) { + setTargetTemperature(new QuantityType<>(minTemperature, CELSIUS), + (QuantityType) command); + delayedRefresh(); + } + } + } else if (CHANNEL_MINIMUM_TEMPERATURE.equals(channelUID.getId())) { + if (command instanceof QuantityType) { + BigDecimal maxTemperature = getMaxTemperature(); + if (maxTemperature != null) { + setTargetTemperature((QuantityType) command, + new QuantityType<>(maxTemperature, CELSIUS)); + delayedRefresh(); + } + } + } else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) { + if (command instanceof QuantityType) { + setTargetTemperature((QuantityType) command); + delayedRefresh(); + } + } + } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) { + logger.debug("Exception while handling {} command for {}: {}", command, thing.getUID(), e.getMessage()); + } + } + + @Override + protected void updateStateWithTraits(SDMTraits traits) { + logger.debug("Refreshing channels for: {}", thing.getUID()); + super.updateStateWithTraits(traits); + + SDMHumidityTrait humidity = traits.humidity; + if (humidity != null) { + updateState(CHANNEL_AMBIENT_HUMIDITY, new QuantityType<>(humidity.ambientHumidityPercent, PERCENT)); + } + + SDMTemperatureTrait temperature = traits.temperature; + if (temperature != null) { + updateState(CHANNEL_AMBIENT_TEMPERATURE, temperatureToState(temperature.ambientTemperatureCelsius)); + } + + SDMThermostatModeTrait thermostatMode = traits.thermostatMode; + if (thermostatMode != null) { + updateState(CHANNEL_CURRENT_MODE, new StringType(thermostatMode.mode.name())); + } + + SDMThermostatEcoTrait thermostatEco = traits.thermostatEco; + if (thermostatEco != null) { + updateState(CHANNEL_CURRENT_ECO_MODE, new StringType(thermostatEco.mode.name())); + } + + SDMFanTrait fan = traits.fan; + if (fan != null) { + updateState(CHANNEL_FAN_TIMER_MODE, fan.timerMode == SDMFanTimerMode.ON ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_FAN_TIMER_TIMEOUT, fan.timerTimeout == null ? UnDefType.NULL + : new DateTimeType(fan.timerTimeout.withZoneSameInstant(timeZoneProvider.getTimeZone()))); + } + + SDMThermostatHvacTrait thermostatHvac = traits.thermostatHvac; + if (thermostatHvac != null) { + updateState(CHANNEL_HVAC_STATUS, new StringType(thermostatHvac.status.name())); + } + + BigDecimal maxTemperature = getMaxTemperature(); + if (maxTemperature != null) { + updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(getMaxTemperature())); + } + + BigDecimal minTemperature = getMinTemperature(); + if (minTemperature != null) { + updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(minTemperature)); + } + + BigDecimal targetTemperature = getTargetTemperature(); + if (targetTemperature != null) { + updateState(CHANNEL_TARGET_TEMPERATURE, temperatureToState(targetTemperature)); + } + } + + private Duration getFanTimerDuration() { + long seconds = 900; + + Channel channel = getThing().getChannel(SDMBindingConstants.CHANNEL_FAN_TIMER_MODE); + if (channel != null) { + Configuration configuration = channel.getConfiguration(); + Object fanTimerDuration = configuration.get(SDMBindingConstants.CONFIG_PROPERTY_FAN_TIMER_DURATION); + if (fanTimerDuration instanceof BigDecimal) { + seconds = ((BigDecimal) fanTimerDuration).longValue(); + } + } + + return Duration.ofSeconds(seconds); + } + + private @Nullable BigDecimal getMinTemperature() { + SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco; + if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) { + return thermostatEco.heatCelsius; + } + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint; + SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode; + if (thermostatMode != null && thermostatMode.mode == SDMThermostatMode.HEATCOOL) { + return thermostatTemperatureSetpoint.heatCelsius; + } + + return null; + } + + private @Nullable BigDecimal getMaxTemperature() { + SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco; + if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) { + return thermostatEco.coolCelsius; + } + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint; + SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode; + if (thermostatMode != null && thermostatMode.mode == SDMThermostatMode.HEATCOOL) { + return thermostatTemperatureSetpoint.coolCelsius; + } + + return null; + } + + private @Nullable BigDecimal getTargetTemperature() { + SDMThermostatEcoTrait thermostatEco = device.traits.thermostatEco; + if (thermostatEco != null && thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) { + return null; + } + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = device.traits.thermostatTemperatureSetpoint; + SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode; + if (thermostatMode != null) { + if (thermostatMode.mode == SDMThermostatMode.COOL) { + return thermostatTemperatureSetpoint.coolCelsius; + } + if (thermostatMode.mode == SDMThermostatMode.HEAT) { + return thermostatTemperatureSetpoint.heatCelsius; + } + } + + return null; + } + + @Override + public void onEvent(SDMEvent event) { + super.onEvent(event); + + SDMTraits traits = getTraitsForUpdate(event); + if (traits == null) { + return; + } + + updateStateWithTraits(traits); + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = traits.thermostatTemperatureSetpoint; + if (thermostatTemperatureSetpoint != null) { + BigDecimal coolCelsius = thermostatTemperatureSetpoint.coolCelsius; + BigDecimal heatCelsius = thermostatTemperatureSetpoint.heatCelsius; + if (coolCelsius != null && heatCelsius != null) { + updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(heatCelsius)); + updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(coolCelsius)); + } + } + + SDMThermostatEcoTrait thermostatEco = traits.thermostatEco; + if (thermostatEco != null) { + if (thermostatEco.mode == SDMThermostatEcoMode.MANUAL_ECO) { + updateState(CHANNEL_MINIMUM_TEMPERATURE, temperatureToState(thermostatEco.heatCelsius)); + updateState(CHANNEL_MAXIMUM_TEMPERATURE, temperatureToState(thermostatEco.coolCelsius)); + } + } + } + + private void setTargetTemperature(QuantityType value) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("setThermostatTargetTemperature value={}", value); + SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode; + if (thermostatMode.mode == SDMThermostatMode.COOL) { + executeDeviceCommand(new SDMSetThermostatCoolSetpointRequest(toCelsiusBigDecimal(value))); + } else if (thermostatMode.mode == SDMThermostatMode.HEAT) { + executeDeviceCommand(new SDMSetThermostatHeatSetpointRequest(toCelsiusBigDecimal(value))); + } else { + throw new IllegalStateException("INVALID use case for setThermostatTargetTemperature"); + } + } + + private void setTargetTemperature(QuantityType minValue, QuantityType maxValue) + throws FailedSendingSDMDataException, InvalidSDMAccessTokenException { + logger.debug("setThermostatTargetTemperature minValue={} maxValue={}", minValue, maxValue); + SDMThermostatModeTrait thermostatMode = device.traits.thermostatMode; + if (thermostatMode.mode == SDMThermostatMode.HEATCOOL) { + executeDeviceCommand(new SDMSetThermostatRangeSetpointRequest(toCelsiusBigDecimal(minValue), + toCelsiusBigDecimal(maxValue))); + } else { + throw new IllegalStateException("INVALID use case for setThermostatTargetTemperature"); + } + } + + protected State temperatureToState(@Nullable BigDecimal value) { + if (value == null) { + return UnDefType.NULL; + } + + QuantityType temperature = new QuantityType<>(value, CELSIUS); + + if (getDeviceTemperatureUnit() == FAHRENHEIT) { + QuantityType converted = temperature.toUnit(FAHRENHEIT); + return converted == null ? UnDefType.NULL : converted; + } + + return temperature; + } + + private Unit getDeviceTemperatureUnit() { + SDMDeviceSettingsTrait deviceSettings = device.traits.deviceSettings; + if (deviceSettings == null) { + return CELSIUS; + } + + switch (deviceSettings.temperatureScale) { + case CELSIUS: + return CELSIUS; + case FAHRENHEIT: + return FAHRENHEIT; + default: + return CELSIUS; + } + } + + private BigDecimal toCelsiusBigDecimal(QuantityType temperature) { + QuantityType celsiusTemperature = temperature.toUnit(CELSIUS); + if (celsiusTemperature == null) { + throw new IllegalArgumentException( + String.format("Temperature '%s' cannot be converted to Celsius unit", temperature)); + } + return celsiusTemperature.toBigDecimal(); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/PubSubSubscriptionListener.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/PubSubSubscriptionListener.java new file mode 100644 index 0000000000000..bc68cbb390116 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/PubSubSubscriptionListener.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.listener; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nest.internal.sdm.api.PubSubAPI; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage; + +/** + * Interface for listeners of {@link PubSubAPI} subscription events. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public interface PubSubSubscriptionListener { + + void onError(Exception exception); + + void onMessage(PubSubMessage message); + + void onNoNewMessages(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMAPIRequestListener.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMAPIRequestListener.java new file mode 100644 index 0000000000000..c022fcd398ab4 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMAPIRequestListener.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.listener; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nest.internal.sdm.api.SDMAPI; + +/** + * Interface for listeners that want to monitor if {@link SDMAPI} requests error or succeed. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public interface SDMAPIRequestListener { + + void onError(Exception exception); + + void onSuccess(); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMEventListener.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMEventListener.java new file mode 100644 index 0000000000000..e2dc84d6e02ba --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/listener/SDMEventListener.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.listener; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent; + +/** + * Interface for {@link SDMEvent} listeners. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public interface SDMEventListener { + + void onEvent(SDMEvent event); +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestBindingConstants.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNBindingConstants.java similarity index 91% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestBindingConstants.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNBindingConstants.java index 2c8f5dd35c2aa..68174aff8a2aa 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestBindingConstants.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNBindingConstants.java @@ -10,21 +10,22 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal; +package org.openhab.binding.nest.internal.wwn; import java.time.Duration; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; /** - * The {@link NestBindingConstants} class defines common constants, which are - * used across the whole binding. + * The {@link WWNBindingConstants} class defines common constants which are used for the WWN implementation in the + * binding. * * @author David Bennett - Initial contribution */ @NonNullByDefault -public class NestBindingConstants { +public class WWNBindingConstants { public static final String BINDING_ID = "nest"; @@ -56,11 +57,14 @@ public class NestBindingConstants { public static final int MIN_SECONDS_BETWEEN_API_CALLS = 60; // List of all Thing Type UIDs - public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); - public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "camera"); - public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke_detector"); - public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "account"); - public static final ThingTypeUID THING_TYPE_STRUCTURE = new ThingTypeUID(BINDING_ID, "structure"); + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "wwn_account"); + public static final ThingTypeUID THING_TYPE_CAMERA = new ThingTypeUID(BINDING_ID, "wwn_camera"); + public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "wwn_smoke_detector"); + public static final ThingTypeUID THING_TYPE_STRUCTURE = new ThingTypeUID(BINDING_ID, "wwn_structure"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wwn_thermostat"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_CAMERA, + THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE, THING_TYPE_THERMOSTAT); // List of all channel group prefixes public static final String CHANNEL_GROUP_CAMERA_PREFIX = "camera#"; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNThingHandlerFactory.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNThingHandlerFactory.java new file mode 100644 index 0000000000000..2e811810216c3 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNThingHandlerFactory.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.wwn; + +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler; +import org.openhab.binding.nest.internal.wwn.handler.WWNCameraHandler; +import org.openhab.binding.nest.internal.wwn.handler.WWNSmokeDetectorHandler; +import org.openhab.binding.nest.internal.wwn.handler.WWNStructureHandler; +import org.openhab.binding.nest.internal.wwn.handler.WWNThermostatHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.jaxrs.client.SseEventSourceFactory; + +/** + * The {@link WWNThingHandlerFactory} is responsible for creating WWN thing handlers. + * + * @author David Bennett - Initial contribution + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nest") +public class WWNThingHandlerFactory extends BaseThingHandlerFactory { + + private final ClientBuilder clientBuilder; + private final SseEventSourceFactory eventSourceFactory; + + @Activate + public WWNThingHandlerFactory(@Reference ClientBuilder clientBuilder, + @Reference SseEventSourceFactory eventSourceFactory) { + this.clientBuilder = clientBuilder; + this.eventSourceFactory = eventSourceFactory; + } + + /** + * The things this factory supports creating. + */ + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + /** + * Creates a handler for the specific thing. This also creates the discovery service when the bridge is created. + */ + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) { + return new WWNAccountHandler((Bridge) thing, clientBuilder, eventSourceFactory); + } else if (THING_TYPE_CAMERA.equals(thingTypeUID)) { + return new WWNCameraHandler(thing); + } else if (THING_TYPE_SMOKE_DETECTOR.equals(thingTypeUID)) { + return new WWNSmokeDetectorHandler(thing); + } else if (THING_TYPE_STRUCTURE.equals(thingTypeUID)) { + return new WWNStructureHandler(thing); + } else if (THING_TYPE_THERMOSTAT.equals(thingTypeUID)) { + return new WWNThermostatHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestUtils.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNUtils.java similarity index 87% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestUtils.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNUtils.java index 7ad59b4c54306..f1dfcae755529 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/NestUtils.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/WWNUtils.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal; +package org.openhab.binding.nest.internal.wwn; import java.io.Reader; @@ -21,17 +21,17 @@ import com.google.gson.GsonBuilder; /** - * Utility class for sharing utility methods between objects. + * Utility class for sharing WWN utility methods between objects. * * @author Wouter Born - Initial contribution */ @NonNullByDefault -public final class NestUtils { +public final class WWNUtils { private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - private NestUtils() { + private WWNUtils() { // hidden utility class constructor } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestBridgeConfiguration.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNAccountConfiguration.java similarity index 88% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestBridgeConfiguration.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNAccountConfiguration.java index 0de3318e0f236..edc36ddaacd74 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestBridgeConfiguration.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNAccountConfiguration.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.config; +package org.openhab.binding.nest.internal.wwn.config; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; /** - * The configuration for the Nest bridge, allowing it to talk to Nest. + * The configuration for the WWN account, allowing it to talk to Nest. * * @author David Bennett - Initial contribution */ @NonNullByDefault -public class NestBridgeConfiguration { +public class WWNAccountConfiguration { public static final String PRODUCT_ID = "productId"; /** Product ID from the Nest product page. */ public String productId = ""; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestDeviceConfiguration.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNDeviceConfiguration.java similarity index 85% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestDeviceConfiguration.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNDeviceConfiguration.java index 8815397e87999..7e38d57e7d134 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestDeviceConfiguration.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNDeviceConfiguration.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.config; +package org.openhab.binding.nest.internal.wwn.config; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The configuration for Nest devices. + * The configuration for WWN devices. * * @author Wouter Born - Initial contribution * @author Wouter Born - Add device configuration to allow file based configuration */ @NonNullByDefault -public class NestDeviceConfiguration { +public class WWNDeviceConfiguration { public static final String DEVICE_ID = "deviceId"; /** Device ID which can be retrieved with the Nest API. */ public String deviceId = ""; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestStructureConfiguration.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNStructureConfiguration.java similarity index 84% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestStructureConfiguration.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNStructureConfiguration.java index afbf67b4df5d0..e42ba230c1f97 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/config/NestStructureConfiguration.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/config/WWNStructureConfiguration.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.config; +package org.openhab.binding.nest.internal.wwn.config; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The configuration for structures. + * The configuration for WWN structures. * * @author Wouter Born - Initial contribution * @author Wouter Born - Add device configuration to allow file based configuration */ @NonNullByDefault -public class NestStructureConfiguration { +public class WWNStructureConfiguration { public static final String STRUCTURE_ID = "structureId"; /** Structure ID which can be retrieved with the Nest API. */ public String structureId = ""; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/discovery/WWNDiscoveryService.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/discovery/WWNDiscoveryService.java new file mode 100644 index 0000000000000..afbc51203d120 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/discovery/WWNDiscoveryService.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.wwn.discovery; + +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration; +import org.openhab.binding.nest.internal.wwn.config.WWNStructureConfiguration; +import org.openhab.binding.nest.internal.wwn.dto.BaseWWNDevice; +import org.openhab.binding.nest.internal.wwn.dto.WWNCamera; +import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector; +import org.openhab.binding.nest.internal.wwn.dto.WWNStructure; +import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat; +import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler; +import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This service connects to the Nest account and creates the correct discovery results for devices + * as they are found through the WWN API. + * + * @author David Bennett - Initial contribution + * @author Wouter Born - Add representation properties + */ +@NonNullByDefault +public class WWNDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_CAMERA, THING_TYPE_THERMOSTAT, + THING_TYPE_SMOKE_DETECTOR, THING_TYPE_STRUCTURE); + + private final Logger logger = LoggerFactory.getLogger(WWNDiscoveryService.class); + + private final DiscoveryDataListener cameraDiscoveryDataListener = new DiscoveryDataListener<>( + WWNCamera.class, THING_TYPE_CAMERA, this::addDeviceDiscoveryResult); + private final DiscoveryDataListener smokeDetectorDiscoveryDataListener = new DiscoveryDataListener<>( + WWNSmokeDetector.class, THING_TYPE_SMOKE_DETECTOR, this::addDeviceDiscoveryResult); + private final DiscoveryDataListener structureDiscoveryDataListener = new DiscoveryDataListener<>( + WWNStructure.class, THING_TYPE_STRUCTURE, this::addStructureDiscoveryResult); + private final DiscoveryDataListener thermostatDiscoveryDataListener = new DiscoveryDataListener<>( + WWNThermostat.class, THING_TYPE_THERMOSTAT, this::addDeviceDiscoveryResult); + + @SuppressWarnings("rawtypes") + private final List discoveryDataListeners = List.of(cameraDiscoveryDataListener, + smokeDetectorDiscoveryDataListener, structureDiscoveryDataListener, thermostatDiscoveryDataListener); + + private @NonNullByDefault({}) WWNAccountHandler accountHandler; + + private static class DiscoveryDataListener implements WWNThingDataListener { + private Class dataClass; + private ThingTypeUID thingTypeUID; + private BiConsumer onDiscovered; + + private DiscoveryDataListener(Class dataClass, ThingTypeUID thingTypeUID, + BiConsumer onDiscovered) { + this.dataClass = dataClass; + this.thingTypeUID = thingTypeUID; + this.onDiscovered = onDiscovered; + } + + @Override + public void onNewData(T data) { + onDiscovered.accept(data, thingTypeUID); + } + + @Override + public void onUpdatedData(T oldData, T data) { + } + + @Override + public void onMissingData(String nestId) { + } + } + + public WWNDiscoveryService() { + super(SUPPORTED_THING_TYPES, 60, true); + } + + @Override + @SuppressWarnings("unchecked") + public void activate() { + discoveryDataListeners.forEach(listener -> accountHandler.addThingDataListener(listener.dataClass, listener)); + addDiscoveryResultsFromLastUpdates(); + } + + @Override + @SuppressWarnings("unchecked") + public void deactivate() { + discoveryDataListeners + .forEach(listener -> accountHandler.removeThingDataListener(listener.dataClass, listener)); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return accountHandler; + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof WWNAccountHandler) { + accountHandler = (WWNAccountHandler) handler; + } + } + + @Override + protected void startScan() { + addDiscoveryResultsFromLastUpdates(); + } + + @SuppressWarnings("unchecked") + private void addDiscoveryResultsFromLastUpdates() { + discoveryDataListeners.forEach(listener -> addDiscoveryResultsFromLastUpdates(listener.dataClass, + listener.thingTypeUID, listener.onDiscovered)); + } + + private void addDiscoveryResultsFromLastUpdates(Class dataClass, ThingTypeUID thingTypeUID, + BiConsumer onDiscovered) { + List lastUpdates = accountHandler.getLastUpdates(dataClass); + lastUpdates.forEach(lastUpdate -> onDiscovered.accept(lastUpdate, thingTypeUID)); + } + + private void addDeviceDiscoveryResult(BaseWWNDevice device, ThingTypeUID typeUID) { + ThingUID bridgeUID = accountHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(typeUID, bridgeUID, device.getDeviceId()); + logger.debug("Discovered {}", thingUID); + Map properties = Map.of(WWNDeviceConfiguration.DEVICE_ID, device.getDeviceId(), + PROPERTY_FIRMWARE_VERSION, device.getSoftwareVersion()); + thingDiscovered(DiscoveryResultBuilder.create(thingUID) // + .withThingType(typeUID) // + .withLabel(device.getNameLong()) // + .withBridge(bridgeUID) // + .withProperties(properties) // + .withRepresentationProperty(WWNDeviceConfiguration.DEVICE_ID) // + .build() // + ); + } + + public void addStructureDiscoveryResult(WWNStructure structure, ThingTypeUID typeUID) { + ThingUID bridgeUID = accountHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(typeUID, bridgeUID, structure.getStructureId()); + logger.debug("Discovered {}", thingUID); + Map properties = Map.of(WWNStructureConfiguration.STRUCTURE_ID, structure.getStructureId()); + thingDiscovered(DiscoveryResultBuilder.create(thingUID) // + .withThingType(THING_TYPE_STRUCTURE) // + .withLabel(structure.getName()) // + .withBridge(bridgeUID) // + .withProperties(properties) // + .withRepresentationProperty(WWNStructureConfiguration.STRUCTURE_ID) // + .build() // + ); + } +} diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/BaseNestDevice.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/BaseWWNDevice.java similarity index 95% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/BaseNestDevice.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/BaseWWNDevice.java index 8f2d23ebabcb8..1ffa6db5ca79e 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/BaseNestDevice.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/BaseWWNDevice.java @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; /** - * Default properties shared across all Nest devices. + * Default properties shared across all WWN devices. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class BaseNestDevice implements NestIdentifiable { +public class BaseWWNDevice implements WWNIdentifiable { private String deviceId; private String name; @@ -80,7 +80,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - BaseNestDevice other = (BaseNestDevice) obj; + BaseWWNDevice other = (BaseWWNDevice) obj; if (deviceId == null) { if (other.deviceId != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/AccessTokenData.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNAccessTokenData.java similarity index 89% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/AccessTokenData.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNAccessTokenData.java index 504f9947be1b7..e9416601defeb 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/AccessTokenData.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNAccessTokenData.java @@ -10,15 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * Deals with the access token data that comes back from Nest when it is requested. + * Deals with the access token data that comes back from WWN when it is requested. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class AccessTokenData { +public class WWNAccessTokenData { private String accessToken; private Long expiresIn; @@ -42,7 +42,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - AccessTokenData other = (AccessTokenData) obj; + WWNAccessTokenData other = (WWNAccessTokenData) obj; if (accessToken == null) { if (other.accessToken != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ActivityZone.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNActivityZone.java similarity index 90% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ActivityZone.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNActivityZone.java index e548407ec54b7..4a8a352b4e34e 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ActivityZone.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNActivityZone.java @@ -10,15 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * The data for a camera activity zone. + * The data for a WWN camera activity zone. * * @author David Bennett - Initial contribution * @author Wouter Born - Extract ActivityZone object from Camera */ -public class ActivityZone { +public class WWNActivityZone { private String name; private int id; @@ -42,7 +42,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - ActivityZone other = (ActivityZone) obj; + WWNActivityZone other = (WWNActivityZone) obj; if (id != other.id) { return false; } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Camera.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCamera.java similarity index 94% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Camera.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCamera.java index f45ffabe23d81..e548066b9334b 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Camera.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCamera.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; import java.util.List; /** - * The data for the camera. + * The data for the WWN camera. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class Camera extends BaseNestDevice { +public class WWNCamera extends BaseWWNDevice { private Boolean isStreaming; private Boolean isAudioInputEnabled; @@ -30,10 +30,10 @@ public class Camera extends BaseNestDevice { private String webUrl; private String appUrl; private Boolean isPublicShareEnabled; - private List activityZones; + private List activityZones; private String publicShareUrl; private String snapshotUrl; - private CameraEvent lastEvent; + private WWNCameraEvent lastEvent; public Boolean isStreaming() { return isStreaming; @@ -63,7 +63,7 @@ public Boolean isPublicShareEnabled() { return isPublicShareEnabled; } - public List getActivityZones() { + public List getActivityZones() { return activityZones; } @@ -75,7 +75,7 @@ public String getSnapshotUrl() { return snapshotUrl; } - public CameraEvent getLastEvent() { + public WWNCameraEvent getLastEvent() { return lastEvent; } @@ -84,13 +84,13 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!super.equals(obj)) { + if (obj == null || !super.equals(obj)) { return false; } if (getClass() != obj.getClass()) { return false; } - Camera other = (Camera) obj; + WWNCamera other = (WWNCamera) obj; if (activityZones == null) { if (other.activityZones != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/CameraEvent.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCameraEvent.java similarity index 97% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/CameraEvent.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCameraEvent.java index 4545d4247be89..b96897378ae60 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/CameraEvent.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNCameraEvent.java @@ -10,19 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; import java.util.List; /** - * The data for a camera event. + * The data for a WWN camera event. * * @author David Bennett - Initial contribution * @author Wouter Born - Extract CameraEvent object from Camera * @author Wouter Born - Add equals, hashCode, toString methods */ -public class CameraEvent { +public class WWNCameraEvent { private Boolean hasSound; private Boolean hasMotion; @@ -91,7 +91,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - CameraEvent other = (CameraEvent) obj; + WWNCameraEvent other = (WWNCameraEvent) obj; if (activityZoneIds == null) { if (other.activityZoneIds != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestDevices.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDevices.java similarity index 82% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestDevices.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDevices.java index f17ae0ae96cb7..3f5d5847a436e 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestDevices.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDevices.java @@ -10,33 +10,33 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Map; /** - * All the Nest devices broken up by type. + * All the WWN devices broken up by type. * * @author David Bennett - Initial contribution */ -public class NestDevices { +public class WWNDevices { - private Map thermostats; - private Map smokeCoAlarms; - private Map cameras; + private Map thermostats; + private Map smokeCoAlarms; + private Map cameras; /** Id to thermostat mapping */ - public Map getThermostats() { + public Map getThermostats() { return thermostats; } /** Id to camera mapping */ - public Map getCameras() { + public Map getCameras() { return cameras; } /** Id to smoke detector */ - public Map getSmokeCoAlarms() { + public Map getSmokeCoAlarms() { return smokeCoAlarms; } @@ -51,7 +51,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - NestDevices other = (NestDevices) obj; + WWNDevices other = (WWNDevices) obj; if (cameras == null) { if (other.cameras != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ETA.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNETA.java similarity index 95% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ETA.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNETA.java index e3985631181c4..90bd4dfbabc8e 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ETA.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNETA.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; /** - * Used to set and update the ETA values for Nest. + * Used to set and update the WWN ETA values. * * @author David Bennett - Initial contribution * @author Wouter Born - Extract ETA object from Structure * @author Wouter Born - Add equals, hashCode, toString methods */ -public class ETA { +public class WWNETA { private String tripId; private Date estimatedArrivalWindowBegin; @@ -62,7 +62,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - ETA other = (ETA) obj; + WWNETA other = (WWNETA) obj; if (estimatedArrivalWindowBegin == null) { if (other.estimatedArrivalWindowBegin != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ErrorData.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNErrorData.java similarity index 94% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ErrorData.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNErrorData.java index 5f6acd7780b9c..be74cfc827664 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/ErrorData.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNErrorData.java @@ -10,16 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * The data of Nest API errors. + * The data of WWN API errors. * * @author Wouter Born - Initial contribution * @author Wouter Born - Improve exception handling * @author Wouter Born - Add equals and hashCode methods */ -public class ErrorData { +public class WWNErrorData { private String error; private String type; @@ -53,7 +53,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - ErrorData other = (ErrorData) obj; + WWNErrorData other = (WWNErrorData) obj; if (error == null) { if (other.error != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestIdentifiable.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNIdentifiable.java similarity index 67% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestIdentifiable.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNIdentifiable.java index 09952e183739c..65e16cf093ee4 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestIdentifiable.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNIdentifiable.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * Interface for uniquely identifiable Nest objects (device or a structure). + * Interface for uniquely identifiable WWN objects (device or a structure). * * @author Wouter Born - Initial contribution * @author Wouter Born - Simplify working with deviceId and structureId */ -public interface NestIdentifiable { +public interface WWNIdentifiable { /** - * Returns the identifier that uniquely identifies the Nest object (deviceId or structureId). + * Returns the identifier that uniquely identifies the WWN object (deviceId or structureId). */ String getId(); } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestMetadata.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNMetadata.java similarity index 92% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestMetadata.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNMetadata.java index 3c4530fc1bca1..02fcece595d8a 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/NestMetadata.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNMetadata.java @@ -10,15 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * The meta data in the data downloads from Nest. + * The WWN meta data in the data downloads. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class NestMetadata { +public class WWNMetadata { private String accessToken; private String clientVersion; @@ -42,7 +42,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - NestMetadata other = (NestMetadata) obj; + WWNMetadata other = (WWNMetadata) obj; if (accessToken == null) { if (other.accessToken != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/SmokeDetector.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNSmokeDetector.java similarity index 93% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/SmokeDetector.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNSmokeDetector.java index 8c791ef9cedf4..096e3f843f6b8 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/SmokeDetector.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNSmokeDetector.java @@ -10,19 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; import com.google.gson.annotations.SerializedName; /** - * Data for the Nest smoke detector. + * Data for the WWN smoke detector. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class SmokeDetector extends BaseNestDevice { +public class WWNSmokeDetector extends BaseWWNDevice { private BatteryHealth batteryHealth; private AlarmState coAlarmState; @@ -87,13 +87,13 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!super.equals(obj)) { + if (obj == null || !super.equals(obj)) { return false; } if (getClass() != obj.getClass()) { return false; } - SmokeDetector other = (SmokeDetector) obj; + WWNSmokeDetector other = (WWNSmokeDetector) obj; if (batteryHealth != other.batteryHealth) { return false; } @@ -117,10 +117,7 @@ public boolean equals(Object obj) { if (smokeAlarmState != other.smokeAlarmState) { return false; } - if (uiColorState != other.uiColorState) { - return false; - } - return true; + return uiColorState == other.uiColorState; } @Override diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Structure.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNStructure.java similarity index 93% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Structure.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNStructure.java index 3d3eba7390cf0..43e5f2bc56e35 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Structure.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNStructure.java @@ -10,23 +10,23 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Date; import java.util.List; import java.util.Map; -import org.openhab.binding.nest.internal.data.SmokeDetector.AlarmState; +import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector.AlarmState; import com.google.gson.annotations.SerializedName; /** - * The structure details from Nest. + * The WWN structure details. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class Structure implements NestIdentifiable { +public class WWNStructure implements WWNIdentifiable { private String structureId; private List thermostats; @@ -38,13 +38,13 @@ public class Structure implements NestIdentifiable { private Date peakPeriodEndTime; private String timeZone; private Date etaBegin; - private SmokeDetector.AlarmState coAlarmState; - private SmokeDetector.AlarmState smokeAlarmState; + private WWNSmokeDetector.AlarmState coAlarmState; + private WWNSmokeDetector.AlarmState smokeAlarmState; private Boolean rhrEnrollment; - private Map wheres; + private Map wheres; private HomeAwayState away; private String name; - private ETA eta; + private WWNETA eta; private SecurityState wwnSecurityState; @Override @@ -112,11 +112,11 @@ public Boolean isRhrEnrollment() { return rhrEnrollment; } - public Map getWheres() { + public Map getWheres() { return wheres; } - public ETA getEta() { + public WWNETA getEta() { return eta; } @@ -155,7 +155,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - Structure other = (Structure) obj; + WWNStructure other = (WWNStructure) obj; if (away != other.away) { return false; } @@ -263,10 +263,7 @@ public boolean equals(Object obj) { } else if (!wheres.equals(other.wheres)) { return false; } - if (wwnSecurityState != other.wwnSecurityState) { - return false; - } - return true; + return wwnSecurityState == other.wwnSecurityState; } @Override diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Thermostat.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNThermostat.java similarity index 98% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Thermostat.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNThermostat.java index ed2c76a1456d0..558b6c7f5ef71 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Thermostat.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNThermostat.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT; import static org.openhab.core.library.unit.SIUnits.CELSIUS; @@ -23,12 +23,12 @@ import com.google.gson.annotations.SerializedName; /** - * Gson class to encapsulate the data for the Nest thermostat. + * Gson class to encapsulate the data for the WWN thermostat. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class Thermostat extends BaseNestDevice { +public class WWNThermostat extends BaseWWNDevice { private Boolean canCool; private Boolean canHeat; @@ -262,13 +262,13 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!super.equals(obj)) { + if (obj == null || !super.equals(obj)) { return false; } if (getClass() != obj.getClass()) { return false; } - Thermostat other = (Thermostat) obj; + WWNThermostat other = (WWNThermostat) obj; if (ambientTemperatureC == null) { if (other.ambientTemperatureC != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelData.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelData.java similarity index 82% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelData.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelData.java index 5a2e4255095e1..87aea4c3cc93d 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelData.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelData.java @@ -10,31 +10,31 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.Map; /** - * Top level data for all the Nest stuff, this is the format the Nest data comes back from Nest in. + * The top level WWN data that is sent by Nest. * * @author David Bennett - Initial contribution * @author Wouter Born - Add equals and hashCode methods */ -public class TopLevelData { +public class WWNTopLevelData { - private NestDevices devices; - private NestMetadata metadata; - private Map structures; + private WWNDevices devices; + private WWNMetadata metadata; + private Map structures; - public NestDevices getDevices() { + public WWNDevices getDevices() { return devices; } - public NestMetadata getMetadata() { + public WWNMetadata getMetadata() { return metadata; } - public Map getStructures() { + public Map getStructures() { return structures; } @@ -49,7 +49,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - TopLevelData other = (TopLevelData) obj; + WWNTopLevelData other = (WWNTopLevelData) obj; if (devices == null) { if (other.devices != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelStreamingData.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelStreamingData.java similarity index 85% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelStreamingData.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelStreamingData.java index f5bc4e11fa79a..9b5354bdb2f37 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/TopLevelStreamingData.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNTopLevelStreamingData.java @@ -10,25 +10,25 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** - * The top level data that is sent by Nest to a streaming REST client using SSE. + * The top level WWN data that is sent by Nest to a streaming REST client using SSE. * * @author Wouter Born - Initial contribution * @author Wouter Born - Replace polling with REST streaming * @author Wouter Born - Add equals and hashCode methods */ -public class TopLevelStreamingData { +public class WWNTopLevelStreamingData { private String path; - private TopLevelData data; + private WWNTopLevelData data; public String getPath() { return path; } - public TopLevelData getData() { + public WWNTopLevelData getData() { return data; } @@ -52,7 +52,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - TopLevelStreamingData other = (TopLevelStreamingData) obj; + WWNTopLevelStreamingData other = (WWNTopLevelStreamingData) obj; if (data == null) { if (other.data != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestUpdateRequest.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNUpdateRequest.java similarity index 83% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestUpdateRequest.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNUpdateRequest.java index 7b62eb6d250db..26b45879dcd5f 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestUpdateRequest.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNUpdateRequest.java @@ -10,21 +10,21 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.rest; +package org.openhab.binding.nest.internal.wwn.dto; import java.util.HashMap; import java.util.Map; /** - * Contains the data needed to do an update request back to Nest. + * Contains the data needed to do an WWN update request back to Nest. * * @author David Bennett - Initial contribution */ -public class NestUpdateRequest { +public class WWNUpdateRequest { private final String updatePath; private final Map values; - private NestUpdateRequest(Builder builder) { + private WWNUpdateRequest(Builder builder) { this.updatePath = builder.basePath + builder.identifier; this.values = builder.values; } @@ -57,8 +57,8 @@ public Builder withAdditionalValue(String field, Object value) { return this; } - public NestUpdateRequest build() { - return new NestUpdateRequest(this); + public WWNUpdateRequest build() { + return new WWNUpdateRequest(this); } } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Where.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNWhere.java similarity index 94% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Where.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNWhere.java index 0a5f0b7c909eb..aa3df5bdc7642 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/data/Where.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNWhere.java @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; /** * @author David Bennett - Initial contribution * @author Wouter Born - Extract Where object from Structure * @author Wouter Born - Add equals, hashCode, toString methods */ -public class Where { +public class WWNWhere { private String whereId; private String name; @@ -40,7 +40,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - Where other = (Where) obj; + WWNWhere other = (WWNWhere) obj; if (name == null) { if (other.name != null) { return false; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedResolvingNestUrlException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedResolvingWWNUrlException.java similarity index 64% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedResolvingNestUrlException.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedResolvingWWNUrlException.java index 23d8ed8d95b7d..23809fcd03598 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedResolvingNestUrlException.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedResolvingWWNUrlException.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.exceptions; +package org.openhab.binding.nest.internal.wwn.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Will be thrown when the bridge was unable to resolve the Nest redirect URL. @@ -18,17 +20,18 @@ * @author Wouter Born - Initial contribution * @author Wouter Born - Improve exception handling while sending data */ +@NonNullByDefault @SuppressWarnings("serial") -public class FailedResolvingNestUrlException extends Exception { - public FailedResolvingNestUrlException(String message) { +public class FailedResolvingWWNUrlException extends Exception { + public FailedResolvingWWNUrlException(String message) { super(message); } - public FailedResolvingNestUrlException(String message, Throwable cause) { + public FailedResolvingWWNUrlException(String message, Throwable cause) { super(message, cause); } - public FailedResolvingNestUrlException(Throwable cause) { + public FailedResolvingWWNUrlException(Throwable cause) { super(cause); } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedRetrievingNestDataException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedRetrievingWWNDataException.java similarity index 64% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedRetrievingNestDataException.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedRetrievingWWNDataException.java index f762c3c696616..7b13589f43a54 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedRetrievingNestDataException.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedRetrievingWWNDataException.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.exceptions; +package org.openhab.binding.nest.internal.wwn.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Will be thrown when the bridge was unable to retrieve data. @@ -18,18 +20,19 @@ * @author Martin van Wingerden - Initial contribution * @author Martin van Wingerden - Added more centralized handling of failure when retrieving data */ +@NonNullByDefault @SuppressWarnings("serial") -public class FailedRetrievingNestDataException extends Exception { +public class FailedRetrievingWWNDataException extends Exception { - public FailedRetrievingNestDataException(String message) { + public FailedRetrievingWWNDataException(String message) { super(message); } - public FailedRetrievingNestDataException(String message, Throwable cause) { + public FailedRetrievingWWNDataException(String message, Throwable cause) { super(message, cause); } - public FailedRetrievingNestDataException(Throwable cause) { + public FailedRetrievingWWNDataException(Throwable cause) { super(cause); } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedSendingNestDataException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedSendingWWNDataException.java similarity index 64% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedSendingNestDataException.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedSendingWWNDataException.java index 02627e87b3e45..a2c5d513d63e1 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/FailedSendingNestDataException.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/FailedSendingWWNDataException.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.exceptions; +package org.openhab.binding.nest.internal.wwn.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Will be thrown when the bridge was unable to send data. @@ -18,17 +20,18 @@ * @author Wouter Born - Initial contribution * @author Wouter Born - Improve exception handling while sending data */ +@NonNullByDefault @SuppressWarnings("serial") -public class FailedSendingNestDataException extends Exception { - public FailedSendingNestDataException(String message) { +public class FailedSendingWWNDataException extends Exception { + public FailedSendingWWNDataException(String message) { super(message); } - public FailedSendingNestDataException(String message, Throwable cause) { + public FailedSendingWWNDataException(String message, Throwable cause) { super(message, cause); } - public FailedSendingNestDataException(Throwable cause) { + public FailedSendingWWNDataException(Throwable cause) { super(cause); } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/InvalidAccessTokenException.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/InvalidWWNAccessTokenException.java similarity index 65% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/InvalidAccessTokenException.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/InvalidWWNAccessTokenException.java index ea14ee6af04fa..2430e405514bf 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/exceptions/InvalidAccessTokenException.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/exceptions/InvalidWWNAccessTokenException.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.exceptions; +package org.openhab.binding.nest.internal.wwn.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Will be thrown when there is no valid access token and it was not possible to refresh it @@ -18,17 +20,18 @@ * @author Martin van Wingerden - Initial contribution * @author Martin van Wingerden - Added more centralized handling of invalid access tokens */ +@NonNullByDefault @SuppressWarnings("serial") -public class InvalidAccessTokenException extends Exception { - public InvalidAccessTokenException(Exception cause) { +public class InvalidWWNAccessTokenException extends Exception { + public InvalidWWNAccessTokenException(Exception cause) { super(cause); } - public InvalidAccessTokenException(String message, Throwable cause) { + public InvalidWWNAccessTokenException(String message, Throwable cause) { super(message, cause); } - public InvalidAccessTokenException(String message) { + public InvalidWWNAccessTokenException(String message) { super(message); } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBridgeHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandler.java similarity index 72% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBridgeHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandler.java index 813877c78cc62..ba0c239ddefb4 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBridgeHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandler.java @@ -10,14 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.JSON_CONTENT_TYPE; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Properties; @@ -30,20 +31,21 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.NestUtils; -import org.openhab.binding.nest.internal.config.NestBridgeConfiguration; -import org.openhab.binding.nest.internal.data.ErrorData; -import org.openhab.binding.nest.internal.data.NestIdentifiable; -import org.openhab.binding.nest.internal.data.TopLevelData; -import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException; -import org.openhab.binding.nest.internal.exceptions.FailedSendingNestDataException; -import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException; -import org.openhab.binding.nest.internal.listener.NestStreamingDataListener; -import org.openhab.binding.nest.internal.listener.NestThingDataListener; -import org.openhab.binding.nest.internal.rest.NestAuthorizer; -import org.openhab.binding.nest.internal.rest.NestStreamingRestClient; -import org.openhab.binding.nest.internal.rest.NestUpdateRequest; -import org.openhab.binding.nest.internal.update.NestCompositeUpdateHandler; +import org.openhab.binding.nest.internal.wwn.WWNUtils; +import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration; +import org.openhab.binding.nest.internal.wwn.discovery.WWNDiscoveryService; +import org.openhab.binding.nest.internal.wwn.dto.WWNErrorData; +import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable; +import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData; +import org.openhab.binding.nest.internal.wwn.dto.WWNUpdateRequest; +import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException; +import org.openhab.binding.nest.internal.wwn.exceptions.FailedSendingWWNDataException; +import org.openhab.binding.nest.internal.wwn.exceptions.InvalidWWNAccessTokenException; +import org.openhab.binding.nest.internal.wwn.listener.WWNStreamingDataListener; +import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener; +import org.openhab.binding.nest.internal.wwn.rest.WWNAuthorizer; +import org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient; +import org.openhab.binding.nest.internal.wwn.update.WWNCompositeUpdateHandler; import org.openhab.core.config.core.Configuration; import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.thing.Bridge; @@ -53,6 +55,7 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.osgi.service.jaxrs.client.SseEventSourceFactory; @@ -60,7 +63,7 @@ import org.slf4j.LoggerFactory; /** - * This bridge handler connects to Nest and handles all the API requests. It pulls down the + * This account handler connects to Nest and handles all the WWN API requests. It pulls down the * updated data, polls the system and does all the co-ordination with the other handlers * to get the data updated to the correct things. * @@ -69,32 +72,32 @@ * @author Wouter Born - Improve exception and URL redirect handling */ @NonNullByDefault -public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener { +public class WWNAccountHandler extends BaseBridgeHandler implements WWNStreamingDataListener { private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30); - private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class); + private final Logger logger = LoggerFactory.getLogger(WWNAccountHandler.class); private final ClientBuilder clientBuilder; private final SseEventSourceFactory eventSourceFactory; - private final List nestUpdateRequests = new CopyOnWriteArrayList<>(); - private final NestCompositeUpdateHandler updateHandler = new NestCompositeUpdateHandler( + private final List nestUpdateRequests = new CopyOnWriteArrayList<>(); + private final WWNCompositeUpdateHandler updateHandler = new WWNCompositeUpdateHandler( this::getPresentThingsNestIds); - private @NonNullByDefault({}) NestAuthorizer authorizer; - private @NonNullByDefault({}) NestBridgeConfiguration config; + private @NonNullByDefault({}) WWNAuthorizer authorizer; + private @NonNullByDefault({}) WWNAccountConfiguration config; private @Nullable ScheduledFuture initializeJob; private @Nullable ScheduledFuture transmitJob; - private @Nullable NestRedirectUrlSupplier redirectUrlSupplier; - private @Nullable NestStreamingRestClient streamingRestClient; + private @Nullable WWNRedirectUrlSupplier redirectUrlSupplier; + private @Nullable WWNStreamingRestClient streamingRestClient; /** * Creates the bridge handler to connect to Nest. * * @param bridge The bridge to connect to Nest with. */ - public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) { + public WWNAccountHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) { super(bridge); this.clientBuilder = clientBuilder; this.eventSourceFactory = eventSourceFactory; @@ -107,8 +110,8 @@ public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSou public void initialize() { logger.debug("Initializing Nest bridge handler"); - config = getConfigAs(NestBridgeConfiguration.class); - authorizer = new NestAuthorizer(config); + config = getConfigAs(WWNAccountConfiguration.class); + authorizer = new WWNAuthorizer(config); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query"); initializeJob = scheduler.schedule(() -> { @@ -119,7 +122,7 @@ public void initialize() { logger.debug("Access Token {}", getExistingOrNewAccessToken()); redirectUrlSupplier = createRedirectUrlSupplier(); restartStreamingUpdates(); - } catch (InvalidAccessTokenException e) { + } catch (InvalidWWNAccessTokenException e) { logger.debug("Invalid access token", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token is invalid and could not be refreshed: " + e.getMessage()); @@ -154,27 +157,27 @@ public void dispose() { this.streamingRestClient = null; } - public boolean addThingDataListener(Class dataClass, NestThingDataListener listener) { + public boolean addThingDataListener(Class dataClass, WWNThingDataListener listener) { return updateHandler.addListener(dataClass, listener); } - public boolean addThingDataListener(Class dataClass, String nestId, NestThingDataListener listener) { + public boolean addThingDataListener(Class dataClass, String nestId, WWNThingDataListener listener) { return updateHandler.addListener(dataClass, nestId, listener); } /** * Adds the update request into the queue for doing something with, send immediately if the queue is empty. */ - public void addUpdateRequest(NestUpdateRequest request) { + public void addUpdateRequest(WWNUpdateRequest request) { nestUpdateRequests.add(request); scheduleTransmitJobForPendingRequests(); } - protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException { - return new NestRedirectUrlSupplier(getHttpHeaders()); + protected WWNRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidWWNAccessTokenException { + return new WWNRedirectUrlSupplier(getHttpHeaders()); } - private String getExistingOrNewAccessToken() throws InvalidAccessTokenException { + private String getExistingOrNewAccessToken() throws InvalidWWNAccessTokenException { String accessToken = config.accessToken; if (accessToken == null || accessToken.isEmpty()) { accessToken = authorizer.getNewAccessToken(); @@ -182,8 +185,8 @@ private String getExistingOrNewAccessToken() throws InvalidAccessTokenException config.pincode = ""; // Update and save the access token in the bridge configuration Configuration configuration = editConfiguration(); - configuration.put(NestBridgeConfiguration.ACCESS_TOKEN, config.accessToken); - configuration.put(NestBridgeConfiguration.PINCODE, config.pincode); + configuration.put(WWNAccountConfiguration.ACCESS_TOKEN, config.accessToken); + configuration.put(WWNAccountConfiguration.PINCODE, config.pincode); updateConfiguration(configuration); logger.debug("Retrieved new access token: {}", config.accessToken); return accessToken; @@ -193,7 +196,7 @@ private String getExistingOrNewAccessToken() throws InvalidAccessTokenException } } - protected Properties getHttpHeaders() throws InvalidAccessTokenException { + protected Properties getHttpHeaders() throws InvalidWWNAccessTokenException { Properties httpHeaders = new Properties(); httpHeaders.put("Authorization", "Bearer " + getExistingOrNewAccessToken()); httpHeaders.put("Content-Type", JSON_CONTENT_TYPE); @@ -208,8 +211,8 @@ public List getLastUpdates(Class dataClass) { return updateHandler.getLastUpdates(dataClass); } - private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException { - NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; + private WWNRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidWWNAccessTokenException { + WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; if (localRedirectUrlSupplier == null) { localRedirectUrlSupplier = createRedirectUrlSupplier(); redirectUrlSupplier = localRedirectUrlSupplier; @@ -222,12 +225,17 @@ private Set getPresentThingsNestIds() { for (Thing thing : getThing().getThings()) { ThingHandler handler = thing.getHandler(); if (handler != null && thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.GONE) { - nestIds.add(((NestIdentifiable) handler).getId()); + nestIds.add(((WWNIdentifiable) handler).getId()); } } return nestIds; } + @Override + public Collection> getServices() { + return List.of(WWNDiscoveryService.class); + } + /** * Handles an incoming command update */ @@ -239,18 +247,18 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void jsonToPutUrl(NestUpdateRequest request) - throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException { + private void jsonToPutUrl(WWNUpdateRequest request) + throws FailedSendingWWNDataException, InvalidWWNAccessTokenException, FailedResolvingWWNUrlException { try { - NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; + WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; if (localRedirectUrlSupplier == null) { - throw new FailedResolvingNestUrlException("redirectUrlSupplier is null"); + throw new FailedResolvingWWNUrlException("redirectUrlSupplier is null"); } String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath(); logger.debug("Putting data to: {}", url); - String jsonContent = NestUtils.toJson(request.getValues()); + String jsonContent = WWNUtils.toJson(request.getValues()); logger.debug("PUT content: {}", jsonContent); ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); @@ -258,13 +266,13 @@ private void jsonToPutUrl(NestUpdateRequest request) REQUEST_TIMEOUT); logger.debug("PUT response: {}", jsonResponse); - ErrorData error = NestUtils.fromJson(jsonResponse, ErrorData.class); + WWNErrorData error = WWNUtils.fromJson(jsonResponse, WWNErrorData.class); if (error.getError() != null && !error.getError().isBlank()) { logger.debug("Nest API error: {}", error); logger.warn("Nest API error: {}", error.getMessage()); } } catch (IOException e) { - throw new FailedSendingNestDataException("Failed to send data", e); + throw new FailedSendingWWNDataException("Failed to send data", e); } } @@ -291,16 +299,16 @@ public void onError(String message) { } @Override - public void onNewTopLevelData(TopLevelData data) { + public void onNewTopLevelData(WWNTopLevelData data) { updateHandler.handleUpdate(data); updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data"); } - public boolean removeThingDataListener(Class dataClass, NestThingDataListener listener) { + public boolean removeThingDataListener(Class dataClass, WWNThingDataListener listener) { return updateHandler.removeListener(dataClass, listener); } - public boolean removeThingDataListener(Class dataClass, String nestId, NestThingDataListener listener) { + public boolean removeThingDataListener(Class dataClass, String nestId, WWNThingDataListener listener) { return updateHandler.removeListener(dataClass, nestId, listener); } @@ -321,14 +329,14 @@ private void scheduleTransmitJobForPendingRequests() { private void startStreamingUpdates() { synchronized (this) { try { - NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient( + WWNStreamingRestClient localStreamingRestClient = new WWNStreamingRestClient( getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory, getOrCreateRedirectUrlSupplier(), scheduler); localStreamingRestClient.addStreamingDataListener(this); localStreamingRestClient.start(); streamingRestClient = localStreamingRestClient; - } catch (InvalidAccessTokenException e) { + } catch (InvalidWWNAccessTokenException e) { logger.debug("Invalid access token", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token is invalid and could not be refreshed: " + e.getMessage()); @@ -337,7 +345,7 @@ private void startStreamingUpdates() { } private void stopStreamingUpdates() { - NestStreamingRestClient localStreamingRestClient = streamingRestClient; + WWNStreamingRestClient localStreamingRestClient = streamingRestClient; if (localStreamingRestClient != null) { synchronized (this) { localStreamingRestClient.stop(); @@ -357,24 +365,24 @@ private void transmitQueue() { try { while (!nestUpdateRequests.isEmpty()) { // nestUpdateRequests is a CopyOnWriteArrayList so its iterator does not support remove operations - NestUpdateRequest request = nestUpdateRequests.get(0); + WWNUpdateRequest request = nestUpdateRequests.get(0); jsonToPutUrl(request); nestUpdateRequests.remove(request); } - } catch (InvalidAccessTokenException e) { + } catch (InvalidWWNAccessTokenException e) { logger.debug("Invalid access token", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token is invalid and could not be refreshed: " + e.getMessage()); - } catch (FailedResolvingNestUrlException e) { + } catch (FailedResolvingWWNUrlException e) { logger.debug("Unable to resolve redirect URL", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS); - } catch (FailedSendingNestDataException e) { + } catch (FailedSendingWWNDataException e) { logger.debug("Error sending data", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS); - NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; + WWNRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier; if (localRedirectUrlSupplier != null) { localRedirectUrlSupplier.resetCache(); } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBaseHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNBaseHandler.java similarity index 67% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBaseHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNBaseHandler.java index 80b52b0ce01b8..659acfd010363 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestBaseHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNBaseHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; import java.time.Instant; import java.time.ZonedDateTime; @@ -22,12 +22,13 @@ import javax.measure.Quantity; import javax.measure.Unit; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.config.NestDeviceConfiguration; -import org.openhab.binding.nest.internal.data.NestIdentifiable; -import org.openhab.binding.nest.internal.listener.NestThingDataListener; -import org.openhab.binding.nest.internal.rest.NestUpdateRequest; +import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration; +import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable; +import org.openhab.binding.nest.internal.wwn.dto.WWNUpdateRequest; +import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -46,7 +47,7 @@ import org.slf4j.LoggerFactory; /** - * Deals with the structures on the Nest API, turning them into a thing in openHAB. + * Deals with the structures on the WWN API, turning them into a thing in openHAB. * * @author David Bennett - Initial contribution * @author Martin van Wingerden - Splitted of NestBaseHandler @@ -55,14 +56,14 @@ * @param the type of update data */ @NonNullByDefault -public abstract class NestBaseHandler extends BaseThingHandler - implements NestThingDataListener, NestIdentifiable { - private final Logger logger = LoggerFactory.getLogger(NestBaseHandler.class); +public abstract class WWNBaseHandler<@NonNull T> extends BaseThingHandler + implements WWNThingDataListener, WWNIdentifiable { + private final Logger logger = LoggerFactory.getLogger(WWNBaseHandler.class); - private @Nullable String deviceId; + private String deviceId = ""; private Class dataClass; - NestBaseHandler(Thing thing, Class dataClass) { + WWNBaseHandler(Thing thing, Class dataClass) { super(thing); this.dataClass = dataClass; } @@ -71,7 +72,7 @@ public abstract class NestBaseHandler extends BaseThingHandler public void initialize() { logger.debug("Initializing handler for {}", getClass().getName()); - NestBridgeHandler handler = getNestBridgeHandler(); + WWNAccountHandler handler = getAccountHandler(); if (handler != null) { boolean success = handler.addThingDataListener(dataClass, getId(), this); logger.debug("Adding {} with ID '{}' as device data listener, result: {}", getClass().getSimpleName(), @@ -83,7 +84,7 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for refresh"); - T lastUpdate = getLastUpdate(); + final @Nullable T lastUpdate = getLastUpdate(); if (lastUpdate != null) { update(null, lastUpdate); } @@ -91,14 +92,14 @@ public void initialize() { @Override public void dispose() { - NestBridgeHandler handler = getNestBridgeHandler(); + WWNAccountHandler handler = getAccountHandler(); if (handler != null) { handler.removeThingDataListener(dataClass, getId(), this); } } protected @Nullable T getLastUpdate() { - NestBridgeHandler handler = getNestBridgeHandler(); + WWNAccountHandler handler = getAccountHandler(); if (handler != null) { return handler.getLastUpdate(dataClass, getId()); } @@ -106,15 +107,13 @@ public void dispose() { } protected void addUpdateRequest(String updatePath, String field, Object value) { - NestBridgeHandler handler = getNestBridgeHandler(); + WWNAccountHandler handler = getAccountHandler(); if (handler != null) { - // @formatter:off - handler.addUpdateRequest(new NestUpdateRequest.Builder() - .withBasePath(updatePath) - .withIdentifier(getId()) - .withAdditionalValue(field, value) - .build()); - // @formatter:on + handler.addUpdateRequest(new WWNUpdateRequest.Builder() // + .withBasePath(updatePath) // + .withIdentifier(getId()) // + .withAdditionalValue(field, value) // + .build()); } } @@ -125,16 +124,16 @@ public String getId() { protected String getDeviceId() { String localDeviceId = deviceId; - if (localDeviceId == null) { - localDeviceId = getConfigAs(NestDeviceConfiguration.class).deviceId; + if (localDeviceId.isEmpty()) { + localDeviceId = getConfigAs(WWNDeviceConfiguration.class).deviceId; deviceId = localDeviceId; } return localDeviceId; } - protected @Nullable NestBridgeHandler getNestBridgeHandler() { + protected @Nullable WWNAccountHandler getAccountHandler() { Bridge bridge = getBridge(); - return bridge != null ? (NestBridgeHandler) bridge.getHandler() : null; + return bridge != null ? (WWNAccountHandler) bridge.getHandler() : null; } protected abstract State getChannelState(ChannelUID channelUID, T data); @@ -165,23 +164,24 @@ protected State getAsStringTypeOrNull(@Nullable Object value) { return value == null ? UnDefType.NULL : new StringType(value.toString()); } - protected State getAsStringTypeListOrNull(@Nullable Collection values) { + protected State getAsStringTypeListOrNull(@Nullable Collection<@NonNull ?> values) { return values == null || values.isEmpty() ? UnDefType.NULL - : new StringType(values.stream().map(v -> v.toString()).collect(Collectors.joining(","))); + : new StringType(values.stream().map(value -> value.toString()).collect(Collectors.joining(","))); } - protected boolean isNotHandling(NestIdentifiable nestIdentifiable) { + protected boolean isNotHandling(WWNIdentifiable nestIdentifiable) { return !(getId().equals(nestIdentifiable.getId())); } - protected void updateLinkedChannels(T oldData, T data) { - getThing().getChannels().stream().map(c -> c.getUID()).filter(this::isLinked).forEach(channelUID -> { - State newState = getChannelState(channelUID, data); - if (oldData == null || !getChannelState(channelUID, oldData).equals(newState)) { - logger.debug("Updating {}", channelUID); - updateState(channelUID, newState); - } - }); + protected void updateLinkedChannels(@Nullable T oldData, T data) { + getThing().getChannels().stream().map(channel -> channel.getUID()).filter(this::isLinked) + .forEach(channelUID -> { + State newState = getChannelState(channelUID, data); + if (oldData == null || !getChannelState(channelUID, oldData).equals(newState)) { + logger.debug("Updating {}", channelUID); + updateState(channelUID, newState); + } + }); } @Override @@ -200,5 +200,5 @@ public void onMissingData(String nestId) { new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Missing from streaming updates")); } - protected abstract void update(T oldData, T data); + protected abstract void update(@Nullable T oldData, T data); } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestCameraHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandler.java similarity index 86% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestCameraHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandler.java index 0c2534b96cb24..22a31e14414e8 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestCameraHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandler.java @@ -10,15 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nest.internal.data.Camera; -import org.openhab.binding.nest.internal.data.CameraEvent; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.wwn.dto.WWNCamera; +import org.openhab.binding.nest.internal.wwn.dto.WWNCameraEvent; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -30,22 +31,21 @@ import org.slf4j.LoggerFactory; /** - * Handles all the updates to the camera as well as handling the commands that send - * updates to Nest. + * Handles all the updates to the camera as well as handling the commands that send updates to the WWN API. * * @author David Bennett - Initial contribution * @author Wouter Born - Handle channel refresh command */ @NonNullByDefault -public class NestCameraHandler extends NestBaseHandler { - private final Logger logger = LoggerFactory.getLogger(NestCameraHandler.class); +public class WWNCameraHandler extends WWNBaseHandler { + private final Logger logger = LoggerFactory.getLogger(WWNCameraHandler.class); - public NestCameraHandler(Thing thing) { - super(thing, Camera.class); + public WWNCameraHandler(Thing thing) { + super(thing, WWNCamera.class); } @Override - protected State getChannelState(ChannelUID channelUID, Camera camera) { + protected State getChannelState(ChannelUID channelUID, WWNCamera camera) { if (channelUID.getId().startsWith(CHANNEL_GROUP_CAMERA_PREFIX)) { return getCameraChannelState(channelUID, camera); } else if (channelUID.getId().startsWith(CHANNEL_GROUP_LAST_EVENT_PREFIX)) { @@ -56,7 +56,7 @@ protected State getChannelState(ChannelUID channelUID, Camera camera) { } } - protected State getCameraChannelState(ChannelUID channelUID, Camera camera) { + protected State getCameraChannelState(ChannelUID channelUID, WWNCamera camera) { switch (channelUID.getId()) { case CHANNEL_CAMERA_APP_URL: return getAsStringTypeOrNull(camera.getAppUrl()); @@ -82,8 +82,8 @@ protected State getCameraChannelState(ChannelUID channelUID, Camera camera) { } } - protected State getLastEventChannelState(ChannelUID channelUID, Camera camera) { - CameraEvent lastEvent = camera.getLastEvent(); + protected State getLastEventChannelState(ChannelUID channelUID, WWNCamera camera) { + WWNCameraEvent lastEvent = camera.getLastEvent(); if (lastEvent == null) { return UnDefType.NULL; } @@ -120,7 +120,7 @@ protected State getLastEventChannelState(ChannelUID channelUID, Camera camera) { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (REFRESH.equals(command)) { - Camera lastUpdate = getLastUpdate(); + WWNCamera lastUpdate = getLastUpdate(); if (lastUpdate != null) { updateState(channelUID, getChannelState(channelUID, lastUpdate)); } @@ -138,7 +138,7 @@ private void addUpdateRequest(String field, Object value) { } @Override - protected void update(Camera oldCamera, Camera camera) { + protected void update(@Nullable WWNCamera oldCamera, WWNCamera camera) { logger.debug("Updating {}", getThing().getUID()); updateLinkedChannels(oldCamera, camera); diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNRedirectUrlSupplier.java similarity index 72% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNRedirectUrlSupplier.java index 90d08da70d14b..1c4dd1cd6bf13 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestRedirectUrlSupplier.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNRedirectUrlSupplier.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -23,33 +23,33 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.openhab.binding.nest.internal.NestBindingConstants; -import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException; +import org.openhab.binding.nest.internal.wwn.WWNBindingConstants; +import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException; import org.openhab.core.io.net.http.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Supplies resolved redirect URLs of {@link NestBindingConstants#NEST_URL} so they can be used with HTTP clients that + * Supplies resolved redirect URLs of {@link WWNBindingConstants#NEST_URL} so they can be used with HTTP clients that * do not pass Authorization headers after redirects like the Jetty client used by {@link HttpUtil}. * * @author Wouter Born - Initial contribution * @author Wouter Born - Extract resolving redirect URL from NestBridgeHandler into NestRedirectUrlSupplier */ @NonNullByDefault -public class NestRedirectUrlSupplier { +public class WWNRedirectUrlSupplier { - private final Logger logger = LoggerFactory.getLogger(NestRedirectUrlSupplier.class); + private final Logger logger = LoggerFactory.getLogger(WWNRedirectUrlSupplier.class); protected String cachedUrl = ""; protected Properties httpHeaders; - public NestRedirectUrlSupplier(Properties httpHeaders) { + public WWNRedirectUrlSupplier(Properties httpHeaders) { this.httpHeaders = httpHeaders; } - public String getRedirectUrl() throws FailedResolvingNestUrlException { + public String getRedirectUrl() throws FailedResolvingWWNUrlException { if (cachedUrl.isEmpty()) { cachedUrl = resolveRedirectUrl(); } @@ -61,7 +61,7 @@ public void resetCache() { } /** - * Resolves the redirect URL for calls using the {@link NestBindingConstants#NEST_URL}. + * Resolves the redirect URL for calls using the {@link WWNBindingConstants#NEST_URL}. * * The Jetty client used by {@link HttpUtil} will not pass the Authorization header after a redirect resulting in * "401 Unauthorized error" issues. @@ -70,11 +70,11 @@ public void resetCache() { * * @see https://developers.nest.com/documentation/cloud/how-to-handle-redirects */ - private String resolveRedirectUrl() throws FailedResolvingNestUrlException { + private String resolveRedirectUrl() throws FailedResolvingWWNUrlException { HttpClient httpClient = new HttpClient(new SslContextFactory.Client()); httpClient.setFollowRedirects(false); - Request request = httpClient.newRequest(NestBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30, + Request request = httpClient.newRequest(WWNBindingConstants.NEST_URL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS); for (String httpHeaderKey : httpHeaders.stringPropertyNames()) { request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey)); @@ -86,7 +86,7 @@ private String resolveRedirectUrl() throws FailedResolvingNestUrlException { response = request.send(); httpClient.stop(); } catch (Exception e) { - throw new FailedResolvingNestUrlException("Failed to resolve redirect URL: " + e.getMessage(), e); + throw new FailedResolvingWWNUrlException("Failed to resolve redirect URL: " + e.getMessage(), e); } int status = response.getStatus(); @@ -95,10 +95,10 @@ private String resolveRedirectUrl() throws FailedResolvingNestUrlException { if (status != HttpStatus.TEMPORARY_REDIRECT_307) { logger.debug("Redirect status: {}", status); logger.debug("Redirect response: {}", response.getContentAsString()); - throw new FailedResolvingNestUrlException("Failed to get redirect URL, expected status " + throw new FailedResolvingWWNUrlException("Failed to get redirect URL, expected status " + HttpStatus.TEMPORARY_REDIRECT_307 + " but was " + status); } else if (redirectUrl == null || redirectUrl.isEmpty()) { - throw new FailedResolvingNestUrlException("Redirect URL is empty"); + throw new FailedResolvingWWNUrlException("Redirect URL is empty"); } redirectUrl = redirectUrl.endsWith("/") ? redirectUrl.substring(0, redirectUrl.length() - 1) : redirectUrl; diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestSmokeDetectorHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandler.java similarity index 76% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestSmokeDetectorHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandler.java index dc85a485b0276..98e9de4c6d51d 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestSmokeDetectorHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandler.java @@ -10,15 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nest.internal.data.SmokeDetector; -import org.openhab.binding.nest.internal.data.SmokeDetector.BatteryHealth; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector; +import org.openhab.binding.nest.internal.wwn.dto.WWNSmokeDetector.BatteryHealth; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -29,21 +30,21 @@ import org.slf4j.LoggerFactory; /** - * The smoke detector handler, it handles the data from Nest for the smoke detector. + * The smoke detector handler, it handles the data from WWN for the smoke detector. * * @author David Bennett - Initial contribution * @author Wouter Born - Handle channel refresh command */ @NonNullByDefault -public class NestSmokeDetectorHandler extends NestBaseHandler { - private final Logger logger = LoggerFactory.getLogger(NestSmokeDetectorHandler.class); +public class WWNSmokeDetectorHandler extends WWNBaseHandler { + private final Logger logger = LoggerFactory.getLogger(WWNSmokeDetectorHandler.class); - public NestSmokeDetectorHandler(Thing thing) { - super(thing, SmokeDetector.class); + public WWNSmokeDetectorHandler(Thing thing) { + super(thing, WWNSmokeDetector.class); } @Override - protected State getChannelState(ChannelUID channelUID, SmokeDetector smokeDetector) { + protected State getChannelState(ChannelUID channelUID, WWNSmokeDetector smokeDetector) { switch (channelUID.getId()) { case CHANNEL_CO_ALARM_STATE: return getAsStringTypeOrNull(smokeDetector.getCoAlarmState()); @@ -72,7 +73,7 @@ protected State getChannelState(ChannelUID channelUID, SmokeDetector smokeDetect @Override public void handleCommand(ChannelUID channelUID, Command command) { if (REFRESH.equals(command)) { - SmokeDetector lastUpdate = getLastUpdate(); + WWNSmokeDetector lastUpdate = getLastUpdate(); if (lastUpdate != null) { updateState(channelUID, getChannelState(channelUID, lastUpdate)); } @@ -80,7 +81,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - protected void update(SmokeDetector oldSmokeDetector, SmokeDetector smokeDetector) { + protected void update(@Nullable WWNSmokeDetector oldSmokeDetector, WWNSmokeDetector smokeDetector) { logger.debug("Updating {}", getThing().getUID()); updateLinkedChannels(oldSmokeDetector, smokeDetector); diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestStructureHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandler.java similarity index 80% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestStructureHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandler.java index e77cdc6a89dd3..df4f04c979c22 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestStructureHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandler.java @@ -10,16 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.config.NestStructureConfiguration; -import org.openhab.binding.nest.internal.data.Structure; -import org.openhab.binding.nest.internal.data.Structure.HomeAwayState; +import org.openhab.binding.nest.internal.wwn.config.WWNStructureConfiguration; +import org.openhab.binding.nest.internal.wwn.dto.WWNStructure; +import org.openhab.binding.nest.internal.wwn.dto.WWNStructure.HomeAwayState; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -31,23 +31,23 @@ import org.slf4j.LoggerFactory; /** - * Deals with the structures on the Nest API, turning them into a thing in openHAB. + * Deals with the structures on the WWN API, turning them into a thing in openHAB. * * @author David Bennett - Initial contribution * @author Wouter Born - Handle channel refresh command */ @NonNullByDefault -public class NestStructureHandler extends NestBaseHandler { - private final Logger logger = LoggerFactory.getLogger(NestStructureHandler.class); +public class WWNStructureHandler extends WWNBaseHandler { + private final Logger logger = LoggerFactory.getLogger(WWNStructureHandler.class); private @Nullable String structureId; - public NestStructureHandler(Thing thing) { - super(thing, Structure.class); + public WWNStructureHandler(Thing thing) { + super(thing, WWNStructure.class); } @Override - protected State getChannelState(ChannelUID channelUID, Structure structure) { + protected State getChannelState(ChannelUID channelUID, WWNStructure structure) { switch (channelUID.getId()) { case CHANNEL_AWAY: return getAsStringTypeOrNull(structure.getAway()); @@ -85,7 +85,7 @@ public String getId() { private String getStructureId() { String localStructureId = structureId; if (localStructureId == null) { - localStructureId = getConfigAs(NestStructureConfiguration.class).structureId; + localStructureId = getConfigAs(WWNStructureConfiguration.class).structureId; structureId = localStructureId; } return localStructureId; @@ -101,7 +101,7 @@ private String getStructureId() { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (REFRESH.equals(command)) { - Structure lastUpdate = getLastUpdate(); + WWNStructure lastUpdate = getLastUpdate(); if (lastUpdate != null) { updateState(channelUID, getChannelState(channelUID, lastUpdate)); } @@ -116,7 +116,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - protected void update(Structure oldStructure, Structure structure) { + protected void update(@Nullable WWNStructure oldStructure, WWNStructure structure) { logger.debug("Updating {}", getThing().getUID()); updateLinkedChannels(oldStructure, structure); diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestThermostatHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandler.java similarity index 91% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestThermostatHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandler.java index 41d9b9470e51d..6951ec0c2ac86 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/handler/NestThermostatHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandler.java @@ -10,9 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.handler; +package org.openhab.binding.nest.internal.wwn.handler; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION; import static org.openhab.core.types.RefreshType.REFRESH; @@ -26,8 +26,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.data.Thermostat; -import org.openhab.binding.nest.internal.data.Thermostat.Mode; +import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat; +import org.openhab.binding.nest.internal.wwn.dto.WWNThermostat.Mode; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -42,22 +42,22 @@ import org.slf4j.LoggerFactory; /** - * The {@link NestThermostatHandler} is responsible for handling commands, which are + * The {@link WWNThermostatHandler} is responsible for handling commands, which are * sent to one of the channels for the thermostat. * * @author David Bennett - Initial contribution * @author Wouter Born - Handle channel refresh command */ @NonNullByDefault -public class NestThermostatHandler extends NestBaseHandler { - private final Logger logger = LoggerFactory.getLogger(NestThermostatHandler.class); +public class WWNThermostatHandler extends WWNBaseHandler { + private final Logger logger = LoggerFactory.getLogger(WWNThermostatHandler.class); - public NestThermostatHandler(Thing thing) { - super(thing, Thermostat.class); + public WWNThermostatHandler(Thing thing) { + super(thing, WWNThermostat.class); } @Override - protected State getChannelState(ChannelUID channelUID, Thermostat thermostat) { + protected State getChannelState(ChannelUID channelUID, WWNThermostat thermostat) { switch (channelUID.getId()) { case CHANNEL_CAN_COOL: return getAsOnOffTypeOrNull(thermostat.isCanCool()); @@ -125,7 +125,7 @@ protected State getChannelState(ChannelUID channelUID, Thermostat thermostat) { @SuppressWarnings("unchecked") public void handleCommand(ChannelUID channelUID, Command command) { if (REFRESH.equals(command)) { - Thermostat lastUpdate = getLastUpdate(); + WWNThermostat lastUpdate = getLastUpdate(); if (lastUpdate != null) { updateState(channelUID, getChannelState(channelUID, lastUpdate)); } @@ -182,7 +182,7 @@ private void addTemperatureUpdateRequest(String celsiusField, String fahrenheitF } private Unit getTemperatureUnit(Unit fallbackUnit) { - Thermostat lastUpdate = getLastUpdate(); + WWNThermostat lastUpdate = getLastUpdate(); if (lastUpdate != null && lastUpdate.getTemperatureUnit() != null) { return lastUpdate.getTemperatureUnit(); } @@ -204,7 +204,7 @@ private Unit getTemperatureUnit(Unit fallbackUnit) { } @Override - protected void update(Thermostat oldThermostat, Thermostat thermostat) { + protected void update(@Nullable WWNThermostat oldThermostat, WWNThermostat thermostat) { logger.debug("Updating {}", getThing().getUID()); updateLinkedChannels(oldThermostat, thermostat); diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestStreamingDataListener.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNStreamingDataListener.java similarity index 69% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestStreamingDataListener.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNStreamingDataListener.java index cbfe7960a79b8..390d6a91f7b0f 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestStreamingDataListener.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNStreamingDataListener.java @@ -10,20 +10,20 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.listener; +package org.openhab.binding.nest.internal.wwn.listener; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nest.internal.data.TopLevelData; -import org.openhab.binding.nest.internal.rest.NestStreamingRestClient; +import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData; +import org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient; /** - * Interface for listeners of events generated by the {@link NestStreamingRestClient}. + * Interface for listeners of events generated by the {@link WWNStreamingRestClient}. * * @author Wouter Born - Initial contribution * @author Wouter Born - Replace polling with REST streaming */ @NonNullByDefault -public interface NestStreamingDataListener { +public interface WWNStreamingDataListener { /** * Authorization has been revoked for a token. @@ -46,7 +46,7 @@ public interface NestStreamingDataListener { void onError(String message); /** - * Initial {@link TopLevelData} or an update is sent. + * Initial {@link WWNTopLevelData} or an update is sent. */ - void onNewTopLevelData(TopLevelData data); + void onNewTopLevelData(WWNTopLevelData data); } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestThingDataListener.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNThingDataListener.java similarity index 88% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestThingDataListener.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNThingDataListener.java index 9ec8eb4b5346b..4a3455d660354 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/listener/NestThingDataListener.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/listener/WWNThingDataListener.java @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.listener; +package org.openhab.binding.nest.internal.wwn.listener; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Used to track incoming data for Nest things. + * Used to track incoming data for WWN things. * * @author Wouter Born - Initial contribution */ @NonNullByDefault -public interface NestThingDataListener { +public interface WWNThingDataListener { /** * An initial value for the data was received or the value is send again due to a refresh. diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestAuthorizer.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNAuthorizer.java similarity index 51% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestAuthorizer.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNAuthorizer.java index a0dd8d8f4cd89..0273f5d78e7ce 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestAuthorizer.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNAuthorizer.java @@ -10,31 +10,31 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.rest; +package org.openhab.binding.nest.internal.wwn.rest; import java.io.IOException; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nest.internal.NestBindingConstants; -import org.openhab.binding.nest.internal.NestUtils; -import org.openhab.binding.nest.internal.config.NestBridgeConfiguration; -import org.openhab.binding.nest.internal.data.AccessTokenData; -import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException; +import org.openhab.binding.nest.internal.wwn.WWNBindingConstants; +import org.openhab.binding.nest.internal.wwn.WWNUtils; +import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration; +import org.openhab.binding.nest.internal.wwn.dto.WWNAccessTokenData; +import org.openhab.binding.nest.internal.wwn.exceptions.InvalidWWNAccessTokenException; import org.openhab.core.io.net.http.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Retrieves the Nest access token using the OAuth 2.0 protocol using pin-based authorization. + * Retrieves the WWN access token using the OAuth 2.0 protocol using pin-based authorization. * * @author David Bennett - Initial contribution * @author Wouter Born - Improve exception handling */ @NonNullByDefault -public class NestAuthorizer { - private final Logger logger = LoggerFactory.getLogger(NestAuthorizer.class); +public class WWNAuthorizer { + private final Logger logger = LoggerFactory.getLogger(WWNAuthorizer.class); - private final NestBridgeConfiguration config; + private final WWNAccountConfiguration config; /** * Create the helper class for the Nest access token. Also creates the folder @@ -42,48 +42,46 @@ public class NestAuthorizer { * * @param config The configuration to use for the token */ - public NestAuthorizer(NestBridgeConfiguration config) { + public WWNAuthorizer(WWNAccountConfiguration config) { this.config = config; } /** * Get the current access token, refreshing if needed. * - * @throws InvalidAccessTokenException thrown when the access token is invalid and could not be refreshed + * @throws InvalidWWNAccessTokenException thrown when the access token is invalid and could not be refreshed */ - public String getNewAccessToken() throws InvalidAccessTokenException { + public String getNewAccessToken() throws InvalidWWNAccessTokenException { try { String pincode = config.pincode; if (pincode == null || pincode.isBlank()) { - throw new InvalidAccessTokenException("Pincode is empty"); + throw new InvalidWWNAccessTokenException("Pincode is empty"); } - // @formatter:off - StringBuilder urlBuilder = new StringBuilder(NestBindingConstants.NEST_ACCESS_TOKEN_URL) - .append("?client_id=") - .append(config.productId) - .append("&client_secret=") - .append(config.productSecret) - .append("&code=") - .append(pincode) + StringBuilder urlBuilder = new StringBuilder(WWNBindingConstants.NEST_ACCESS_TOKEN_URL) // + .append("?client_id=") // + .append(config.productId) // + .append("&client_secret=") // + .append(config.productSecret) // + .append("&code=") // + .append(pincode) // .append("&grant_type=authorization_code"); - // @formatter:on logger.debug("Requesting access token from URL: {}", urlBuilder); String responseContentAsString = HttpUtil.executeUrl("POST", urlBuilder.toString(), null, null, "application/x-www-form-urlencoded", 10_000); - AccessTokenData data = NestUtils.fromJson(responseContentAsString, AccessTokenData.class); + WWNAccessTokenData data = WWNUtils.fromJson(responseContentAsString, WWNAccessTokenData.class); logger.debug("Received: {}", data); String accessToken = data.getAccessToken(); if (accessToken == null || accessToken.isBlank()) { - throw new InvalidAccessTokenException("Pincode to obtain access token is already used or invalid)"); + throw new InvalidWWNAccessTokenException("Pincode to obtain access token is already used or invalid)"); } return accessToken; } catch (IOException e) { - throw new InvalidAccessTokenException("Access token request failed", e); + throw new InvalidWWNAccessTokenException("Access token request failed", e); } } } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRequestFilter.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRequestFilter.java similarity index 86% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRequestFilter.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRequestFilter.java index b89b97b072b04..83ee467a6a270 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRequestFilter.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRequestFilter.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.rest; +package org.openhab.binding.nest.internal.wwn.rest; import java.io.IOException; @@ -23,16 +23,16 @@ import org.eclipse.jdt.annotation.Nullable; /** - * Inserts Authorization and Cache-Control headers for requests on the streaming REST API. + * Inserts Authorization and Cache-Control headers for requests on the streaming WWN REST API. * * @author Wouter Born - Initial contribution * @author Wouter Born - Replace polling with REST streaming */ @NonNullByDefault -public class NestStreamingRequestFilter implements ClientRequestFilter { +public class WWNStreamingRequestFilter implements ClientRequestFilter { private final String accessToken; - public NestStreamingRequestFilter(String accessToken) { + public WWNStreamingRequestFilter(String accessToken) { this.accessToken = accessToken; } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRestClient.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRestClient.java similarity index 82% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRestClient.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRestClient.java index 3f39b33fd7a49..50da59a761e81 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/rest/NestStreamingRestClient.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/rest/WWNStreamingRestClient.java @@ -10,9 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.rest; +package org.openhab.binding.nest.internal.wwn.rest; -import static org.openhab.binding.nest.internal.NestBindingConstants.KEEP_ALIVE_MILLIS; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.KEEP_ALIVE_MILLIS; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -27,24 +27,24 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.NestUtils; -import org.openhab.binding.nest.internal.data.TopLevelData; -import org.openhab.binding.nest.internal.data.TopLevelStreamingData; -import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException; -import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier; -import org.openhab.binding.nest.internal.listener.NestStreamingDataListener; +import org.openhab.binding.nest.internal.wwn.WWNUtils; +import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData; +import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelStreamingData; +import org.openhab.binding.nest.internal.wwn.exceptions.FailedResolvingWWNUrlException; +import org.openhab.binding.nest.internal.wwn.handler.WWNRedirectUrlSupplier; +import org.openhab.binding.nest.internal.wwn.listener.WWNStreamingDataListener; import org.osgi.service.jaxrs.client.SseEventSourceFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A client that generates events based on Nest streaming REST API Server-Sent Events (SSE). + * A client that generates events based on Nest streaming WWN REST API Server-Sent Events (SSE). * * @author Wouter Born - Initial contribution * @author Wouter Born - Replace polling with REST streaming */ @NonNullByDefault -public class NestStreamingRestClient { +public class WWNStreamingRestClient { // Assume connection timeout when 2 keep alive message should have been received private static final long CONNECTION_TIMEOUT_MILLIS = 2 * KEEP_ALIVE_MILLIS + KEEP_ALIVE_MILLIS / 2; @@ -55,25 +55,25 @@ public class NestStreamingRestClient { public static final String OPEN = "open"; public static final String PUT = "put"; - private final Logger logger = LoggerFactory.getLogger(NestStreamingRestClient.class); + private final Logger logger = LoggerFactory.getLogger(WWNStreamingRestClient.class); private final String accessToken; private final ClientBuilder clientBuilder; private final SseEventSourceFactory eventSourceFactory; - private final NestRedirectUrlSupplier redirectUrlSupplier; + private final WWNRedirectUrlSupplier redirectUrlSupplier; private final ScheduledExecutorService scheduler; private final Object startStopLock = new Object(); - private final List listeners = new CopyOnWriteArrayList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); private @Nullable ScheduledFuture checkConnectionJob; private boolean connected; private @Nullable SseEventSource eventSource; private long lastEventTimestamp; - private @Nullable TopLevelData lastReceivedTopLevelData; + private @Nullable WWNTopLevelData lastReceivedTopLevelData; - public NestStreamingRestClient(String accessToken, ClientBuilder clientBuilder, - SseEventSourceFactory eventSourceFactory, NestRedirectUrlSupplier redirectUrlSupplier, + public WWNStreamingRestClient(String accessToken, ClientBuilder clientBuilder, + SseEventSourceFactory eventSourceFactory, WWNRedirectUrlSupplier redirectUrlSupplier, ScheduledExecutorService scheduler) { this.accessToken = accessToken; this.clientBuilder = clientBuilder; @@ -82,8 +82,8 @@ public NestStreamingRestClient(String accessToken, ClientBuilder clientBuilder, this.scheduler = scheduler; } - private SseEventSource createEventSource() throws FailedResolvingNestUrlException { - Client client = clientBuilder.register(new NestStreamingRequestFilter(accessToken)).build(); + private SseEventSource createEventSource() throws FailedResolvingWWNUrlException { + Client client = clientBuilder.register(new WWNStreamingRequestFilter(accessToken)).build(); SseEventSource eventSource = eventSourceFactory.newSource(client.target(redirectUrlSupplier.getRedirectUrl())); eventSource.register(this::onEvent, this::onError); return eventSource; @@ -122,7 +122,7 @@ private void reopenEventSource() { localEventSource.open(); eventSource = localEventSource; - } catch (FailedResolvingNestUrlException e) { + } catch (FailedResolvingWWNUrlException e) { logger.debug("Failed to resolve Nest redirect URL while opening new EventSource"); } } @@ -175,15 +175,15 @@ private void stopCheckConnectionJob(boolean mayInterruptIfRunning) { } } - public boolean addStreamingDataListener(NestStreamingDataListener listener) { + public boolean addStreamingDataListener(WWNStreamingDataListener listener) { return listeners.add(listener); } - public boolean removeStreamingDataListener(NestStreamingDataListener listener) { + public boolean removeStreamingDataListener(WWNStreamingDataListener listener) { return listeners.remove(listener); } - public @Nullable TopLevelData getLastReceivedTopLevelData() { + public @Nullable WWNTopLevelData getLastReceivedTopLevelData() { return lastReceivedTopLevelData; } @@ -214,7 +214,7 @@ private void onEvent(InboundSseEvent inboundEvent) { logger.debug("Event stream opened"); } else if (PUT.equals(name)) { logger.debug("Data has changed (or initial data sent)"); - TopLevelData topLevelData = NestUtils.fromJson(data, TopLevelStreamingData.class).getData(); + WWNTopLevelData topLevelData = WWNUtils.fromJson(data, WWNTopLevelStreamingData.class).getData(); lastReceivedTopLevelData = topLevelData; listeners.forEach(listener -> listener.onNewTopLevelData(topLevelData)); } else { diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestCompositeUpdateHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNCompositeUpdateHandler.java similarity index 69% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestCompositeUpdateHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNCompositeUpdateHandler.java index eacd62329963f..0dd86e696a0d2 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestCompositeUpdateHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNCompositeUpdateHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.update; +package org.openhab.binding.nest.internal.wwn.update; import java.util.HashSet; import java.util.List; @@ -20,36 +20,37 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.data.NestIdentifiable; -import org.openhab.binding.nest.internal.data.TopLevelData; -import org.openhab.binding.nest.internal.listener.NestThingDataListener; +import org.openhab.binding.nest.internal.wwn.dto.WWNIdentifiable; +import org.openhab.binding.nest.internal.wwn.dto.WWNTopLevelData; +import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener; /** - * Handles all Nest data updates through delegation to the {@link NestUpdateHandler} for the respective data type. + * Handles all Nest data updates through delegation to the {@link WWNUpdateHandler} for the respective data type. * * @author Wouter Born - Initial contribution */ @NonNullByDefault -public class NestCompositeUpdateHandler { +public class WWNCompositeUpdateHandler { private final Supplier> presentNestIdsSupplier; - private final Map, NestUpdateHandler> updateHandlersMap = new ConcurrentHashMap<>(); + private final Map, WWNUpdateHandler> updateHandlersMap = new ConcurrentHashMap<>(); - public NestCompositeUpdateHandler(Supplier> presentNestIdsSupplier) { + public WWNCompositeUpdateHandler(Supplier> presentNestIdsSupplier) { this.presentNestIdsSupplier = presentNestIdsSupplier; } - public boolean addListener(Class dataClass, NestThingDataListener listener) { + public boolean addListener(Class dataClass, WWNThingDataListener listener) { return getOrCreateUpdateHandler(dataClass).addListener(listener); } - public boolean addListener(Class dataClass, String nestId, NestThingDataListener listener) { + public boolean addListener(Class dataClass, String nestId, WWNThingDataListener listener) { return getOrCreateUpdateHandler(dataClass).addListener(nestId, listener); } - private Set findMissingNestIds(Set updates) { + private Set findMissingNestIds(Set updates) { Set nestIds = updates.stream().map(u -> u.getId()).collect(Collectors.toSet()); Set missingNestIds = presentNestIdsSupplier.get(); missingNestIds.removeAll(nestIds); @@ -64,8 +65,8 @@ public List getLastUpdates(Class dataClass) { return getOrCreateUpdateHandler(dataClass).getLastUpdates(); } - private Set getNestUpdates(TopLevelData data) { - Set updates = new HashSet<>(); + private Set getNestUpdates(WWNTopLevelData data) { + Set updates = new HashSet<>(); if (data.getDevices() != null) { if (data.getDevices().getCameras() != null) { updates.addAll(data.getDevices().getCameras().values()); @@ -84,20 +85,20 @@ private Set getNestUpdates(TopLevelData data) { } @SuppressWarnings("unchecked") - private NestUpdateHandler getOrCreateUpdateHandler(Class dataClass) { - NestUpdateHandler handler = (NestUpdateHandler) updateHandlersMap.get(dataClass); + private <@NonNull T> WWNUpdateHandler getOrCreateUpdateHandler(Class dataClass) { + WWNUpdateHandler handler = (WWNUpdateHandler) updateHandlersMap.get(dataClass); if (handler == null) { - handler = new NestUpdateHandler<>(); + handler = new WWNUpdateHandler<>(); updateHandlersMap.put(dataClass, handler); } return handler; } @SuppressWarnings("unchecked") - public void handleUpdate(TopLevelData data) { - Set updates = getNestUpdates(data); + public void handleUpdate(WWNTopLevelData data) { + Set updates = getNestUpdates(data); updates.forEach(update -> { - Class updateClass = (Class) update.getClass(); + Class updateClass = (Class) update.getClass(); getOrCreateUpdateHandler(updateClass).handleUpdate(updateClass, update.getId(), update); }); @@ -109,11 +110,11 @@ public void handleUpdate(TopLevelData data) { } } - public boolean removeListener(Class dataClass, NestThingDataListener listener) { + public boolean removeListener(Class dataClass, WWNThingDataListener listener) { return getOrCreateUpdateHandler(dataClass).removeListener(listener); } - public boolean removeListener(Class dataClass, String nestId, NestThingDataListener listener) { + public boolean removeListener(Class dataClass, String nestId, WWNThingDataListener listener) { return getOrCreateUpdateHandler(dataClass).removeListener(nestId, listener); } diff --git a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestUpdateHandler.java b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNUpdateHandler.java similarity index 70% rename from bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestUpdateHandler.java rename to bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNUpdateHandler.java index 9f78942711318..53f3acc71a616 100644 --- a/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/update/NestUpdateHandler.java +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/wwn/update/WWNUpdateHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.update; +package org.openhab.binding.nest.internal.wwn.update; import java.util.ArrayList; import java.util.HashSet; @@ -20,9 +20,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.nest.internal.listener.NestThingDataListener; +import org.openhab.binding.nest.internal.wwn.listener.WWNThingDataListener; /** * Handles the updates of one type of data by notifying listeners of changes and storing the update value. @@ -32,7 +33,7 @@ * @param the type of update data */ @NonNullByDefault -public class NestUpdateHandler { +public class WWNUpdateHandler<@NonNull T> { /** * The ID used for listeners that subscribe to any Nest update. @@ -40,13 +41,13 @@ public class NestUpdateHandler { private static final String ANY_ID = "*"; private final Map lastUpdates = new ConcurrentHashMap<>(); - private final Map>> listenersMap = new ConcurrentHashMap<>(); + private final Map>> listenersMap = new ConcurrentHashMap<>(); - public boolean addListener(NestThingDataListener listener) { + public boolean addListener(WWNThingDataListener listener) { return addListener(ANY_ID, listener); } - public boolean addListener(String nestId, NestThingDataListener listener) { + public boolean addListener(String nestId, WWNThingDataListener listener) { return getOrCreateListeners(nestId).add(listener); } @@ -58,21 +59,21 @@ public List getLastUpdates() { return new ArrayList<>(lastUpdates.values()); } - private Set> getListeners(String nestId) { - Set> listeners = new HashSet<>(); - Set> idListeners = listenersMap.get(nestId); + private Set> getListeners(String nestId) { + Set> listeners = new HashSet<>(); + Set> idListeners = listenersMap.get(nestId); if (idListeners != null) { listeners.addAll(idListeners); } - Set> anyListeners = listenersMap.get(ANY_ID); + Set> anyListeners = listenersMap.get(ANY_ID); if (anyListeners != null) { listeners.addAll(anyListeners); } return listeners; } - private Set> getOrCreateListeners(String nestId) { - Set> listeners = listenersMap.get(nestId); + private Set> getOrCreateListeners(String nestId) { + Set> listeners = listenersMap.get(nestId); if (listeners == null) { listeners = new CopyOnWriteArraySet<>(); listenersMap.put(nestId, listeners); @@ -88,13 +89,13 @@ public void handleMissingNestIds(Set nestIds) { } public void handleUpdate(Class dataClass, String nestId, T update) { - T lastUpdate = getLastUpdate(nestId); + final @Nullable T lastUpdate = getLastUpdate(nestId); lastUpdates.put(nestId, update); notifyListeners(nestId, lastUpdate, update); } private void notifyListeners(String nestId, @Nullable T lastUpdate, T update) { - Set> listeners = getListeners(nestId); + Set> listeners = getListeners(nestId); if (lastUpdate == null) { listeners.forEach(l -> l.onNewData(update)); } else if (!lastUpdate.equals(update)) { @@ -102,11 +103,11 @@ private void notifyListeners(String nestId, @Nullable T lastUpdate, T update) { } } - public boolean removeListener(NestThingDataListener listener) { + public boolean removeListener(WWNThingDataListener listener) { return removeListener(ANY_ID, listener); } - public boolean removeListener(String nestId, NestThingDataListener listener) { + public boolean removeListener(String nestId, WWNThingDataListener listener) { return getOrCreateListeners(nestId).remove(listener); } diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/sdm-config.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/sdm-config.xml new file mode 100644 index 0000000000000..052c48ba73ea7 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/sdm-config.xml @@ -0,0 +1,96 @@ + + + + + + + The parameters used when communicating with the SDM API + + + + The parameters used when communicating with the Pub/Sub API + + + + + The UUID that identifies the SDM project in the SDM "Device Access Console" + + + + Identifies the OAuth 2.0 client used for accessing the SDM project + + + password + + The OAuth 2.0 client secret used for accessing the SDM project + + + + This is the one time authorization code used to retrieve the refresh and access token used with the SDM + API + + + + + Identifies the Google Cloud Platform project where the Pub/Sub subscription is created + + + + Identifies the subscription that is created for subscribing to SDM Pub/Sub events + + + + Identifies the OAuth 2.0 client used for accessing the Pub/Sub subscription + + + password + + The OAuth 2.0 client secret used for accessing the Pub/Sub subscription + + + + This is the one time authorization code used to retrieve the refresh and access token used with the + Pub/Sub API + + + + + + + + + + This is refresh interval in seconds to update the Nest device information + 300 + s + + + + + + + The width in pixels used for generating event images. A default value of 480 pixels is used if not + configured. + px + + + + The height in pixels used for generating event images. This parameter is ignored when the image width + parameter is also configured. + px + + + + + + + Specifies the length of time in seconds that the timer is set to run. + 900 + s + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/wwn-config.xml similarity index 57% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/wwn-config.xml index 1a406a5880ed3..37554c07f6f7d 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/config/wwn-config.xml @@ -4,14 +4,10 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + - - The OAuth parameters used when communicating with the Nest API - - - - Local settings + + The OAuth parameters used when communicating with the WWN API @@ -22,30 +18,19 @@ The product secret from the Nest product page - - - 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 - - + - 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 - true + The access token used for authenticating to the WWN API - + - + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-account.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-account.xml new file mode 100644 index 0000000000000..5ea34c073b5ed --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-account.xml @@ -0,0 +1,13 @@ + + + + + + An account for using the Smart Device Management (SDM) API + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-camera.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-camera.xml new file mode 100644 index 0000000000000..0c73064a67c38 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-camera.xml @@ -0,0 +1,27 @@ + + + + + + + + + + A Nest Camera registered with your SDM account + + + + + + + + + deviceId + + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-channels.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-channels.xml new file mode 100644 index 0000000000000..196a7c8f4d8c5 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-channels.xml @@ -0,0 +1,231 @@ + + + + + + + Information about the last chime event + + + + Static image based on a chime event + + + + The last time that the door chime was pressed + + + + + + + Information about the last motion event + + + + Static image based on a motion event + + + + The last time that motion was detected + + + + + + + Information about the last person event + + + + Static image based on a person event + + + + The last time that a person was detected + + + + + + + Information about the last sound event + + + + Static image based on a sound event + + + + The last time that a sound was detected + + + + + + Image + + Static image based on a event + + + + + + DateTime + + The time that the event occurred + + + + + + Information for accessing the live stream + + + + + + + + + + String + + The RTSP video stream URL for the most recent event + + + + + DateTime + + Live stream token expiration time + + + + + String + + Live stream current token value + + + + + String + + Live stream token extension value + + + + + + Number:Dimensionless + + Lists the current ambient humidity percentage from the thermostat + Humidity + + + + + Number:Temperature + + Lists the current ambient temperature from the thermostat + Temperature + + + + + String + + Lists the current eco mode from the thermostat + + + + + + + + + + String + + Lists the current mode from the thermostat + + + + + + + + + + + + Switch + + Lists the current fan timer mode + + + + + DateTime + + Timestamp at which timer mode turns OFF + + + + String + + Provides the thermostat HVAC Status + + + + + + + + + + + Number:Temperature + + Lists the maximum temperature setting from the thermostat + Temperature + + + + + Number:Temperature + + Lists the minimum temperature setting from the thermostat + Temperature + + + + + Number:Temperature + + Lists the target temperature setting from the thermostat + Temperature + + + + + Number:Temperature + + Lists the cool temperature setting from the thermostat + Temperature + + + + + Number:Temperature + + Lists the heat temperature setting from the thermostat + Temperature + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-display.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-display.xml new file mode 100644 index 0000000000000..582c6807443a7 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-display.xml @@ -0,0 +1,27 @@ + + + + + + + + + + A Nest Display registered with your SDM account + + + + + + + + + deviceId + + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-doorbell.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-doorbell.xml new file mode 100644 index 0000000000000..c8c5647370ee7 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-doorbell.xml @@ -0,0 +1,28 @@ + + + + + + + + + + A Nest Doorbell registered with your SDM account + + + + + + + + + + deviceId + + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-thermostat.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-thermostat.xml new file mode 100644 index 0000000000000..7d549e16747f2 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/sdm-thermostat.xml @@ -0,0 +1,35 @@ + + + + + + + + + + A Thermostat to control the various aspects of the house's HVAC system + + + + + + + + + + + + + + + + + deviceId + + + + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/thermostat.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/thermostat.xml deleted file mode 100644 index 816e2b3591591..0000000000000 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/thermostat.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - A Thermostat to control the various aspects of the house's HVAC system - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Nest - - - deviceId - - - - diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-account.xml similarity index 64% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/bridge.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-account.xml index 60e6e60450793..4f98c2179fbcb 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-account.xml @@ -4,9 +4,10 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - - An account for using the Nest REST API - + + + An account for using the Works with Nest (WWN) API + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/camera.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-camera.xml similarity index 70% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/camera.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-camera.xml index 67c11800114a2..02f24b34fcc99 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/camera.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-camera.xml @@ -4,17 +4,17 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + - A Nest Cam registered with your account + A Nest Camera registered with your WWN account - - + + Information about the last camera event (requires Nest Aware subscription) @@ -26,6 +26,7 @@ deviceId - + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-channels.xml similarity index 75% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/channels.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-channels.xml index b3d0346ddcbbc..9ed5ec5ab1303 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-channels.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + DateTime Timestamp of the last successful interaction with Nest @@ -13,7 +13,7 @@ - + String Away state of the structure @@ -25,39 +25,39 @@ - + String Country code of the structure - + String Postal code of the structure - + String The time zone for the structure - + DateTime Peak period start for the Rush Hour Rewards program - + DateTime Peak period end for the Rush Hour Rewards program - + DateTime @@ -66,14 +66,14 @@ - + Switch If rush hour rewards system is enabled or not - + String Security state of the structure @@ -86,166 +86,166 @@ - + Information about the camera - - - - - - - - - + + + + + + + + + - + Switch If the audio input is enabled for this camera - + Switch If the video history is enabled for this camera - + Switch If the public sharing of this camera is enabled - + Switch If the camera is currently streaming - + String The web URL for the camera, allows you to see the camera in a web page - + String The publicly available URL for the camera - + String The URL showing a snapshot of the camera - + String The app URL for the camera, allows you to see the camera in an app - + DateTime Timestamp of the last online status change - + Information about the camera event - - - - - - - - - - - + + + + + + + + + + + - + Switch If sound was detected in the camera event - + Switch If motion was detected in the camera event - + Switch If a person was detected in the camera event - + DateTime Timestamp when the camera event started - + DateTime Timestamp when the camera event ended - + DateTime Timestamp when the camera event URLs expire - + String The web URL for the camera event, allows you to see the camera event in a web page - + String The app URL for the camera event, allows you to see the camera event in an app - + String The URL showing an image for the camera event - + String The URL showing an animated image for the camera event - + String Identifiers for activity zones that detected the event (comma separated) @@ -253,7 +253,7 @@ - + String Current color state of the protect @@ -267,7 +267,7 @@ - + String Carbon monoxide alarm state @@ -280,7 +280,7 @@ - + String Smoke alarm state @@ -293,14 +293,14 @@ - + Switch If the manual test is currently active - + DateTime Timestamp of the last successful manual test @@ -308,7 +308,7 @@ - + Number:Temperature Current temperature @@ -316,7 +316,7 @@ - + Number:Temperature The set point temperature @@ -324,7 +324,7 @@ - + Number:Temperature The max set point temperature @@ -332,7 +332,7 @@ - + Number:Temperature The min set point temperature @@ -340,7 +340,7 @@ - + Number:Temperature The eco range max set point temperature @@ -348,7 +348,7 @@ - + Number:Temperature The eco range min set point temperature @@ -356,7 +356,7 @@ - + Number:Temperature The locked range max set point temperature @@ -364,7 +364,7 @@ - + Number:Temperature The locked range min set point temperature @@ -372,14 +372,14 @@ - + Switch If the thermostat has the temperature locked to only be within a set range - + String Current mode of the Nest thermostat @@ -394,7 +394,7 @@ - + String The previous mode of the Nest thermostat @@ -409,7 +409,7 @@ - + String The active state of the Nest thermostat @@ -422,7 +422,7 @@ - + Number:Dimensionless Indicates the current relative humidity @@ -430,35 +430,35 @@ - + Number:Time Time left to the target temperature approximately - + Switch If the thermostat can actually turn on heating - + Switch If the thermostat can actually turn on cooling - + Switch If the fan timer is engaged - + Number:Time Length of time that the fan is set to run @@ -476,45 +476,46 @@ - + DateTime Timestamp when the fan stops running - + Switch If the thermostat can control the fan - + Switch If the thermostat is currently in a leaf mode - + Switch If sunlight correction is enabled - + Switch If sunlight correction is active - + Switch If the system is currently using emergency heat + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/smoke-detector.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-smoke-detector.xml similarity index 59% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/smoke-detector.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-smoke-detector.xml index d1fc874898931..e5730dc68de29 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/smoke-detector.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-smoke-detector.xml @@ -4,22 +4,22 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + The smoke detector/Nest Protect for the account - + - - - - - + + + + + @@ -28,6 +28,7 @@ deviceId - + + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/structure.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-structure.xml similarity index 50% rename from bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/structure.xml rename to bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-structure.xml index 242ea0c316f01..c594e6a690a6d 100644 --- a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/structure.xml +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-structure.xml @@ -4,9 +4,9 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + @@ -15,17 +15,17 @@ structure if you have more than one house - - - - - - - - - - - + + + + + + + + + + + @@ -34,7 +34,7 @@ structureId - + diff --git a/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-thermostat.xml b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-thermostat.xml new file mode 100644 index 0000000000000..99ffeeed9e55e --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/resources/OH-INF/thing/wwn-thermostat.xml @@ -0,0 +1,52 @@ + + + + + + + + + + A Thermostat to control the various aspects of the house's HVAC system + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nest + + + deviceId + + + + + diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponsesTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponsesTest.java new file mode 100644 index 0000000000000..3ffbe42418c44 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/PubSubRequestsResponsesTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.*; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubAcknowledgeRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubCreateRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullRequest; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubPullResponse; +import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubReceivedMessage; + +/** + * Tests (de)serialization of {@link + * org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses} from/to JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class PubSubRequestsResponsesTest { + + @Test + public void deserializePullSubscriptionResponse() throws IOException { + PubSubPullResponse response = fromJson("pull-subscription-response.json", PubSubPullResponse.class); + assertThat(response, is(notNullValue())); + + List receivedMessages = response.receivedMessages; + assertThat(receivedMessages, is(notNullValue())); + assertThat(receivedMessages, hasSize(3)); + + PubSubReceivedMessage receivedMessage = receivedMessages.get(0); + assertThat(receivedMessage, is(notNullValue())); + assertThat(receivedMessage.ackId, is("AID1")); + PubSubMessage message = receivedMessage.message; + assertThat(message, is(notNullValue())); + assertThat(message.data, is("ZGF0YTE=")); + assertThat(message.messageId, is("1000000000000001")); + assertThat(message.publishTime, is(ZonedDateTime.parse("2021-01-01T01:00:00.000Z"))); + + receivedMessage = receivedMessages.get(1); + assertThat(receivedMessage, is(notNullValue())); + assertThat(receivedMessage.ackId, is("AID2")); + message = receivedMessage.message; + assertThat(message, is(notNullValue())); + assertThat(message.data, is("ZGF0YTI=")); + assertThat(message.messageId, is("2000000000000002")); + assertThat(message.publishTime, is(ZonedDateTime.parse("2021-02-02T02:00:00.000Z"))); + + receivedMessage = receivedMessages.get(2); + assertThat(receivedMessage, is(notNullValue())); + assertThat(receivedMessage.ackId, is("AID3")); + message = receivedMessage.message; + assertThat(message, is(notNullValue())); + assertThat(message.data, is("ZGF0YTM=")); + assertThat(message.messageId, is("3000000000000003")); + assertThat(message.publishTime, is(ZonedDateTime.parse("2021-03-03T03:00:00.000Z"))); + } + + @Test + public void serializeAcknowledgeSubscriptionRequest() throws IOException { + String json = toJson(new PubSubAcknowledgeRequest(List.of("AID1", "AID2", "AID3"))); + assertThat(json, is(fromFile("acknowledge-subscription-request.json"))); + } + + @Test + public void serializeCreateSubscriptionRequest() throws IOException { + String json = toJson(new PubSubCreateRequest("projects/sdm-prod/topics/enterprise-project-id", true)); + assertThat(json, is(fromFile("create-subscription-request.json"))); + } + + @Test + public void serializePullSubscriptionRequest() throws IOException { + String json = toJson(new PubSubPullRequest(123)); + assertThat(json, is(fromFile("pull-subscription-request.json"))); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommandsTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommandsTest.java new file mode 100644 index 0000000000000..dfefc20ece547 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMCommandsTest.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.*; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.Duration; +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCameraRtspStreamUrls; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMExtendCameraRtspStreamResults; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResults; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResponse; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResults; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetFanTimerRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatCoolSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatEcoModeRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatHeatSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatModeRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMSetThermostatRangeSetpointRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMStopCameraRtspStreamRequest; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode; + +/** + * Tests (de)serialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMCommands} requests + * and responses from/to JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMCommandsTest { + + @Test + public void deserializeExtendCameraRtspStreamResponse() throws IOException { + SDMExtendCameraRtspStreamResponse response = fromJson("extend-camera-rtsp-stream-response.json", + SDMExtendCameraRtspStreamResponse.class); + assertThat(response, is(notNullValue())); + + SDMExtendCameraRtspStreamResults results = response.results; + assertThat(results, is(notNullValue())); + + assertThat(results.streamExtensionToken, is("dGNUlTU2CjY5Y3VKaTZwR3o4Y1...")); + assertThat(results.streamToken, is("g.0.newStreamingToken")); + assertThat(results.expiresAt, is(ZonedDateTime.parse("2018-01-04T18:30:00.000Z"))); + } + + @Test + public void deserializeGenerateCameraImageResponse() throws IOException { + SDMGenerateCameraImageResponse response = fromJson("generate-camera-image-response.json", + SDMGenerateCameraImageResponse.class); + assertThat(response, is(notNullValue())); + + SDMGenerateCameraImageResults results = response.results; + assertThat(results, is(notNullValue())); + assertThat(results.url, is("https://domain/sdm_resource/dGNUlTU2CjY5Y3VKaTZwR3o4Y1...")); + assertThat(results.token, is("g.0.eventToken")); + } + + @Test + public void deserializeGenerateCameraRtspStreamResponse() throws IOException { + SDMGenerateCameraRtspStreamResponse response = fromJson("generate-camera-rtsp-stream-response.json", + SDMGenerateCameraRtspStreamResponse.class); + assertThat(response, is(notNullValue())); + + SDMGenerateCameraRtspStreamResults results = response.results; + assertThat(results, is(notNullValue())); + + SDMCameraRtspStreamUrls streamUrls = results.streamUrls; + assertThat(streamUrls, is(notNullValue())); + assertThat(streamUrls.rtspUrl, is("rtsps://someurl.com/CjY5Y3VKaTZwR3o4Y19YbTVfMF...?auth=g.0.streamingToken")); + + assertThat(results.streamExtensionToken, is("CjY5Y3VKaTZwR3o4Y19YbTVfMF...")); + assertThat(results.streamToken, is("g.0.streamingToken")); + assertThat(results.expiresAt, is(ZonedDateTime.parse("2018-01-04T18:30:00.000Z"))); + } + + @Test + public void serializeExtendCameraRtspStreamRequest() throws IOException { + String json = toJson(new SDMExtendCameraRtspStreamRequest("CjY5Y3VKaTZwR3o4Y19YbTVfMF...")); + assertThat(json, is(fromFile("extend-camera-rtsp-stream-request.json"))); + } + + @Test + public void serializeGenerateCameraImageRequest() throws IOException { + String json = toJson(new SDMGenerateCameraImageRequest("FWWVQVUdGNUlTU2V4MGV2aTNXV...")); + assertThat(json, is(fromFile("generate-camera-image-request.json"))); + } + + @Test + public void serializeGenerateCameraRtspStreamRequest() throws IOException { + String json = toJson(new SDMGenerateCameraRtspStreamRequest()); + assertThat(json, is(fromFile("generate-camera-rtsp-stream-request.json"))); + } + + @Test + public void serializeSetFanTimerRequestWithDuration() throws IOException { + String json = toJson(new SDMSetFanTimerRequest(SDMFanTimerMode.ON, Duration.ofSeconds(3600))); + assertThat(json, is(fromFile("set-fan-timer-request-with-duration.json"))); + } + + @Test + public void serializeSetFanTimerRequestWithoutDuration() throws IOException { + String json = toJson(new SDMSetFanTimerRequest(SDMFanTimerMode.ON)); + assertThat(json, is(fromFile("set-fan-timer-request-without-duration.json"))); + } + + @Test + public void serializeSetThermostatCoolSetpointRequest() throws IOException { + String json = toJson(new SDMSetThermostatCoolSetpointRequest(new BigDecimal("20.0"))); + assertThat(json, is(fromFile("set-thermostat-cool-setpoint-request.json"))); + } + + @Test + public void serializeSetThermostatEcoModeRequest() throws IOException { + String json = toJson(new SDMSetThermostatEcoModeRequest(SDMThermostatEcoMode.MANUAL_ECO)); + assertThat(json, is(fromFile("set-thermostat-eco-mode-request.json"))); + } + + @Test + public void serializeSetThermostatHeatSetpointRequest() throws IOException { + String json = toJson(new SDMSetThermostatHeatSetpointRequest(new BigDecimal("15.0"))); + assertThat(json, is(fromFile("set-thermostat-heat-setpoint-request.json"))); + } + + @Test + public void serializeSetThermostatModeRequest() throws IOException { + String json = toJson(new SDMSetThermostatModeRequest(SDMThermostatMode.HEATCOOL)); + assertThat(json, is(fromFile("set-thermostat-mode-request.json"))); + } + + @Test + public void serializeSetThermostatRangeSetpointRequest() throws IOException { + String json = toJson(new SDMSetThermostatRangeSetpointRequest(new BigDecimal("15.0"), new BigDecimal("20.0"))); + assertThat(json, is(fromFile("set-thermostat-range-setpoint-request.json"))); + } + + @Test + public void serializeStopCameraRtspStreamRequest() throws IOException { + String json = toJson(new SDMStopCameraRtspStreamRequest("CjY5Y3VKaTZwR3o4Y19YbTVfMF...")); + assertThat(json, is(fromFile("stop-camera-rtsp-stream-request.json"))); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDataUtil.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDataUtil.java new file mode 100644 index 0000000000000..aa552e1a57847 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDataUtil.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.stream.JsonWriter; + +/** + * Utility class for working with Nest SDM test data in unit tests. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMDataUtil { + + public static Reader openDataReader(String fileName) throws UnsupportedEncodingException, FileNotFoundException { + String packagePath = (SDMDataUtil.class.getPackage().getName()).replaceAll("\\.", "/"); + String filePath = "src/test/resources/" + packagePath + "/" + fileName; + + InputStream inputStream = new FileInputStream(filePath); + return new InputStreamReader(inputStream, "UTF-8"); + } + + public static T fromJson(String fileName, Class dataClass) throws IOException { + try (Reader reader = openDataReader(fileName)) { + return GSON.fromJson(reader, dataClass); + } + } + + public static String fromFile(String fileName) throws IOException { + try (Reader reader = openDataReader(fileName)) { + return new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n")); + } + } + + public static String toJson(Object object) { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setIndent(" "); + GSON.toJson(object, object.getClass(), jsonWriter); + return writer.toString(); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceTest.java new file mode 100644 index 0000000000000..e7ddda07e9ce6 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMDeviceTest.java @@ -0,0 +1,298 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraImageTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraLiveStreamTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityStatus; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceInfoTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTimerMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMFanTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHumidityTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHvacStatus; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMResolution; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureScale; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatEcoTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatHvacTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatMode; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatModeTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatTemperatureSetpointTrait; + +/** + * Tests deserialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMDevice}s from JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMDeviceTest { + + @Test + public void deserializeThermostatDevice() throws IOException { + SDMDevice device = getThermostatDevice(); + assertThat(device, is(notNullValue())); + + assertThat(device.name.name, is("enterprises/project-id/devices/thermostat-device-id")); + assertThat(device.type, is(SDMDeviceType.THERMOSTAT)); + + SDMTraits traits = device.traits; + assertThat(traits, is(notNullValue())); + assertThat(traits.traitList(), hasSize(10)); + + SDMDeviceInfoTrait deviceInfo = traits.deviceInfo; + assertThat(deviceInfo, is(notNullValue())); + assertThat(deviceInfo.customName, is("")); + + SDMHumidityTrait humidity = traits.humidity; + assertThat(humidity, is(notNullValue())); + assertThat(humidity.ambientHumidityPercent, is(new BigDecimal(26))); + + SDMConnectivityTrait connectivity = traits.connectivity; + assertThat(connectivity, is(notNullValue())); + assertThat(connectivity.status, is(SDMConnectivityStatus.ONLINE)); + + SDMFanTrait fan = traits.fan; + assertThat(fan, is(notNullValue())); + assertThat(fan.timerMode, is(SDMFanTimerMode.ON)); + assertThat(fan.timerTimeout, is(ZonedDateTime.parse("2019-05-10T03:22:54Z"))); + + SDMThermostatModeTrait thermostatMode = traits.thermostatMode; + assertThat(thermostatMode, is(notNullValue())); + assertThat(thermostatMode.mode, is(SDMThermostatMode.HEAT)); + assertThat(thermostatMode.availableModes, is(List.of(SDMThermostatMode.HEAT, SDMThermostatMode.OFF))); + + SDMThermostatEcoTrait thermostatEco = traits.thermostatEco; + assertThat(thermostatEco, is(notNullValue())); + assertThat(thermostatEco.availableModes, + is(List.of(SDMThermostatEcoMode.OFF, SDMThermostatEcoMode.MANUAL_ECO))); + assertThat(thermostatEco.mode, is(SDMThermostatEcoMode.OFF)); + assertThat(thermostatEco.heatCelsius, is(new BigDecimal("15.34473"))); + assertThat(thermostatEco.coolCelsius, is(new BigDecimal("24.44443"))); + + SDMThermostatHvacTrait thermostatHvac = traits.thermostatHvac; + assertThat(thermostatHvac, is(notNullValue())); + assertThat(thermostatHvac.status, is(SDMHvacStatus.OFF)); + + SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings; + assertThat(deviceSettings, is(notNullValue())); + assertThat(deviceSettings.temperatureScale, is(SDMTemperatureScale.CELSIUS)); + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = traits.thermostatTemperatureSetpoint; + assertThat(thermostatTemperatureSetpoint, is(notNullValue())); + assertThat(thermostatTemperatureSetpoint.heatCelsius, is(new BigDecimal("14.92249"))); + assertThat(thermostatTemperatureSetpoint.coolCelsius, is(nullValue())); + + SDMTemperatureTrait temperature = traits.temperature; + assertThat(temperature, is(notNullValue())); + assertThat(temperature.ambientTemperatureCelsius, is(new BigDecimal("19.73"))); + + List parentRelations = device.parentRelations; + assertThat(parentRelations, is(notNullValue())); + assertThat(parentRelations, hasSize(1)); + + assertThat(parentRelations.get(0).parent.name, + is("enterprises/project-id/structures/structure-id/rooms/thermostat-room-id")); + assertThat(parentRelations.get(0).displayName, is("Thermostat Room Name")); + } + + protected SDMDevice getThermostatDevice() throws IOException { + return fromJson("thermostat-device-response.json", SDMDevice.class); + } + + @Test + public void deserializeCameraDevice() throws IOException { + SDMDevice device = getCameraDevice(); + assertThat(device, is(notNullValue())); + + assertThat(device.name.name, is("enterprises/project-id/devices/camera-device-id")); + assertThat(device.type, is(SDMDeviceType.CAMERA)); + + SDMTraits traits = device.traits; + assertThat(traits, is(notNullValue())); + assertThat(traits.traitList(), hasSize(7)); + + SDMDeviceInfoTrait deviceInfo = traits.deviceInfo; + assertThat(deviceInfo, is(notNullValue())); + assertThat(deviceInfo.customName, is("")); + + SDMConnectivityTrait connectivity = traits.connectivity; + assertThat(connectivity, is(nullValue())); + + SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream; + assertThat(cameraLiveStream, is(notNullValue())); + + SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution; + assertThat(maxVideoResolution, is(notNullValue())); + assertThat(maxVideoResolution.width, is(640)); + assertThat(maxVideoResolution.height, is(480)); + + assertThat(cameraLiveStream.videoCodecs, is(List.of("H264"))); + assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC"))); + + SDMCameraImageTrait cameraImage = traits.cameraImage; + assertThat(cameraImage, is(notNullValue())); + + SDMResolution maxImageResolution = cameraImage.maxImageResolution; + assertThat(maxImageResolution, is(notNullValue())); + assertThat(maxImageResolution.width, is(1920)); + assertThat(maxImageResolution.height, is(1200)); + + assertThat(traits.cameraPerson, is(notNullValue())); + assertThat(traits.cameraSound, is(notNullValue())); + assertThat(traits.cameraMotion, is(notNullValue())); + assertThat(traits.cameraEventImage, is(notNullValue())); + assertThat(traits.doorbellChime, is(nullValue())); + + List parentRelations = device.parentRelations; + assertThat(parentRelations, is(notNullValue())); + assertThat(parentRelations, hasSize(1)); + + assertThat(parentRelations.get(0).parent.name, + is("enterprises/project-id/structures/structure-id/rooms/camera-room-id")); + assertThat(parentRelations.get(0).displayName, is("Camera Room Name")); + } + + protected SDMDevice getCameraDevice() throws IOException { + return fromJson("camera-device-response.json", SDMDevice.class); + } + + @Test + public void deserializeDisplayDevice() throws IOException { + SDMDevice device = getDisplayDevice(); + assertThat(device, is(notNullValue())); + + assertThat(device.name.name, is("enterprises/project-id/devices/display-device-id")); + assertThat(device.type, is(SDMDeviceType.DISPLAY)); + + SDMTraits traits = device.traits; + assertThat(traits, is(notNullValue())); + assertThat(traits.traitList(), hasSize(7)); + + SDMDeviceInfoTrait deviceInfo = traits.deviceInfo; + assertThat(deviceInfo, is(notNullValue())); + assertThat(deviceInfo.customName, is("")); + + SDMConnectivityTrait connectivity = traits.connectivity; + assertThat(connectivity, is(nullValue())); + + SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream; + assertThat(cameraLiveStream, is(notNullValue())); + + SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution; + assertThat(maxVideoResolution, is(notNullValue())); + assertThat(maxVideoResolution.width, is(640)); + assertThat(maxVideoResolution.height, is(480)); + + assertThat(cameraLiveStream.videoCodecs, is(List.of("H264"))); + assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC"))); + + SDMCameraImageTrait cameraImage = traits.cameraImage; + assertThat(cameraImage, is(notNullValue())); + + SDMResolution maxImageResolution = cameraImage.maxImageResolution; + assertThat(maxImageResolution, is(notNullValue())); + assertThat(maxImageResolution.width, is(1920)); + assertThat(maxImageResolution.height, is(1200)); + + assertThat(traits.cameraPerson, is(notNullValue())); + assertThat(traits.cameraSound, is(notNullValue())); + assertThat(traits.cameraMotion, is(notNullValue())); + assertThat(traits.cameraEventImage, is(notNullValue())); + assertThat(traits.doorbellChime, is(nullValue())); + + List parentRelations = device.parentRelations; + assertThat(parentRelations, is(notNullValue())); + assertThat(parentRelations, hasSize(1)); + + assertThat(parentRelations.get(0).parent.name, + is("enterprises/project-id/structures/structure-id/rooms/display-room-id")); + assertThat(parentRelations.get(0).displayName, is("Display Room Name")); + } + + protected SDMDevice getDisplayDevice() throws IOException { + return fromJson("display-device-response.json", SDMDevice.class); + } + + @Test + public void deserializeDoorbellDevice() throws IOException { + SDMDevice device = getDoorbellDevice(); + assertThat(device, is(notNullValue())); + + assertThat(device.name.name, is("enterprises/project-id/devices/doorbell-device-id")); + assertThat(device.type, is(SDMDeviceType.DOORBELL)); + + SDMTraits traits = device.traits; + assertThat(traits, is(notNullValue())); + assertThat(traits.traitList(), hasSize(8)); + + SDMDeviceInfoTrait deviceInfo = traits.deviceInfo; + assertThat(deviceInfo, is(notNullValue())); + assertThat(deviceInfo.customName, is("")); + + SDMConnectivityTrait connectivity = traits.connectivity; + assertThat(connectivity, is(nullValue())); + + SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream; + assertThat(cameraLiveStream, is(notNullValue())); + + SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution; + assertThat(maxVideoResolution, is(notNullValue())); + assertThat(maxVideoResolution.width, is(640)); + assertThat(maxVideoResolution.height, is(480)); + + assertThat(cameraLiveStream.videoCodecs, is(List.of("H264"))); + assertThat(cameraLiveStream.audioCodecs, is(List.of("AAC"))); + + SDMCameraImageTrait cameraImage = traits.cameraImage; + assertThat(cameraImage, is(notNullValue())); + + SDMResolution maxImageResolution = cameraImage.maxImageResolution; + assertThat(maxImageResolution, is(notNullValue())); + assertThat(maxImageResolution.width, is(1920)); + assertThat(maxImageResolution.height, is(1200)); + + assertThat(traits.cameraPerson, is(notNullValue())); + assertThat(traits.cameraSound, is(notNullValue())); + assertThat(traits.cameraMotion, is(notNullValue())); + assertThat(traits.cameraEventImage, is(notNullValue())); + assertThat(traits.doorbellChime, is(notNullValue())); + + List parentRelations = device.parentRelations; + assertThat(parentRelations, is(notNullValue())); + assertThat(parentRelations, hasSize(1)); + + assertThat(parentRelations.get(0).parent.name, + is("enterprises/project-id/structures/structure-id/rooms/doorbell-room-id")); + assertThat(parentRelations.get(0).displayName, is("Doorbell Room Name")); + } + + protected SDMDevice getDoorbellDevice() throws IOException { + return fromJson("doorbell-device-response.json", SDMDevice.class); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMErrorTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMErrorTest.java new file mode 100644 index 0000000000000..62658fc95b913 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMErrorTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMError.SDMErrorDetails; + +/** + * Tests deserialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMError}s from JSON. + * + * @author Wouter Born - Initial contribution + */ +public class SDMErrorTest { + + @Test + public void deserializeFailedPreconditionError() throws IOException { + SDMError error = fromJson("failed-precondition-error.json", SDMError.class); + assertThat(error, is(notNullValue())); + + SDMErrorDetails details = error.error; + assertThat(details, is(notNullValue())); + assertThat(details.code, is(400)); + assertThat(details.message, is("Thermostat fan unavailable.")); + assertThat(details.status, is("FAILED_PRECONDITION")); + } + + @Test + public void deserializeNotFoundError() throws IOException { + SDMError error = fromJson("not-found-error.json", SDMError.class); + assertThat(error, is(notNullValue())); + + SDMErrorDetails details = error.error; + assertThat(details, is(notNullValue())); + assertThat(details.code, is(404)); + assertThat(details.message, is("Device enterprises/project-id/devices/device-id not found.")); + assertThat(details.status, is("NOT_FOUND")); + } + + @Test + public void deserializeResponseWithoutError() throws IOException { + SDMError error = fromJson("list-devices-response.json", SDMError.class); + assertThat(error, is(notNullValue())); + + SDMErrorDetails details = error.error; + assertThat(details, is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMEventTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMEventTest.java new file mode 100644 index 0000000000000..f1c151c9096fd --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMEventTest.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMDeviceEvent; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMRelationUpdate; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMRelationUpdateType; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate; +import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdateEvents; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMHvacStatus; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMTemperatureTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatHvacTrait; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMThermostatTemperatureSetpointTrait; + +/** + * Tests deserialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMEvent}s from JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMEventTest { + + @Test + public void deserializeResourceUpdateEvent() throws IOException { + SDMEvent event = fromJson("resource-update-event.json", SDMEvent.class); + assertThat(event, is(notNullValue())); + + assertThat(event.eventId, is("053a5f98-8c9d-426e-acf1-6b8660558832")); + assertThat(event.timestamp, is(ZonedDateTime.parse("2019-01-01T00:00:01Z"))); + + assertThat(event.relationUpdate, is(nullValue())); + + SDMResourceUpdate resourceUpdate = event.resourceUpdate; + assertThat(resourceUpdate, is(notNullValue())); + assertThat(resourceUpdate.name.name, is("enterprises/project-id/devices/device-id")); + + SDMTraits traits = resourceUpdate.traits; + assertThat(traits, is(notNullValue())); + assertThat(traits.traitList(), hasSize(3)); + + SDMResourceUpdateEvents events = resourceUpdate.events; + assertThat(events, is(notNullValue())); + assertThat(events.eventList(), hasSize(4)); + + SDMDeviceEvent cameraMotionEvent = events.cameraMotionEvent; + assertThat(cameraMotionEvent, is(notNullValue())); + assertThat(cameraMotionEvent.eventSessionId, is("ESI1")); + assertThat(cameraMotionEvent.eventId, is("EID1")); + + SDMDeviceEvent cameraPersonEvent = events.cameraPersonEvent; + assertThat(cameraPersonEvent, is(notNullValue())); + assertThat(cameraPersonEvent.eventSessionId, is("ESI2")); + assertThat(cameraPersonEvent.eventId, is("EID2")); + + SDMDeviceEvent cameraSoundEvent = events.cameraSoundEvent; + assertThat(cameraSoundEvent, is(notNullValue())); + assertThat(cameraSoundEvent.eventSessionId, is("ESI3")); + assertThat(cameraSoundEvent.eventId, is("EID3")); + + SDMDeviceEvent doorbellChimeEvent = events.doorbellChimeEvent; + assertThat(doorbellChimeEvent, is(notNullValue())); + assertThat(doorbellChimeEvent.eventSessionId, is("ESI4")); + assertThat(doorbellChimeEvent.eventId, is("EID4")); + + SDMTemperatureTrait temperature = traits.temperature; + assertThat(temperature, is(notNullValue())); + assertThat(temperature.ambientTemperatureCelsius, is(new BigDecimal("19.73"))); + + SDMThermostatHvacTrait thermostatHvac = traits.thermostatHvac; + assertThat(thermostatHvac, is(notNullValue())); + assertThat(thermostatHvac.status, is(SDMHvacStatus.OFF)); + + SDMThermostatTemperatureSetpointTrait thermostatTemperatureSetpoint = traits.thermostatTemperatureSetpoint; + assertThat(thermostatTemperatureSetpoint, is(notNullValue())); + assertThat(thermostatTemperatureSetpoint.heatCelsius, is(new BigDecimal("14.92249"))); + assertThat(thermostatTemperatureSetpoint.coolCelsius, is(nullValue())); + + assertThat(event.userId, is("AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi")); + assertThat(event.resourceGroup, is(List.of(new SDMResourceName("enterprises/project-id/devices/device-id")))); + } + + @Test + public void deserializeRelationCreatedEvent() throws IOException { + SDMEvent event = fromJson("relation-created-event.json", SDMEvent.class); + assertThat(event, is(notNullValue())); + + assertThat(event.eventId, is("0120ecc7-3b57-4eb4-9941-91609f189fb4")); + assertThat(event.timestamp, is(ZonedDateTime.parse("2019-01-01T00:00:01Z"))); + + SDMRelationUpdate relationUpdate = event.relationUpdate; + assertThat(relationUpdate, is(notNullValue())); + assertThat(relationUpdate.type, is(SDMRelationUpdateType.CREATED)); + assertThat(relationUpdate.subject.name, is("enterprises/project-id/structures/structure-id")); + assertThat(relationUpdate.object.name, is("enterprises/project-id/devices/device-id")); + + assertThat(event.resourceUpdate, is(nullValue())); + assertThat(event.userId, is("AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi")); + assertThat(event.resourceGroup, is(nullValue())); + } + + @Test + public void deserializeRelationDeletedEvent() throws IOException { + SDMEvent event = fromJson("relation-deleted-event.json", SDMEvent.class); + assertThat(event, is(notNullValue())); + + assertThat(event.eventId, is("0120ecc7-3b57-4eb4-9941-91609f189fb4")); + assertThat(event.timestamp, is(ZonedDateTime.parse("2019-01-01T00:00:01Z"))); + + SDMRelationUpdate relationUpdate = event.relationUpdate; + assertThat(relationUpdate, is(notNullValue())); + assertThat(relationUpdate.type, is(SDMRelationUpdateType.DELETED)); + assertThat(relationUpdate.subject.name, is("enterprises/project-id/structures/structure-id")); + assertThat(relationUpdate.object.name, is("enterprises/project-id/devices/device-id")); + + assertThat(event.resourceUpdate, is(nullValue())); + assertThat(event.userId, is("AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi")); + assertThat(event.resourceGroup, is(nullValue())); + } + + @Test + public void deserializeRelationUpdatedEvent() throws IOException { + SDMEvent event = fromJson("relation-updated-event.json", SDMEvent.class); + assertThat(event, is(notNullValue())); + + assertThat(event.eventId, is("0120ecc7-3b57-4eb4-9941-91609f189fb4")); + assertThat(event.timestamp, is(ZonedDateTime.parse("2019-01-01T00:00:01Z"))); + + SDMRelationUpdate relationUpdate = event.relationUpdate; + assertThat(relationUpdate, is(notNullValue())); + assertThat(relationUpdate.type, is(SDMRelationUpdateType.UPDATED)); + assertThat(relationUpdate.subject.name, is("enterprises/project-id/structures/structure-id")); + assertThat(relationUpdate.object.name, is("enterprises/project-id/devices/device-id")); + + assertThat(event.resourceUpdate, is(nullValue())); + assertThat(event.userId, is("AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi")); + assertThat(event.resourceGroup, is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponseTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponseTest.java new file mode 100644 index 0000000000000..fa7b55fed1003 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListDevicesResponseTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; + +/** + * Tests deserialization of {@link + * org.openhab.binding.nest.internal.sdm.dto.SDMListDevicesResponse}s from JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMListDevicesResponseTest extends SDMDeviceTest { + + private List devices = List.of(); + + @BeforeEach + public void deserializeListDevicesResponse() throws IOException { + SDMListDevicesResponse response = fromJson("list-devices-response.json", SDMListDevicesResponse.class); + assertThat(response, is(notNullValue())); + + devices = response.devices; + assertThat(devices, is(notNullValue())); + assertThat(devices, hasSize(4)); + } + + @Override + protected SDMDevice getThermostatDevice() throws IOException { + return devices.get(0); + } + + @Override + protected SDMDevice getCameraDevice() throws IOException { + return devices.get(1); + } + + @Override + protected SDMDevice getDisplayDevice() throws IOException { + return devices.get(2); + } + + @Override + protected SDMDevice getDoorbellDevice() throws IOException { + return devices.get(3); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponseTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponseTest.java new file mode 100644 index 0000000000000..82150247edce7 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListRoomsResponseTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMRoomInfoTrait; + +/** + * Tests deserialization of {@link org.openhab.binding.nest.internal.sdm.dto.SDMListRoomsResponse}s + * from JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMListRoomsResponseTest { + + @Test + public void deserializeListDevicesResponse() throws IOException { + SDMListRoomsResponse response = fromJson("list-rooms-response.json", SDMListRoomsResponse.class); + assertThat(response, is(notNullValue())); + + List rooms = response.rooms; + assertThat(rooms, is(notNullValue())); + assertThat(rooms, hasSize(2)); + + SDMRoom room = rooms.get(0); + assertThat(room, is(notNullValue())); + assertThat(room.name.name, is("enterprises/project-id/structures/structure-id/rooms/kitchen-room-id")); + SDMTraits traits = room.traits; + assertThat(traits.traitList(), hasSize(1)); + SDMRoomInfoTrait roomInfo = room.traits.roomInfo; + assertThat(roomInfo, is(notNullValue())); + assertThat(roomInfo.customName, is("Kitchen")); + + room = rooms.get(1); + assertThat(room, is(notNullValue())); + assertThat(room.name.name, is("enterprises/project-id/structures/structure-id/rooms/living-room-id")); + traits = room.traits; + assertThat(traits.traitList(), hasSize(1)); + roomInfo = room.traits.roomInfo; + assertThat(roomInfo, is(notNullValue())); + assertThat(roomInfo.customName, is("Living")); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponseTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponseTest.java new file mode 100644 index 0000000000000..838683de4cb75 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMListStructuresResponseTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.openhab.binding.nest.internal.sdm.dto.SDMDataUtil.fromJson; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMStructureInfoTrait; + +/** + * Tests deserialization of {@link + * org.openhab.binding.nest.internal.sdm.dto.SDMListStructuresResponse}s from JSON. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMListStructuresResponseTest { + + @Test + public void deserializeListDevicesResponse() throws IOException { + SDMListStructuresResponse response = fromJson("list-structures-response.json", SDMListStructuresResponse.class); + assertThat(response, is(notNullValue())); + + List structures = response.structures; + assertThat(structures, is(notNullValue())); + assertThat(structures, hasSize(2)); + + SDMStructure structure = structures.get(0); + assertThat(structure, is(notNullValue())); + assertThat(structure.name.name, is("enterprises/project-id/structures/beach-house-structure-id")); + SDMTraits traits = structure.traits; + assertThat(traits.traitList(), hasSize(1)); + SDMStructureInfoTrait structureInfo = structure.traits.structureInfo; + assertThat(structureInfo, is(notNullValue())); + assertThat(structureInfo.customName, is("Beach House")); + + structure = structures.get(1); + assertThat(structure, is(notNullValue())); + assertThat(structure.name.name, is("enterprises/project-id/structures/home-structure-id")); + traits = structure.traits; + assertThat(traits.traitList(), hasSize(1)); + structureInfo = structure.traits.structureInfo; + assertThat(structureInfo, is(notNullValue())); + assertThat(structureInfo.customName, is("Home")); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceNameTest.java b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceNameTest.java new file mode 100644 index 0000000000000..71771c11013d9 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceNameTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nest.internal.sdm.dto; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.core.Is.is; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.nest.internal.sdm.dto.SDMResourceName.SDMResourceNameType; + +/** + * Tests the data provided by {@link org.openhab.binding.nest.internal.sdm.dto.SDMResourceName} + * based on resource name strings. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class SDMResourceNameTest { + + @Test + public void nameless() { + SDMResourceName resourceName = SDMResourceName.NAMELESS; + assertThat(resourceName.name, is(emptyString())); + assertThat(resourceName.projectId, is(emptyString())); + assertThat(resourceName.deviceId, is(emptyString())); + assertThat(resourceName.structureId, is(emptyString())); + assertThat(resourceName.roomId, is(emptyString())); + assertThat(resourceName.type, is(SDMResourceNameType.UNKNOWN)); + } + + @Test + public void deviceName() { + String name = "enterprises/project-id/devices/device-id"; + + SDMResourceName resourceName = new SDMResourceName(name); + assertThat(resourceName.name, is(name)); + assertThat(resourceName.projectId, is("project-id")); + assertThat(resourceName.deviceId, is("device-id")); + assertThat(resourceName.structureId, is(emptyString())); + assertThat(resourceName.roomId, is(emptyString())); + assertThat(resourceName.type, is(SDMResourceNameType.DEVICE)); + } + + @Test + public void structureName() { + String name = "enterprises/project-id/structures/structure-id"; + + SDMResourceName resourceName = new SDMResourceName(name); + assertThat(resourceName.name, is(name)); + assertThat(resourceName.projectId, is("project-id")); + assertThat(resourceName.deviceId, is(emptyString())); + assertThat(resourceName.structureId, is("structure-id")); + assertThat(resourceName.roomId, is(emptyString())); + assertThat(resourceName.type, is(SDMResourceNameType.STRUCTURE)); + } + + @Test + public void roomName() { + String name = "enterprises/project-id/structures/structure-id/rooms/room-id"; + + SDMResourceName resourceName = new SDMResourceName(name); + assertThat(resourceName.name, is(name)); + assertThat(resourceName.projectId, is("project-id")); + assertThat(resourceName.deviceId, is(emptyString())); + assertThat(resourceName.structureId, is("structure-id")); + assertThat(resourceName.roomId, is("room-id")); + assertThat(resourceName.type, is(SDMResourceNameType.ROOM)); + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/acknowledge-subscription-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/acknowledge-subscription-request.json new file mode 100644 index 0000000000000..e092587f39513 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/acknowledge-subscription-request.json @@ -0,0 +1,7 @@ +{ + "ackIds": [ + "AID1", + "AID2", + "AID3" + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/camera-device-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/camera-device-response.json new file mode 100644 index 0000000000000..290521eb35fb6 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/camera-device-response.json @@ -0,0 +1,38 @@ +{ + "name": "enterprises/project-id/devices/camera-device-id", + "type": "sdm.devices.types.CAMERA", + "assignee": "enterprises/project-id/structures/structure-id/rooms/camera-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/camera-room-id", + "displayName": "Camera Room Name" + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/create-subscription-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/create-subscription-request.json new file mode 100644 index 0000000000000..49288973a2970 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/create-subscription-request.json @@ -0,0 +1,4 @@ +{ + "topic": "projects/sdm-prod/topics/enterprise-project-id", + "enableMessageOrdering": true +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/display-device-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/display-device-response.json new file mode 100644 index 0000000000000..32e63b9e36f24 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/display-device-response.json @@ -0,0 +1,38 @@ +{ + "name": "enterprises/project-id/devices/display-device-id", + "type": "sdm.devices.types.DISPLAY", + "assignee": "enterprises/project-id/structures/structure-id/rooms/display-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/display-room-id", + "displayName": "Display Room Name" + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/doorbell-device-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/doorbell-device-response.json new file mode 100644 index 0000000000000..eda96ae802312 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/doorbell-device-response.json @@ -0,0 +1,39 @@ +{ + "name": "enterprises/project-id/devices/doorbell-device-id", + "type": "sdm.devices.types.DOORBELL", + "assignee": "enterprises/project-id/structures/structure-id/rooms/doorbell-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {}, + "sdm.devices.traits.DoorbellChime": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/doorbell-room-id", + "displayName": "Doorbell Room Name" + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-request.json new file mode 100644 index 0000000000000..80050fcbeeb89 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.CameraLiveStream.ExtendRtspStream", + "params": { + "streamExtensionToken": "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-response.json new file mode 100644 index 0000000000000..4aad65c65acdb --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/extend-camera-rtsp-stream-response.json @@ -0,0 +1,7 @@ +{ + "results": { + "streamExtensionToken": "dGNUlTU2CjY5Y3VKaTZwR3o4Y1...", + "streamToken": "g.0.newStreamingToken", + "expiresAt": "2018-01-04T18:30:00.000Z" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/failed-precondition-error.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/failed-precondition-error.json new file mode 100644 index 0000000000000..98b5dcdb936c5 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/failed-precondition-error.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 400, + "message": "Thermostat fan unavailable.", + "status": "FAILED_PRECONDITION" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-request.json new file mode 100644 index 0000000000000..e1808f8819c63 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.CameraEventImage.GenerateImage", + "params": { + "eventId": "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-response.json new file mode 100644 index 0000000000000..eaa49caac74c8 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-image-response.json @@ -0,0 +1,6 @@ +{ + "results": { + "url": "https://domain/sdm_resource/dGNUlTU2CjY5Y3VKaTZwR3o4Y1...", + "token": "g.0.eventToken" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-request.json new file mode 100644 index 0000000000000..99ab8a3a7387a --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-request.json @@ -0,0 +1,4 @@ +{ + "command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream", + "params": {} +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-response.json new file mode 100644 index 0000000000000..2860392f696d3 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/generate-camera-rtsp-stream-response.json @@ -0,0 +1,10 @@ +{ + "results": { + "streamUrls": { + "rtspUrl": "rtsps://someurl.com/CjY5Y3VKaTZwR3o4Y19YbTVfMF...?auth=g.0.streamingToken" + }, + "streamExtensionToken": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "streamToken": "g.0.streamingToken", + "expiresAt": "2018-01-04T18:30:00.000Z" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-devices-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-devices-response.json new file mode 100644 index 0000000000000..786d968efac1e --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-devices-response.json @@ -0,0 +1,173 @@ +{ + "devices": [ + { + "name": "enterprises/project-id/devices/thermostat-device-id", + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/thermostat-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 26 + }, + "sdm.devices.traits.Connectivity": { + "status": "ONLINE" + }, + "sdm.devices.traits.Fan" : { + "timerMode" : "ON", + "timerTimeout" : "2019-05-10T03:22:54Z" + }, + "sdm.devices.traits.ThermostatMode": { + "mode": "HEAT", + "availableModes": [ + "HEAT", + "OFF" + ] + }, + "sdm.devices.traits.ThermostatEco": { + "availableModes": [ + "OFF", + "MANUAL_ECO" + ], + "mode": "OFF", + "heatCelsius": 15.34473, + "coolCelsius": 24.44443 + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF" + }, + "sdm.devices.traits.Settings": { + "temperatureScale": "CELSIUS" + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "heatCelsius": 14.92249 + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 19.73 + } + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/thermostat-room-id", + "displayName": "Thermostat Room Name" + } + ] + }, + { + "name": "enterprises/project-id/devices/camera-device-id", + "type": "sdm.devices.types.CAMERA", + "assignee": "enterprises/project-id/structures/structure-id/rooms/camera-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/camera-room-id", + "displayName": "Camera Room Name" + } + ] + }, + { + "name": "enterprises/project-id/devices/display-device-id", + "type": "sdm.devices.types.DISPLAY", + "assignee": "enterprises/project-id/structures/structure-id/rooms/display-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/display-room-id", + "displayName": "Display Room Name" + } + ] + }, + { + "name": "enterprises/project-id/devices/doorbell-device-id", + "type": "sdm.devices.types.DOORBELL", + "assignee": "enterprises/project-id/structures/structure-id/rooms/doorbell-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480 + }, + "videoCodecs": [ + "H264" + ], + "audioCodecs": [ + "AAC" + ] + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 1920, + "height": 1200 + } + }, + "sdm.devices.traits.CameraPerson": {}, + "sdm.devices.traits.CameraSound": {}, + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraEventImage": {}, + "sdm.devices.traits.DoorbellChime": {} + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/doorbell-room-id", + "displayName": "Doorbell Room Name" + } + ] + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-rooms-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-rooms-response.json new file mode 100644 index 0000000000000..49387c2ecbfbf --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-rooms-response.json @@ -0,0 +1,20 @@ +{ + "rooms": [ + { + "name": "enterprises/project-id/structures/structure-id/rooms/kitchen-room-id", + "traits": { + "sdm.structures.traits.RoomInfo": { + "customName": "Kitchen" + } + } + }, + { + "name": "enterprises/project-id/structures/structure-id/rooms/living-room-id", + "traits": { + "sdm.structures.traits.RoomInfo": { + "customName": "Living" + } + } + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-structures-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-structures-response.json new file mode 100644 index 0000000000000..efc2bd038cd34 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/list-structures-response.json @@ -0,0 +1,20 @@ +{ + "structures": [ + { + "name": "enterprises/project-id/structures/beach-house-structure-id", + "traits": { + "sdm.structures.traits.Info": { + "customName": "Beach House" + } + } + }, + { + "name": "enterprises/project-id/structures/home-structure-id", + "traits": { + "sdm.structures.traits.Info": { + "customName": "Home" + } + } + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/not-found-error.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/not-found-error.json new file mode 100644 index 0000000000000..7db63be922e7f --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/not-found-error.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 404, + "message": "Device enterprises/project-id/devices/device-id not found.", + "status": "NOT_FOUND" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-request.json new file mode 100644 index 0000000000000..05c7b7ec0c4ef --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-request.json @@ -0,0 +1,3 @@ +{ + "maxMessages": 123 +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-response.json new file mode 100644 index 0000000000000..8e7af635bec0b --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/pull-subscription-response.json @@ -0,0 +1,28 @@ +{ + "receivedMessages": [ + { + "ackId": "AID1", + "message": { + "data": "ZGF0YTE=", + "messageId": "1000000000000001", + "publishTime": "2021-01-01T01:00:00.000Z" + } + }, + { + "ackId": "AID2", + "message": { + "data": "ZGF0YTI=", + "messageId": "2000000000000002", + "publishTime": "2021-02-02T02:00:00.000Z" + } + }, + { + "ackId": "AID3", + "message": { + "data": "ZGF0YTM=", + "messageId": "3000000000000003", + "publishTime": "2021-03-03T03:00:00.000Z" + } + } + ] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-created-event.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-created-event.json new file mode 100644 index 0000000000000..31fe4b924ef40 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-created-event.json @@ -0,0 +1,10 @@ +{ + "eventId": "0120ecc7-3b57-4eb4-9941-91609f189fb4", + "timestamp": "2019-01-01T00:00:01Z", + "relationUpdate": { + "type": "CREATED", + "subject": "enterprises/project-id/structures/structure-id", + "object": "enterprises/project-id/devices/device-id" + }, + "userId": "AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi" +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-deleted-event.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-deleted-event.json new file mode 100644 index 0000000000000..15a6c98bca270 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-deleted-event.json @@ -0,0 +1,10 @@ +{ + "eventId": "0120ecc7-3b57-4eb4-9941-91609f189fb4", + "timestamp": "2019-01-01T00:00:01Z", + "relationUpdate": { + "type": "DELETED", + "subject": "enterprises/project-id/structures/structure-id", + "object": "enterprises/project-id/devices/device-id" + }, + "userId": "AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi" +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-updated-event.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-updated-event.json new file mode 100644 index 0000000000000..e638934580899 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/relation-updated-event.json @@ -0,0 +1,10 @@ +{ + "eventId": "0120ecc7-3b57-4eb4-9941-91609f189fb4", + "timestamp": "2019-01-01T00:00:01Z", + "relationUpdate": { + "type": "UPDATED", + "subject": "enterprises/project-id/structures/structure-id", + "object": "enterprises/project-id/devices/device-id" + }, + "userId": "AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi" +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/resource-update-event.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/resource-update-event.json new file mode 100644 index 0000000000000..537abf2edaf83 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/resource-update-event.json @@ -0,0 +1,38 @@ +{ + "eventId": "053a5f98-8c9d-426e-acf1-6b8660558832", + "timestamp": "2019-01-01T00:00:01Z", + "resourceUpdate": { + "name": "enterprises/project-id/devices/device-id", + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": "ESI1", + "eventId": "EID1" + }, + "sdm.devices.events.CameraPerson.Person": { + "eventSessionId": "ESI2", + "eventId": "EID2" + }, + "sdm.devices.events.CameraSound.Sound" : { + "eventSessionId" : "ESI3", + "eventId" : "EID3" + }, + "sdm.devices.events.DoorbellChime.Chime" : { + "eventSessionId" : "ESI4", + "eventId" : "EID4" + } + }, + "traits": { + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 19.73 + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF" + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "heatCelsius": 14.92249 + } + } + }, + "userId": "AVPHwEuBfnPOnTqzVFT4IONX2Qqhu9EJ4ubO-bNnQ-yi", + "resourceGroup": ["enterprises/project-id/devices/device-id"] +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-with-duration.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-with-duration.json new file mode 100644 index 0000000000000..18b2ba815dfcf --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-with-duration.json @@ -0,0 +1,7 @@ +{ + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "timerMode": "ON", + "duration": "3600s" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-without-duration.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-without-duration.json new file mode 100644 index 0000000000000..434773511e6fa --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-fan-timer-request-without-duration.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "timerMode": "ON" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-cool-setpoint-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-cool-setpoint-request.json new file mode 100644 index 0000000000000..a69dc7aee8008 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-cool-setpoint-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool", + "params": { + "coolCelsius": 20.0 + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-eco-mode-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-eco-mode-request.json new file mode 100644 index 0000000000000..aaca3d49b8b24 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-eco-mode-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.ThermostatEco.SetMode", + "params": { + "mode": "MANUAL_ECO" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-heat-setpoint-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-heat-setpoint-request.json new file mode 100644 index 0000000000000..64d78ed61412a --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-heat-setpoint-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat", + "params": { + "heatCelsius": 15.0 + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-mode-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-mode-request.json new file mode 100644 index 0000000000000..b3193f32d7bfa --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-mode-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.ThermostatMode.SetMode", + "params": { + "mode": "HEATCOOL" + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-range-setpoint-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-range-setpoint-request.json new file mode 100644 index 0000000000000..9020a72b37c7c --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/set-thermostat-range-setpoint-request.json @@ -0,0 +1,7 @@ +{ + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange", + "params": { + "heatCelsius": 15.0, + "coolCelsius": 20.0 + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/stop-camera-rtsp-stream-request.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/stop-camera-rtsp-stream-request.json new file mode 100644 index 0000000000000..a1a1b38dd69ea --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/stop-camera-rtsp-stream-request.json @@ -0,0 +1,6 @@ +{ + "command": "sdm.devices.commands.CameraLiveStream.StopRtspStream", + "params": { + "streamExtensionToken": "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." + } +} diff --git a/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/thermostat-device-response.json b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/thermostat-device-response.json new file mode 100644 index 0000000000000..409b5edfbda6b --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/test/resources/org/openhab/binding/nest/internal/sdm/dto/thermostat-device-response.json @@ -0,0 +1,54 @@ +{ + "name": "enterprises/project-id/devices/thermostat-device-id", + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/thermostat-room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "" + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 26 + }, + "sdm.devices.traits.Connectivity": { + "status": "ONLINE" + }, + "sdm.devices.traits.Fan" : { + "timerMode" : "ON", + "timerTimeout" : "2019-05-10T03:22:54Z" + }, + "sdm.devices.traits.ThermostatMode": { + "mode": "HEAT", + "availableModes": [ + "HEAT", + "OFF" + ] + }, + "sdm.devices.traits.ThermostatEco": { + "availableModes": [ + "OFF", + "MANUAL_ECO" + ], + "mode": "OFF", + "heatCelsius": 15.34473, + "coolCelsius": 24.44443 + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF" + }, + "sdm.devices.traits.Settings": { + "temperatureScale": "CELSIUS" + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "heatCelsius": 14.92249 + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 19.73 + } + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/thermostat-room-id", + "displayName": "Thermostat Room Name" + } + ] +} diff --git a/itests/org.openhab.binding.nest.tests/itest.bndrun b/itests/org.openhab.binding.nest.tests/itest.bndrun index 7a804b69611a7..d623f3dbc0510 100644 --- a/itests/org.openhab.binding.nest.tests/itest.bndrun +++ b/itests/org.openhab.binding.nest.tests/itest.bndrun @@ -29,6 +29,7 @@ Fragment-Host: org.openhab.binding.nest org.openhab.binding.nest;version='[3.1.0,3.1.1)',\ org.openhab.binding.nest.tests;version='[3.1.0,3.1.1)',\ org.openhab.core;version='[3.1.0,3.1.1)',\ + org.openhab.core.auth.oauth2client;version='[3.1.0,3.1.1)',\ org.openhab.core.binding.xml;version='[3.1.0,3.1.1)',\ org.openhab.core.config.core;version='[3.1.0,3.1.1)',\ org.openhab.core.config.discovery;version='[3.1.0,3.1.1)',\ diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/NestDataUtil.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDataUtil.java similarity index 89% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/NestDataUtil.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDataUtil.java index f37fef5e3770f..b69895eba913d 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/NestDataUtil.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNDataUtil.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import java.io.BufferedReader; import java.io.IOException; @@ -23,16 +23,16 @@ import javax.measure.Unit; import javax.measure.quantity.Temperature; -import org.openhab.binding.nest.internal.NestUtils; +import org.openhab.binding.nest.internal.wwn.WWNUtils; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; /** * Utility class for working with Nest test data in unit tests. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public final class NestDataUtil { +public final class WWNDataUtil { public static final String COMPLETE_DATA_FILE_NAME = "top-level-streaming-data.json"; public static final String INCOMPLETE_DATA_FILE_NAME = "top-level-streaming-data-incomplete.json"; @@ -61,20 +61,20 @@ public final class NestDataUtil { public static final String THERMOSTAT1_DEVICE_ID = "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV"; public static final String THERMOSTAT1_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw"; - private NestDataUtil() { + private WWNDataUtil() { // Hidden utility class constructor } public static Reader openDataReader(String fileName) throws UnsupportedEncodingException { - String packagePath = (NestDataUtil.class.getPackage().getName()).replaceAll("\\.", "/"); + String packagePath = (WWNDataUtil.class.getPackage().getName()).replaceAll("\\.", "/"); String filePath = "/" + packagePath + "/" + fileName; - InputStream inputStream = NestDataUtil.class.getClassLoader().getResourceAsStream(filePath); + InputStream inputStream = WWNDataUtil.class.getClassLoader().getResourceAsStream(filePath); return new InputStreamReader(inputStream, "UTF-8"); } public static T fromJson(String fileName, Class dataClass) throws IOException { try (Reader reader = openDataReader(fileName)) { - return NestUtils.fromJson(reader, dataClass); + return WWNUtils.fromJson(reader, dataClass); } } diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/GsonParsingTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNGsonParsingTest.java similarity index 80% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/GsonParsingTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNGsonParsingTest.java index edcdcaa801740..d5fafa3379f0b 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/data/GsonParsingTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/dto/WWNGsonParsingTest.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.internal.data; +package org.openhab.binding.nest.internal.wwn.dto; import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.nest.internal.data.NestDataUtil.*; +import static org.openhab.binding.nest.internal.wwn.dto.WWNDataUtil.*; import java.io.IOException; import java.text.SimpleDateFormat; @@ -30,9 +30,9 @@ * @author David Bennett - Initial contribution * @author Wouter Born - Increase test coverage */ -public class GsonParsingTest { +public class WWNGsonParsingTest { - private final Logger logger = LoggerFactory.getLogger(GsonParsingTest.class); + private final Logger logger = LoggerFactory.getLogger(WWNGsonParsingTest.class); private static void assertEqualDateTime(String expected, Date actual) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); @@ -41,7 +41,7 @@ private static void assertEqualDateTime(String expected, Date actual) { @Test public void verifyCompleteInput() throws IOException { - TopLevelData topLevel = fromJson("top-level-data.json", TopLevelData.class); + WWNTopLevelData topLevel = fromJson("top-level-data.json", WWNTopLevelData.class); assertEquals(topLevel.getDevices().getThermostats().size(), 1); assertNotNull(topLevel.getDevices().getThermostats().get(THERMOSTAT1_DEVICE_ID)); @@ -57,12 +57,12 @@ public void verifyCompleteInput() throws IOException { @Test public void verifyCompleteStreamingInput() throws IOException { - TopLevelStreamingData topLevelStreamingData = fromJson("top-level-streaming-data.json", - TopLevelStreamingData.class); + WWNTopLevelStreamingData topLevelStreamingData = fromJson("top-level-streaming-data.json", + WWNTopLevelStreamingData.class); assertEquals("/", topLevelStreamingData.getPath()); - TopLevelData data = topLevelStreamingData.getData(); + WWNTopLevelData data = topLevelStreamingData.getData(); assertEquals(data.getDevices().getThermostats().size(), 1); assertNotNull(data.getDevices().getThermostats().get(THERMOSTAT1_DEVICE_ID)); assertEquals(data.getDevices().getCameras().size(), 2); @@ -77,7 +77,7 @@ public void verifyCompleteStreamingInput() throws IOException { @Test public void verifyThermostat() throws IOException { - Thermostat thermostat = fromJson("thermostat-data.json", Thermostat.class); + WWNThermostat thermostat = fromJson("thermostat-data.json", WWNThermostat.class); logger.debug("Thermostat: {}", thermostat); assertTrue(thermostat.isOnline()); @@ -97,12 +97,12 @@ public void verifyThermostat() throws IOException { assertEquals(Double.valueOf(12.5), thermostat.getEcoTemperatureLow()); assertEquals(Double.valueOf(22.0), thermostat.getLockedTempMax()); assertEquals(Double.valueOf(20.0), thermostat.getLockedTempMin()); - assertEquals(Thermostat.Mode.HEAT, thermostat.getMode()); + assertEquals(WWNThermostat.Mode.HEAT, thermostat.getMode()); assertEquals("Living Room (Living Room)", thermostat.getName()); assertEquals("Living Room Thermostat (Living Room)", thermostat.getNameLong()); assertEquals(null, thermostat.getPreviousHvacMode()); assertEquals("5.6-7", thermostat.getSoftwareVersion()); - assertEquals(Thermostat.State.OFF, thermostat.getHvacState()); + assertEquals(WWNThermostat.State.OFF, thermostat.getHvacState()); assertEquals(STRUCTURE1_STRUCTURE_ID, thermostat.getStructureId()); assertEquals(Double.valueOf(15.5), thermostat.getTargetTemperature()); assertEquals(Double.valueOf(24.0), thermostat.getTargetTemperatureHigh()); @@ -115,22 +115,22 @@ public void verifyThermostat() throws IOException { @Test public void thermostatTimeToTargetSupportedValueParsing() { - assertEquals((Integer) 0, Thermostat.parseTimeToTarget("~0")); - assertEquals((Integer) 5, Thermostat.parseTimeToTarget("<5")); - assertEquals((Integer) 10, Thermostat.parseTimeToTarget("<10")); - assertEquals((Integer) 15, Thermostat.parseTimeToTarget("~15")); - assertEquals((Integer) 90, Thermostat.parseTimeToTarget("~90")); - assertEquals((Integer) 120, Thermostat.parseTimeToTarget(">120")); + assertEquals((Integer) 0, WWNThermostat.parseTimeToTarget("~0")); + assertEquals((Integer) 5, WWNThermostat.parseTimeToTarget("<5")); + assertEquals((Integer) 10, WWNThermostat.parseTimeToTarget("<10")); + assertEquals((Integer) 15, WWNThermostat.parseTimeToTarget("~15")); + assertEquals((Integer) 90, WWNThermostat.parseTimeToTarget("~90")); + assertEquals((Integer) 120, WWNThermostat.parseTimeToTarget(">120")); } @Test public void thermostatTimeToTargetUnsupportedValueParsing() { - assertThrows(NumberFormatException.class, () -> Thermostat.parseTimeToTarget("#5")); + assertThrows(NumberFormatException.class, () -> WWNThermostat.parseTimeToTarget("#5")); } @Test public void verifyCamera() throws IOException { - Camera camera = fromJson("camera-data.json", Camera.class); + WWNCamera camera = fromJson("camera-data.json", WWNCamera.class); logger.debug("Camera: {}", camera); assertTrue(camera.isOnline()); @@ -166,7 +166,7 @@ public void verifyCamera() throws IOException { @Test public void verifySmokeDetector() throws IOException { - SmokeDetector smokeDetector = fromJson("smoke-detector-data.json", SmokeDetector.class); + WWNSmokeDetector smokeDetector = fromJson("smoke-detector-data.json", WWNSmokeDetector.class); logger.debug("SmokeDetector: {}", smokeDetector); assertTrue(smokeDetector.isOnline()); @@ -175,17 +175,17 @@ public void verifySmokeDetector() throws IOException { assertEquals("Downstairs", smokeDetector.getName()); assertEquals("Downstairs Nest Protect", smokeDetector.getNameLong()); assertEqualDateTime("2017-02-02T20:53:05.338Z", smokeDetector.getLastConnection()); - assertEquals(SmokeDetector.BatteryHealth.OK, smokeDetector.getBatteryHealth()); - assertEquals(SmokeDetector.AlarmState.OK, smokeDetector.getCoAlarmState()); - assertEquals(SmokeDetector.AlarmState.OK, smokeDetector.getSmokeAlarmState()); + assertEquals(WWNSmokeDetector.BatteryHealth.OK, smokeDetector.getBatteryHealth()); + assertEquals(WWNSmokeDetector.AlarmState.OK, smokeDetector.getCoAlarmState()); + assertEquals(WWNSmokeDetector.AlarmState.OK, smokeDetector.getSmokeAlarmState()); assertEquals("3.1rc9", smokeDetector.getSoftwareVersion()); assertEquals(STRUCTURE1_STRUCTURE_ID, smokeDetector.getStructureId()); - assertEquals(SmokeDetector.UiColorState.GREEN, smokeDetector.getUiColorState()); + assertEquals(WWNSmokeDetector.UiColorState.GREEN, smokeDetector.getUiColorState()); } @Test public void verifyAccessToken() throws IOException { - AccessTokenData accessToken = fromJson("access-token-data.json", AccessTokenData.class); + WWNAccessTokenData accessToken = fromJson("access-token-data.json", WWNAccessTokenData.class); logger.debug("AccessTokenData: {}", accessToken); assertEquals("access_token", accessToken.getAccessToken()); @@ -194,13 +194,13 @@ public void verifyAccessToken() throws IOException { @Test public void verifyStructure() throws IOException { - Structure structure = fromJson("structure-data.json", Structure.class); + WWNStructure structure = fromJson("structure-data.json", WWNStructure.class); logger.debug("Structure: {}", structure); assertEquals("Home", structure.getName()); assertEquals("US", structure.getCountryCode()); assertEquals("98056", structure.getPostalCode()); - assertEquals(Structure.HomeAwayState.HOME, structure.getAway()); + assertEquals(WWNStructure.HomeAwayState.HOME, structure.getAway()); assertEqualDateTime("2017-02-02T03:10:08.000Z", structure.getEtaBegin()); assertNull(structure.getEta()); assertNull(structure.getPeakPeriodEndTime()); @@ -212,7 +212,7 @@ public void verifyStructure() throws IOException { @Test public void verifyError() throws IOException { - ErrorData error = fromJson("error-data.json", ErrorData.class); + WWNErrorData error = fromJson("error-data.json", WWNErrorData.class); logger.debug("ErrorData: {}", error); assertEquals("blocked", error.getError()); diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestBridgeHandlerTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandlerTest.java similarity index 81% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestBridgeHandlerTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandlerTest.java index dbcc2749a6416..fe9713b5e3547 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestBridgeHandlerTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNAccountHandlerTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; @@ -25,10 +25,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.openhab.binding.nest.internal.config.NestBridgeConfiguration; -import org.openhab.binding.nest.internal.handler.NestBridgeHandler; -import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier; -import org.openhab.binding.nest.test.NestTestBridgeHandler; +import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration; +import org.openhab.binding.nest.internal.wwn.test.WWNTestAccountHandler; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; @@ -38,12 +36,12 @@ import org.osgi.service.jaxrs.client.SseEventSourceFactory; /** - * Tests cases for {@link NestBridgeHandler}. + * Tests cases for {@link WWNAccountHandler}. * * @author David Bennett - Initial contribution */ @ExtendWith(MockitoExtension.class) -public class NestBridgeHandlerTest { +public class WWNAccountHandlerTest { private ThingHandler handler; @@ -52,11 +50,11 @@ public class NestBridgeHandlerTest { private @Mock ClientBuilder clientBuilder; private @Mock Configuration configuration; private @Mock SseEventSourceFactory eventSourceFactory; - private @Mock NestRedirectUrlSupplier redirectUrlSupplier; + private @Mock WWNRedirectUrlSupplier redirectUrlSupplier; @BeforeEach public void beforeEach() { - handler = new NestTestBridgeHandler(bridge, clientBuilder, eventSourceFactory, "http://localhost"); + handler = new WWNTestAccountHandler(bridge, clientBuilder, eventSourceFactory, "http://localhost"); handler.setCallback(callback); } @@ -64,8 +62,8 @@ public void beforeEach() { @Test public void initializeShouldCallTheCallback() { when(bridge.getConfiguration()).thenReturn(configuration); - NestBridgeConfiguration bridgeConfig = new NestBridgeConfiguration(); - when(configuration.as(eq(NestBridgeConfiguration.class))).thenReturn(bridgeConfig); + WWNAccountConfiguration bridgeConfig = new WWNAccountConfiguration(); + when(configuration.as(eq(WWNAccountConfiguration.class))).thenReturn(bridgeConfig); bridgeConfig.accessToken = "my token"; // we expect the handler#initialize method to call the callback during execution and diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestCameraHandlerTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandlerTest.java similarity index 90% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestCameraHandlerTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandlerTest.java index 3410017c4f9f8..4db719f9f270c 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestCameraHandlerTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNCameraHandlerTest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.binding.nest.internal.data.NestDataUtil.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.dto.WWNDataUtil.*; import static org.openhab.core.library.types.OnOffType.*; import java.io.IOException; @@ -23,8 +23,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.openhab.binding.nest.internal.config.NestDeviceConfiguration; -import org.openhab.binding.nest.internal.handler.NestCameraHandler; +import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; @@ -35,23 +34,23 @@ import org.openhab.core.thing.binding.builder.ThingBuilder; /** - * Tests for {@link NestCameraHandler}. + * Tests for {@link WWNCameraHandler}. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestCameraHandlerTest extends NestThingHandlerOSGiTest { +public class WWNCameraHandlerTest extends WWNThingHandlerOSGiTest { private static final ThingUID CAMERA_UID = new ThingUID(THING_TYPE_CAMERA, "camera1"); private static final int CHANNEL_COUNT = 20; - public NestCameraHandlerTest() { - super(NestCameraHandler.class); + public WWNCameraHandlerTest() { + super(WWNCameraHandler.class); } @Override protected Thing buildThing(Bridge bridge) { Map properties = new HashMap<>(); - properties.put(NestDeviceConfiguration.DEVICE_ID, CAMERA1_DEVICE_ID); + properties.put(WWNDeviceConfiguration.DEVICE_ID, CAMERA1_DEVICE_ID); return ThingBuilder.create(THING_TYPE_CAMERA, CAMERA_UID).withLabel("Test Camera").withBridge(bridge.getUID()) .withChannels(buildChannels(THING_TYPE_CAMERA, CAMERA_UID)) diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestSmokeDetectorHandlerTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandlerTest.java similarity index 86% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestSmokeDetectorHandlerTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandlerTest.java index 644635f4692b0..95314805a4332 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestSmokeDetectorHandlerTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNSmokeDetectorHandlerTest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.binding.nest.internal.data.NestDataUtil.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.dto.WWNDataUtil.*; import static org.openhab.core.library.types.OnOffType.OFF; import java.io.IOException; @@ -23,8 +23,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.openhab.binding.nest.internal.config.NestDeviceConfiguration; -import org.openhab.binding.nest.internal.handler.NestSmokeDetectorHandler; +import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; @@ -35,23 +34,23 @@ import org.openhab.core.thing.binding.builder.ThingBuilder; /** - * Tests for {@link NestSmokeDetectorHandler}. + * Tests for {@link WWNSmokeDetectorHandler}. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestSmokeDetectorHandlerTest extends NestThingHandlerOSGiTest { +public class WWNSmokeDetectorHandlerTest extends WWNThingHandlerOSGiTest { private static final ThingUID SMOKE_DETECTOR_UID = new ThingUID(THING_TYPE_SMOKE_DETECTOR, "smoke1"); private static final int CHANNEL_COUNT = 7; - public NestSmokeDetectorHandlerTest() { - super(NestSmokeDetectorHandler.class); + public WWNSmokeDetectorHandlerTest() { + super(WWNSmokeDetectorHandler.class); } @Override protected Thing buildThing(Bridge bridge) { Map properties = new HashMap<>(); - properties.put(NestDeviceConfiguration.DEVICE_ID, SMOKE1_DEVICE_ID); + properties.put(WWNDeviceConfiguration.DEVICE_ID, SMOKE1_DEVICE_ID); return ThingBuilder.create(THING_TYPE_SMOKE_DETECTOR, SMOKE_DETECTOR_UID).withLabel("Test Smoke Detector") .withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_SMOKE_DETECTOR, SMOKE_DETECTOR_UID)) diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestStructureHandlerTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandlerTest.java similarity index 88% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestStructureHandlerTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandlerTest.java index 66c074d6f0458..02b40cb2780d6 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestStructureHandlerTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNStructureHandlerTest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.binding.nest.internal.data.NestDataUtil.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.dto.WWNDataUtil.*; import static org.openhab.core.library.types.OnOffType.OFF; import java.io.IOException; @@ -23,8 +23,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.openhab.binding.nest.internal.config.NestStructureConfiguration; -import org.openhab.binding.nest.internal.handler.NestStructureHandler; +import org.openhab.binding.nest.internal.wwn.config.WWNStructureConfiguration; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; @@ -35,23 +34,23 @@ import org.openhab.core.thing.binding.builder.ThingBuilder; /** - * Tests for {@link NestStructureHandler}. + * Tests for {@link WWNStructureHandler}. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestStructureHandlerTest extends NestThingHandlerOSGiTest { +public class WWNStructureHandlerTest extends WWNThingHandlerOSGiTest { private static final ThingUID STRUCTURE_UID = new ThingUID(THING_TYPE_STRUCTURE, "structure1"); private static final int CHANNEL_COUNT = 11; - public NestStructureHandlerTest() { - super(NestStructureHandler.class); + public WWNStructureHandlerTest() { + super(WWNStructureHandler.class); } @Override protected Thing buildThing(Bridge bridge) { Map properties = new HashMap<>(); - properties.put(NestStructureConfiguration.STRUCTURE_ID, STRUCTURE1_STRUCTURE_ID); + properties.put(WWNStructureConfiguration.STRUCTURE_ID, STRUCTURE1_STRUCTURE_ID); return ThingBuilder.create(THING_TYPE_STRUCTURE, STRUCTURE_UID).withLabel("Test Structure") .withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_STRUCTURE, STRUCTURE_UID)) diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThermostatHandlerTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandlerTest.java similarity index 95% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThermostatHandlerTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandlerTest.java index 6e5099101492d..d2020fafc0999 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThermostatHandlerTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThermostatHandlerTest.java @@ -10,12 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.binding.nest.internal.data.NestDataUtil.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.dto.WWNDataUtil.*; import static org.openhab.core.library.types.OnOffType.*; import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT; import static org.openhab.core.library.unit.SIUnits.CELSIUS; @@ -25,8 +25,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.openhab.binding.nest.internal.config.NestDeviceConfiguration; -import org.openhab.binding.nest.internal.handler.NestThermostatHandler; +import org.openhab.binding.nest.internal.wwn.config.WWNDeviceConfiguration; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -39,23 +38,23 @@ import org.openhab.core.thing.binding.builder.ThingBuilder; /** - * Tests for {@link NestThermostatHandler}. + * Tests for {@link WWNThermostatHandler}. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestThermostatHandlerTest extends NestThingHandlerOSGiTest { +public class WWNThermostatHandlerTest extends WWNThingHandlerOSGiTest { private static final ThingUID THERMOSTAT_UID = new ThingUID(THING_TYPE_THERMOSTAT, "thermostat1"); private static final int CHANNEL_COUNT = 25; - public NestThermostatHandlerTest() { - super(NestThermostatHandler.class); + public WWNThermostatHandlerTest() { + super(WWNThermostatHandler.class); } @Override protected Thing buildThing(Bridge bridge) { Map properties = new HashMap<>(); - properties.put(NestDeviceConfiguration.DEVICE_ID, THERMOSTAT1_DEVICE_ID); + properties.put(WWNDeviceConfiguration.DEVICE_ID, THERMOSTAT1_DEVICE_ID); return ThingBuilder.create(THING_TYPE_THERMOSTAT, THERMOSTAT_UID).withLabel("Test Thermostat") .withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_THERMOSTAT, THERMOSTAT_UID)) diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThingHandlerOSGiTest.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThingHandlerOSGiTest.java similarity index 87% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThingHandlerOSGiTest.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThingHandlerOSGiTest.java index 5ac87e1bbfa24..a88a19989e89b 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/handler/NestThingHandlerOSGiTest.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/handler/WWNThingHandlerOSGiTest.java @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.handler; +package org.openhab.binding.nest.internal.wwn.handler; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.mockito.Mockito.*; -import static org.openhab.binding.nest.internal.rest.NestStreamingRestClient.PUT; +import static org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient.PUT; import java.io.IOException; import java.time.Instant; @@ -36,12 +36,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.openhab.binding.nest.internal.config.NestBridgeConfiguration; -import org.openhab.binding.nest.internal.handler.NestBaseHandler; -import org.openhab.binding.nest.test.NestTestApiServlet; -import org.openhab.binding.nest.test.NestTestBridgeHandler; -import org.openhab.binding.nest.test.NestTestHandlerFactory; -import org.openhab.binding.nest.test.NestTestServer; +import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration; +import org.openhab.binding.nest.internal.wwn.test.WWNTestAccountHandler; +import org.openhab.binding.nest.internal.wwn.test.WWNTestApiServlet; +import org.openhab.binding.nest.internal.wwn.test.WWNTestHandlerFactory; +import org.openhab.binding.nest.internal.wwn.test.WWNTestServer; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.EventPublisher; import org.openhab.core.items.Item; @@ -84,21 +83,21 @@ import org.slf4j.LoggerFactory; /** - * {@link NestThingHandlerOSGiTest} is an abstract base class for Nest OSGi based tests. + * {@link WWNThingHandlerOSGiTest} is an abstract base class for Nest OSGi based tests. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public abstract class NestThingHandlerOSGiTest extends JavaOSGiTest { +public abstract class WWNThingHandlerOSGiTest extends JavaOSGiTest { private static final String SERVER_HOST = "127.0.0.1"; private static final int SERVER_PORT = TestPortUtil.findFreePort(); private static final int SERVER_TIMEOUT = -1; private static final String REDIRECT_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT; - private final Logger logger = LoggerFactory.getLogger(NestThingHandlerOSGiTest.class); + private final Logger logger = LoggerFactory.getLogger(WWNThingHandlerOSGiTest.class); - private static NestTestServer server; - private static NestTestApiServlet servlet = new NestTestApiServlet(); + private static WWNTestServer server; + private static WWNTestApiServlet servlet = new WWNTestApiServlet(); private ChannelTypeRegistry channelTypeRegistry; private ChannelGroupTypeRegistry channelGroupTypeRegistry; @@ -111,23 +110,23 @@ public abstract class NestThingHandlerOSGiTest extends JavaOSGiTest { private VolatileStorageService volatileStorageService = new VolatileStorageService(); protected Bridge bridge; - protected NestTestBridgeHandler bridgeHandler; + protected WWNTestAccountHandler bridgeHandler; protected Thing thing; - protected NestBaseHandler thingHandler; - private Class> thingClass; + protected WWNBaseHandler thingHandler; + private Class> thingClass; - private NestTestHandlerFactory nestTestHandlerFactory; + private WWNTestHandlerFactory nestTestHandlerFactory; private @NonNullByDefault({}) ClientBuilder clientBuilder; private @NonNullByDefault({}) SseEventSourceFactory eventSourceFactory; - public NestThingHandlerOSGiTest(Class> thingClass) { + public WWNThingHandlerOSGiTest(Class> thingClass) { this.thingClass = thingClass; } @BeforeAll public static void setUpClass() throws Exception { ServletHolder holder = new ServletHolder(servlet); - server = new NestTestServer(SERVER_HOST, SERVER_PORT, SERVER_TIMEOUT, holder); + server = new WWNTestServer(SERVER_HOST, SERVER_PORT, SERVER_TIMEOUT, holder); server.startServer(); } @@ -168,18 +167,18 @@ public void setUp() throws ItemNotFoundException { ComponentContext componentContext = mock(ComponentContext.class); when(componentContext.getBundleContext()).thenReturn(bundleContext); - nestTestHandlerFactory = new NestTestHandlerFactory(clientBuilder, eventSourceFactory); + nestTestHandlerFactory = new WWNTestHandlerFactory(clientBuilder, eventSourceFactory); nestTestHandlerFactory.activate(componentContext, - Map.of(NestTestHandlerFactory.REDIRECT_URL_CONFIG_PROPERTY, REDIRECT_URL)); + Map.of(WWNTestHandlerFactory.REDIRECT_URL_CONFIG_PROPERTY, REDIRECT_URL)); registerService(nestTestHandlerFactory); - nestTestHandlerFactory = getService(ThingHandlerFactory.class, NestTestHandlerFactory.class); + nestTestHandlerFactory = getService(ThingHandlerFactory.class, WWNTestHandlerFactory.class); assertThat("Could not get NestTestHandlerFactory", nestTestHandlerFactory, is(notNullValue())); bridge = buildBridge(); thing = buildThing(bridge); - bridgeHandler = addThing(bridge, NestTestBridgeHandler.class); + bridgeHandler = addThing(bridge, WWNTestAccountHandler.class); thingHandler = addThing(thing, thingClass); createAndLinkItems(); @@ -203,13 +202,13 @@ public void tearDown() { protected Bridge buildBridge() { Map properties = new HashMap<>(); - properties.put(NestBridgeConfiguration.ACCESS_TOKEN, + properties.put(WWNAccountConfiguration.ACCESS_TOKEN, "c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc"); - properties.put(NestBridgeConfiguration.PINCODE, "64P2XRYT"); - properties.put(NestBridgeConfiguration.PRODUCT_ID, "8fdf9885-ca07-4252-1aa3-f3d5ca9589e0"); - properties.put(NestBridgeConfiguration.PRODUCT_SECRET, "QITLR3iyUlWaj9dbvCxsCKp4f"); + properties.put(WWNAccountConfiguration.PINCODE, "64P2XRYT"); + properties.put(WWNAccountConfiguration.PRODUCT_ID, "8fdf9885-ca07-4252-1aa3-f3d5ca9589e0"); + properties.put(WWNAccountConfiguration.PRODUCT_SECRET, "QITLR3iyUlWaj9dbvCxsCKp4f"); - return BridgeBuilder.create(NestTestBridgeHandler.THING_TYPE_TEST_BRIDGE, "test_account") + return BridgeBuilder.create(WWNTestAccountHandler.THING_TYPE_TEST_BRIDGE, "test_account") .withLabel("Test Account").withConfiguration(new Configuration(properties)).build(); } diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestBridgeHandler.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestAccountHandler.java similarity index 63% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestBridgeHandler.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestAccountHandler.java index bda2051ff79de..4db40b24aa0a8 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestBridgeHandler.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestAccountHandler.java @@ -10,32 +10,31 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.test; +package org.openhab.binding.nest.internal.wwn.test; -import static org.openhab.binding.nest.internal.NestBindingConstants.BINDING_ID; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.BINDING_ID; -import java.util.Collections; import java.util.Properties; import java.util.Set; import javax.ws.rs.client.ClientBuilder; -import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException; -import org.openhab.binding.nest.internal.handler.NestBridgeHandler; -import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier; +import org.openhab.binding.nest.internal.wwn.exceptions.InvalidWWNAccessTokenException; +import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler; +import org.openhab.binding.nest.internal.wwn.handler.WWNRedirectUrlSupplier; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingTypeUID; import org.osgi.service.jaxrs.client.SseEventSourceFactory; /** - * The {@link NestTestBridgeHandler} is a {@link NestBridgeHandler} modified for testing. Using the + * The {@link WWNTestAccountHandler} is a {@link WWNAccountHandler} modified for testing. Using the * {@link NestTestRedirectUrlSupplier} it will always connect to same provided {@link #redirectUrl}. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestTestBridgeHandler extends NestBridgeHandler { +public class WWNTestAccountHandler extends WWNAccountHandler { - class NestTestRedirectUrlSupplier extends NestRedirectUrlSupplier { + class NestTestRedirectUrlSupplier extends WWNRedirectUrlSupplier { NestTestRedirectUrlSupplier(Properties httpHeaders) { super(httpHeaders); @@ -48,19 +47,19 @@ public void resetCache() { } } - public static final ThingTypeUID THING_TYPE_TEST_BRIDGE = new ThingTypeUID(BINDING_ID, "test_account"); - public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_TEST_BRIDGE); + public static final ThingTypeUID THING_TYPE_TEST_BRIDGE = new ThingTypeUID(BINDING_ID, "wwn_test_account"); + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_TEST_BRIDGE); private String redirectUrl; - public NestTestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory, + public WWNTestAccountHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory, String redirectUrl) { super(bridge, clientBuilder, eventSourceFactory); this.redirectUrl = redirectUrl; } @Override - protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException { + protected WWNRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidWWNAccessTokenException { return new NestTestRedirectUrlSupplier(getHttpHeaders()); } } diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestApiServlet.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestApiServlet.java similarity index 93% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestApiServlet.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestApiServlet.java index 1f207a1789a8a..bab27bbe6a4e9 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestApiServlet.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestApiServlet.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.test; +package org.openhab.binding.nest.internal.wwn.test; -import static org.openhab.binding.nest.internal.NestBindingConstants.*; -import static org.openhab.binding.nest.internal.rest.NestStreamingRestClient.*; +import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*; +import static org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient.*; import java.io.IOException; import java.io.InputStreamReader; @@ -39,11 +39,11 @@ import com.google.gson.reflect.TypeToken; /** - * The {@link NestTestApiServlet} mocks the Nest API during tests. + * The {@link WWNTestApiServlet} mocks the Nest API during tests. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ -public class NestTestApiServlet extends HttpServlet { +public class WWNTestApiServlet extends HttpServlet { private static final long serialVersionUID = -5414910055159062745L; @@ -52,7 +52,7 @@ public class NestTestApiServlet extends HttpServlet { private static final String UPDATE_PATHS[] = { NEST_CAMERA_UPDATE_PATH, NEST_SMOKE_ALARM_UPDATE_PATH, NEST_STRUCTURE_UPDATE_PATH, NEST_THERMOSTAT_UPDATE_PATH }; - private final Logger logger = LoggerFactory.getLogger(NestTestApiServlet.class); + private final Logger logger = LoggerFactory.getLogger(WWNTestApiServlet.class); private class SseEvent { private String name; diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestHandlerFactory.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestHandlerFactory.java similarity index 79% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestHandlerFactory.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestHandlerFactory.java index bafe947b4b27a..0f88bf2650583 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestHandlerFactory.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestHandlerFactory.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.test; +package org.openhab.binding.nest.internal.wwn.test; import java.util.HashMap; import java.util.Hashtable; @@ -20,8 +20,8 @@ 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.wwn.discovery.WWNDiscoveryService; +import org.openhab.binding.nest.internal.wwn.handler.WWNAccountHandler; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -38,12 +38,12 @@ import org.osgi.service.jaxrs.client.SseEventSourceFactory; /** - * The {@link NestTestHandlerFactory} is responsible for creating test things and thing handlers. + * The {@link WWNTestHandlerFactory} is responsible for creating test things and thing handlers. * - * @author Wouter Born - Increase test coverage + * @author Wouter Born - Initial contribution */ @NonNullByDefault -public class NestTestHandlerFactory extends BaseThingHandlerFactory implements ThingHandlerFactory { +public class WWNTestHandlerFactory extends BaseThingHandlerFactory implements ThingHandlerFactory { public static final String REDIRECT_URL_CONFIG_PROPERTY = "redirect.url"; @@ -54,7 +54,7 @@ public class NestTestHandlerFactory extends BaseThingHandlerFactory implements T private String redirectUrl = "http://localhost"; @Activate - public NestTestHandlerFactory(@Reference ClientBuilder clientBuilder, + public WWNTestHandlerFactory(@Reference ClientBuilder clientBuilder, @Reference SseEventSourceFactory eventSourceFactory) { this.clientBuilder = clientBuilder; this.eventSourceFactory = eventSourceFactory; @@ -62,7 +62,7 @@ public NestTestHandlerFactory(@Reference ClientBuilder clientBuilder, @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return NestTestBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID); + return WWNTestAccountHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID); } @Activate @@ -82,10 +82,11 @@ public void modified(Map config) { @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (thingTypeUID.equals(NestTestBridgeHandler.THING_TYPE_TEST_BRIDGE)) { - NestTestBridgeHandler handler = new NestTestBridgeHandler((Bridge) thing, clientBuilder, eventSourceFactory, + if (thingTypeUID.equals(WWNTestAccountHandler.THING_TYPE_TEST_BRIDGE)) { + WWNTestAccountHandler handler = new WWNTestAccountHandler((Bridge) thing, clientBuilder, eventSourceFactory, redirectUrl); - NestDiscoveryService service = new NestDiscoveryService(handler); + WWNDiscoveryService service = new WWNDiscoveryService(); + service.setThingHandler(handler); // Register the discovery service. discoveryService.put(handler.getThing().getUID(), bundleContext.registerService(DiscoveryService.class.getName(), service, new Hashtable<>())); @@ -101,11 +102,11 @@ public void modified(Map config) { */ @Override protected void removeHandler(ThingHandler thingHandler) { - if (thingHandler instanceof NestBridgeHandler) { + if (thingHandler instanceof WWNAccountHandler) { ServiceRegistration registration = discoveryService.get(thingHandler.getThing().getUID()); if (registration != null) { // Unregister the discovery service. - NestDiscoveryService service = (NestDiscoveryService) bundleContext + WWNDiscoveryService service = (WWNDiscoveryService) bundleContext .getService(registration.getReference()); service.deactivate(); registration.unregister(); diff --git a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestServer.java b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestServer.java similarity index 88% rename from itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestServer.java rename to itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestServer.java index a531f5f734e31..a3cb848ccd200 100644 --- a/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/test/NestTestServer.java +++ b/itests/org.openhab.binding.nest.tests/src/main/java/org/openhab/binding/nest/internal/wwn/test/WWNTestServer.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.nest.test; +package org.openhab.binding.nest.internal.wwn.test; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -24,11 +24,11 @@ * * Based on {@code TestServer} of the FS Internet Radio Binding. * - * @author Velin Yordanov - initial contribution + * @author Velin Yordanov - Initial contribution * @author Wouter Born - Increase test coverage */ -public class NestTestServer { - private final Logger logger = LoggerFactory.getLogger(NestTestServer.class); +public class WWNTestServer { + private final Logger logger = LoggerFactory.getLogger(WWNTestServer.class); private Server server; private String host; @@ -36,7 +36,7 @@ public class NestTestServer { private int timeout; private ServletHolder servletHolder; - public NestTestServer(String host, int port, int timeout, ServletHolder servletHolder) { + public WWNTestServer(String host, int port, int timeout, ServletHolder servletHolder) { this.host = host; this.port = port; this.timeout = timeout; diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/access-token-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/access-token-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/access-token-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/access-token-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/camera-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/camera-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/camera-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/camera-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/error-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/error-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/error-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/error-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/smoke-detector-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/smoke-detector-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/smoke-detector-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/smoke-detector-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/structure-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/structure-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/structure-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/structure-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/thermostat-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/thermostat-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/thermostat-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/thermostat-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-data.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data-empty.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data-empty.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data-empty.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data-empty.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data-incomplete.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data-incomplete.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data-incomplete.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data-incomplete.json diff --git a/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data.json b/itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data.json similarity index 100% rename from itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/data/top-level-streaming-data.json rename to itests/org.openhab.binding.nest.tests/src/main/resources/org/openhab/binding/nest/internal/wwn/dto/top-level-streaming-data.json