From 00c17558b16211d363ed5d70cdc21b0898f0f740 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 Port nestdeviceaccess add-on to OH3 based on: https://github.com/bhigg-code/openhab-addons/tree/2.5.x/bundles/org.openhab.binding.nestdeviceaccess Rework code so WWN and SDM are supported by existing binding Fixes #8664 Also-by: Brian Higginbotham Signed-off-by: Wouter Born --- .../org.openhab.binding.nest/sdm/README.md | 159 +++++++ .../sdm/doc/logo-google-nest_480.png | Bin 0 -> 17293 bytes .../sdm/doc/nesthello.jpeg | Bin 0 -> 3169 bytes .../sdm/doc/nestthermostat.jpeg | Bin 0 -> 3849 bytes .../nest/internal/NestHandlerFactory.java | 135 ------ .../discovery/NestDiscoveryService.java | 171 ------- .../internal/sdm/SDMBindingConstants.java | 92 ++++ .../internal/sdm/SDMThingHandlerFactory.java | 77 +++ .../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 | 105 +++++ .../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 | 316 +++++++++++++ .../sdm/handler/SDMCameraHandler.java | 199 ++++++++ .../sdm/handler/SDMThermostatHandler.java | 356 ++++++++++++++ .../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} | 10 +- .../dto/WWNStructure.java} | 22 +- .../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} | 6 +- .../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 163 files changed, 7467 insertions(+), 1039 deletions(-) create mode 100644 bundles/org.openhab.binding.nest/sdm/README.md create mode 100644 bundles/org.openhab.binding.nest/sdm/doc/logo-google-nest_480.png create mode 100644 bundles/org.openhab.binding.nest/sdm/doc/nesthello.jpeg create mode 100644 bundles/org.openhab.binding.nest/sdm/doc/nestthermostat.jpeg 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} (95%) rename bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/{data/Structure.java => wwn/dto/WWNStructure.java} (94%) 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} (92%) 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/sdm/README.md b/bundles/org.openhab.binding.nest/sdm/README.md new file mode 100644 index 0000000000000..13a69eb01dfc5 --- /dev/null +++ b/bundles/org.openhab.binding.nest/sdm/README.md @@ -0,0 +1,159 @@ +# NestDeviceAccess Binding + +This binding integrates Nest products through the [Google Smart Device Management (SDM) API](https://developers.google.com/home/smart-device-management). + +![Nest Logo](doc/logo-google-nest_480.png) + +_If possible, provide some resources like pictures, a YouTube video, etc. to give an impression of what can be done with this binding. You can place such resources into a `doc` folder next to this README.md._ + +## Supported Things + +The NestDeviceAccess Binding will support things allowed by the Google Smart Device Management (SDM) API. Currently +the binding implements the Thermostat and device traits for Nest products defined at the [SDM traits](https://developers.google.com/nest/device-access/traits). + + +Thermostat Trait - Currently supported (Tested against generation 2 and 3 Nest Thermostats) + +![Nest Thermostat](doc/nestthermostat.jpeg) + +Doorbell Trait - Currently in testing + +![Nest Hello](doc/nesthello.jpeg) + +Camera Trait - (Needs to be implemented) + + +## Discovery + +The NestDeviceAccess binding works through discovery by leveraging the Google SDM API to perform a devices trait call to get all devices allowed by the accessToken. The devices are then enumerated to identify the "Type" of device. If it is a Thermostat or Doorbell "Type" then the device will be added to the inbox. + +Once added to the inbox, the device can be added as a thing. The thing will import several default properties to allow communication with the SDM API. + +Note: You MUST configure the discovery service through the services/cfg folder.. The format is listed under the BINDING CONFIGURATION section of this document. The file must be named nestdeviceaccess.cfg +## Binding Configuration + +'# Configuration for the Nest Device Access Binding +# +'# There is general project information for Google that must be provided in order to discover Nest products +'# The configuration data is a per project configuration and can be changed by the user. +'# A sandbox project created by Brian Higginbotham @BHigg was created and listed below for testing purposes only +'# Use the project at your own risk for testing or create your own project through Google and enable the SDM APIs for individual use. +'# Google Pubsub properties added for Doorbell eventing capabilities. You will need to enable a PubSub in your google project and tie a service account with view access to the pubsub subscription and topic that are created. More instructions can be found on Google's website. +'# Note this project is limited in nature as a sandbox project to 30 API calls/min by Google. +'# +'# +'#projectId is the Google project provided through the project creation process +projectId= + +'#clientId is the Google clientId for your application +clientId= + +'#clientSecret is the Google clientSecret used to fetch the initial and refresh accessTokens +clientSecret= + +'#authorizationToken is used to authorize your devices with the project and provide the user with a unique refresh and first time access token. +authorizationToken= + +'#refreshToken is used to get accessTokens from the application +refreshToken= + +'# NEW PROPERTIES for the Google Pubsub configurations. Optional for Thermostat. Mandatory for Doorbell and Camera + +'#ServiceAccount information that is used to get pubsub information +serviceAccountPath= + +'#SubscriptionId for the PubSub that was enabled.. This is a named value when creating the pubsub subscription +subscriptionId= + +'#pubsubProjectId is a project identifier that was provided to you when creating the pubsub.. ex openhab-nest-int-XXXXXXXXXX +pubsubProjectId= + + +## Thing Configuration + +refreshInterval is used to tell the thing to refresh status (in seconds) and is required. + +## Channels + + +| channel | type | description | +|------------------|--------|-------------------------------------| +|-------- Thermostat Thing----------------------------------------| +| thermostatName | Text | This is the name of the Thermostat | +| thermostatHumidtyPercent | Number:Length | This is the Humidity Percentage | +| thermostatAmbientTemperature | Number:Dimensionless | This is the ambient Temperature | +| thermostatTemperatureCool | Number:Dimensionless | This is the Cool Temperature Reading for the Thermostat (Only valid for Cool and Heat-Cool)| +| thermostatTemperatureHeat | Number:Dimensionless | This is the Heat Temperature Reading for the Thermostat (Only valid for Heat-Cool and Heat)| +| thermostatCurrentMode | Text | This is the current mode of the HVAC +| thermostatCurrentEcoMode | Text | This is the current mode of the Eco Setting for HVAC +| thermostatTargetTemperature | Number:Dimensionless | This is a aggregate temperature setting for the thermostat (Only valid for Heat and Cool) +| thermostatMinTemperature | Number:Dimensionless | This is a setting used for Eco and Heat-Cool HVAC Mode +| thermostatMaxTemperature | Number:Dimensionless | This is a setting used for Eco and Heat-Cool HVAC Mode +| thermostatScaleSetting | Text | This is the Scale setting for the Thermostat (FAHERNHEIT or CELSIUS) +| ----------------------------------------------------------------| +|--------- Doorbell Thing-----------------------------------------| +|doorbellSoundEventImage | Image | This is a generated image based on a Sound event. The event has an event ID that is required to generate the image. | +|doorbellMotionEventImage | Image | This is a generated image based on a Motion event. The event has an event ID that is required to generate the image. | +|doorbellPersonEventImage | Image | This is a generated image based on a Person event. The event has an event ID that is required to generate the image. | +|doorbellChimeEventImage | Image | This is a generated image based on a Chime event. The event has an event ID that is required to generate the image. | +| doorbellChimeLastEventTime | Text | This is the time a door Chime event was last received | +| doorbellChimeEvent | Switch | This is a switch that flips when a Chime event is received | +| doorbellPersonEvent | Switch | This is a switch that flips when a Person event is received | +| doorbellPersonLastEventTime | Text | This is the time a Person was detected by the doorbell | +| doorbellMotionEvent | Switch | This is a switch that flips when a Motion event is received | +| doorbellMotionLastEventTime | Text | This is the time a Motion event was last received | +| doorbellSoundEvent | Switch | This is a switch that flips when a Sound event is received | +| doorbellSoundLastEventTime | Text | This is the time a Sound event was last received | +| doorbellLiveStreamUrl | Text | This is the generated Live Stream URL when a motion is detected. Note: The URL includes a token that can be used to view an rtsps stream..| +| doorbellLiveStreamExpirationTime | Text | This is the Live Stream Expiration time when the token and URL must be generated again | +| doorbellLiveStreamExtensionToken | Text | This is the Live Stream Extension Token that is used to request an extension to the initial LiveStreamUrl embedded Token. | +| doorbellLiveStreamCurrentToken | Text | This is the Live Stream Current Token that is embedded in the LiveStreamURL. | +|-----------------------------------------------------------------| + +## Full Example + +#Demo.sitemap +Frame label="Dining Room Thermostat" icon="temperature"{ + Switch item=NestDiningRoomThermostat_CurrentMode label="HVAC Mode" mappings=[OFF="OFF",COOL="COOL",HEAT="HEAT",HEATCOOL="HEATCOOL"] icon="climate" + Text item=NestDiningRoomThermostat_AmbientTemperature label="Current Ambient Temperature" icon="temperature" + Text item=NestDiningRoomThermostat_HumidityPercentage label="Current Humidity" icon="humidity" + Setpoint item=NestDiningRoomThermostat_TargetTemperatureSetting label="Target Temperature [%d]" minValue=65 maxValue=80 step=1 visibility=[NestDiningRoomThermostat_CurrentMode=="COOL",NestDiningRoomThermostat_ScaleSetting=="FAHRENHEIT"] +} + +Text label="Front Door" icon="door"{ + Frame label="Doorbell" icon="door"{ + Switch item=NestFrontDoorDoorbell_Has_Chime label="Has Chime" mappings=[OFF="OFF",ON="ON"] + Image item=NestFrontDoorDoorbell_ChimeEventImage label="Front Door Image" visibility=[NestFrontDoorDoorbell_Has_Chime=="ON"] + Text item=NestFrontDoorDoorbell_ChimeLastEventTime label="Last Chime Event Time" visibility=[NestFrontDoorDoorbell_Has_Chime=="ON"] + Switch item=NestFrontDoorDoorbell_Has_Sound label="Has Sound" mappings=[OFF="OFF",ON="ON"] + Image item=NestFrontDoorDoorbell_SoundEventImage label="Front Door Image" visibility=[NestFrontDoorDoorbell_Has_Sound=="ON"] + Text item=NestFrontDoorDoorbell_SoundLastEventTime label="Last Chime Event Time" visibility=[NestFrontDoorDoorbell_Has_Sound=="ON"] + Switch item=NestFrontDoorDoorbell_Has_Motion label="Has Motion" mappings=[OFF="OFF",ON="ON"] + Image item=NestFrontDoorDoorbell_MotionEventImage label="Front Door Image" visibility=[NestFrontDoorDoorbell_Has_Motion=="ON"] + Text item=NestFrontDoorDoorbell_MotionLastEventTime label="Last Motion Event Time" visibility=[NestFrontDoorDoorbell_Has_Motion=="ON"] + Switch item=NestFrontDoorDoorbell_Has_Person label="Has Person" mappings=[OFF="OFF",ON="ON"] + Image item=NestFrontDoorDoorbell_PersonEventImage label="Front Door Image" visibility=[NestFrontDoorDoorbell_Has_Person=="ON"] + Text item=NestFrontDoorDoorbell_PersonLastEventTime label="Last Motion Event Time" visibility=[NestFrontDoorDoorbell_Has_Person=="ON"] + Text item=NestFrontDoorDoorbell_LiveStreamExpiration label="Live Stream Expiration" + Text item=NestFrontDoorDoorbell_LiveStreamUrl label="Live Stream URL" + Text item=NestFrontDoorDoorbell_LiveStreamExtensionToken label="Live Steam Extension Token" + Text item=NestFrontDoorDoorbell_LiveStreamCurrentToken label="Live Steam Token" + } + } + +## Any custom content here! + +The NestDeviceAccess Binding is built with a sandbox project in Google. This means that there is a limit of 30 requests to the API/min. This is used for testing. However, if you switch the services/nestdeviceaccess.cfg configuration for the binding to use a different projectId and clientId, you can use this binding on a better project. + +To configure the discovery service, you must place a nestdeviceaccess.cfg in the services dir + +You only need either the authorizationToken or refreshToken. If you use the authorizationToken, the binding will fetch your refreshToken and add it to the openhab log file (Make sure you update the nestdeviceaccess.cfg file with your refreshToken.) Otherwise, go through the linked instructions below and get your refreshToken and update the nestdeviceaccess.cfg file. + +It is pretty easy to see if the nest discovery works, if the parameters are in the nestdeviceaccess.cfg file, when you go to the inbox and try to add a NestDeviceAccess thing, it will start the discovery. Otherwise, it will ask for the parameters manually. + +Make sure you follow the instructions on [Google Nest Authorization instructions](https://developers.google.com/nest/device-access/authorize) in order to get your initial Authorization and Refresh token. You can store those in the nestdeviceaccess.cfg file for configuration of the discovery service. + +If you have a doorbell/camera or want to utilize the eventing capability for devices, including thermostats, then you need to setup a Pubsub in your Google projects. [Google PubSub Creation](https://developers.google.com/nest/device-access/subscribe-to-events) + +I've included a sample project projectId, clientId, and clientSecret in the nestdeviceaccess.cfg for testing purposes only. You can get the authorizationToken per the above instructions and I will output your refreshToken and initial accessToken in the openhab.log file. You will need to update the nestdeviceaccess.cfg file with this data after initial usage. + diff --git a/bundles/org.openhab.binding.nest/sdm/doc/logo-google-nest_480.png b/bundles/org.openhab.binding.nest/sdm/doc/logo-google-nest_480.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6356138e9a4319efb962622de17a08435a65e0 GIT binary patch literal 17293 zcmeIZvHAhq9mo5(*-XAl*o}3o8mrNOve9DIi_H zS+DE<{+<`#&+`vFH*c1mojHBZoSAdxg|?;&5dkd$1Og#?sQN$`0>S2jKroH*Zh(Dov9F$oy{|va+YTac>tSuj{Ll^NV5e&bvkmn6VkZTGU`0FW8~Yk-s7u&* zxbeWyb9e&WJi%%RL`pWm6K3OL=gVwu=iuZn&AQv#$;#|xE6r*otih+@sc7fuq#ERH zrx&EDZxiHVBW}woE5j@mAOQfl+4;hl1KeEQeIx>;S^vV70RN+hd0CnNPVse-X8ngK zV-0O)MGtQ~W?>!?s13i6FteyQ55I_@khnNEvj88z7%!g~FTWs^UqnKPPeMR|`QJZQ zfX&<1UPAYQ^1ryiH)&Q!UtdoNUS5BHe;$899uIE^UVd?Lab7+FUI76pm;v<(boYe? zK;3=V{sZBGosW&Tlc%qfhdVPGBFx&u&sUliQ2GxGZk`$%{{z_F=U+hqk?{t=JbC$f z_;}si(0u)!?c=L!_y3dezs~m25A?L-)wT2S@bk6-`mksF&tf3E|2rb|L_m#%mbVj7 z3e5F^hmD_`oxAVD2hyzI7am(DTL~KhQ5${%aWN>L5Wh84$i@~1h1v4kLIv$bL_~!J z1Yx3f;{Sp3zpWQh65xBFAj+pGE+Ed&uOu!msw}RcBrN_wi2s3zqKNW;);@Ii@rAkD z*!>5$6Ttn?S`p>{>skp#Z#$T;hqu0mhwFc=K-~vBdgmknfyib(d?2qM zFugTr8E0mc&2+@|BJ=i(O}A;|>G+A;N3PXJH-1M~&}hiUW$`@mrDwh|87aiv75(j1 zR_JZnY8*m~=MlqW59KI2 zaf^RQ=6)ggRk~bHKh{>XtaY$L;&^@#w@GhQwOqE-r)(qJBTKeQI&l9zIwE{VZK|O^ ze?BnA03WtW|NZ!{2>xpY|8G0O%JW-tS35bE15ECONE@YJguH zz@=hNu4VChUt}~k)`5gYJ7pqUDlm^A@d=IU*H1PS54deFl2tx_f=gvb-@flY&*-QD zfh_VNDEVN=gW1{8XxIZW2^3$Z*i?0DU=>X?^-BWsVNiD#rBp%md;&NR)%9=(qQ!h!eS?6YV zEYXNE1d`j1p!9@z{pqom(kxZIyF=BznCEd!27x$I!*Jq$T}_STd$Q6Cati3c+GAfT zc!KCx!CW38Ju4e5!5!0VhR5zLK)^GuPKg1Du)k&4<+DsnF&FT5IjND)z_x;>(rZsn z9ALSPpd>{(G0q`b)BOHap9IxKIk5tP_#g>PwgHab*d?I^cNm3|o)m+d*YfIi2gvK> zr17~N3m9B@P7Z+xu$AYME$=YC>vmdFC+0*@!IDwwfR-*_e6Hep0Gx?8nk5RV`f$ZPkUOU- zFgTY20^wso5;$)HHrj_uxD#%VIaOh_uhwi^djXiN-9C{LA@STa$({NsK_S}~&w*2B5XfiszkVjDHG~s8 z#4b4!w1y-S=5qrnSSuSQRsf2gp#&A5X=}@C#??X~5iz$P%fpwaDd?gSpI*#yM(1-k zq+miI-}!5wmIIvy)zDOGuoUMKBLiC~2TdUmW->Oe&kFP!!(0i3rScl)TnQ_{#$>gD zxr))laP<7Iw|+HSH$7kF7|UJd?!bpY@Za7*>Z70!aiemd+T*S5b=8snJ68|Bezx*$ zZ2zqe5$&DKLD^bUJP3q7(}`K#%q+)Rn?^(aV2(X{M4{XUSkXCixmDis8AT-0kzksw z+@{7cSPufB$pN#~&2pAU6tv}A5o8Zc`HQh2kWXMT>+;2A8K?ZCs0epbEJ@%flzuiwBf6GnXHV9QrdV(DeNUz(Vu*<0J?wp>1tb?AVmm)PL8$s(xD2lWVQb zSfq~yzgr9xjg6p;bEyCnS)j>Fd|v(3s3rW!7mstG40}rrjUE57)obZF+29t_c{2n2 z$&_4R2e-%#tIN2Rr@>Mj@UZ z<72!ZXR%hVxNkb{UXDuRjGaT!dBmC=URivvjeGC5Yhb3n2bx3mm?X+LiBgtgv7%d# zTMTac+Xn_EGptr{WU*yg)0iP3Y!}VY&Gf%;k%0B_lBUvIQ+P`~%=C7+fr+QhBOK8m zmBiA;DjuLI(_=1oY_9zvZhAAkrcvY9A1Ur=Jx?Tmu}iO*=3Nu+=#MCCtO1U{h+kCa zt@IJw<8iv`NdyjJ#?%V74G;NnVN>PjyMK&A3$0Mqco-Z6YmMg1i+dgb6UI>LR9c&I z>_K_!Bv`5c#dojVkz*!VQ=dj@m^Yz5a%Xh7`hW*PwI&!EgBIQAi0ZudEaef|QvWKc zAe85wsDWM4!A`zuYjMVAbuzT(J!`IR&H4UleKFje@-J{TWsW`rLRs;o|8vmhBU}YQrw&y-1 z+*}(Y?QwfZLFsN09T!trbup6MEz-X)okX7^LSG%rDQvA$508_Wtz3iue(`6;DPq{n z9I5f_X zfE3?{DMg|x)U--_I-;DIc6(3^U-Vsbj;({Ym9;UpDLbx1;<4m3@VRI?-3>Na-L4hZ zMAXk|*p;sO;2Wu;S<5R=fMGOM2c7n13P9v8@wn1mn)yntJKcK*wi|Rud*l7M;qD%s z#SrB)H3;O8t=utJtknAP-J=x~V_f;QhHUGuQ4X+?LAC+1&Sp4Q*r$# z;ktF-Uit2)qp+!(ZBE5@lxH5`I)eJpkk>NJ01%(RrZ&W#Aez7^+hBZgIT5zvwTH?g zC)ju1Mfs69LRfIBV@VgE4W7m8f<2Mu@vs!Ec=M~N!*%E$*Y&qfsKxmauf8pC=!hd@ ztyJg|1PfYC1!!#P-)@ zcx75oH{Gg|axrt~c*-q0YHRkFaIp#?#mZ)*0U{sG-BNQVW;mxckulK$ZGm;B^O>`o zou@|^S}|-3mw|f+;a3@`*SzzNHfg{dv0Ya9i*7|=lp&^?vDSRT-YaIOf znG=hk?8O8sY4_^k%*F?x0%nQ&pwpOGOZ4WShr#H3Dz~kERI32qG@oTEa?ED{CAbO*}?Bc-e zVjKs$6tBDqNu-7;UHa+GesK&QvAI5MFC4Wy?fEuis!0N|fEDsFU_)xz%8P4yGgP`M zp1tPSJutnV(u%db#O=KHty6j#>bgb(Ia-{QJvRuueM)la$X2QB|J4> z=a5AJA%@uobM|G&D4WC0gEmb11B4bbSRQinQ900uYsoyD zJ0ZIdv+E~5m-ElP)#2Zp0}AnjZUi$Km03-J=TAOhMy&b9){V%cja#6o9G&^a1bqqG z*#fM33@%iiy+cy@QORH};-EvD`A7c;mUb9c$je7}i&HcivVWo-bZjQ$D6SM@B?_ht zae3_AYwOnolKZ1i1-YkUtixOXI3=X6-3ezzVFw^>-PH$4kZzSrT^k5Q<+Yeyws>65 z%YZmOOL(E?pg52rW^50DnMnj`>-$H93FPy3C&W8t+z|EX>O2_yuyzt{&epX@I8{mwt9%obEI*miT)_TIOoEN}4dTknWvW`c36_>s+V23Z8cy7cM zNI57}iwqQJZa3lZp4QE|1`0}k3O~zrZG}=s3Lz*Dhn%JffmRqVcLrlsu9t21e#VM^ zI^|L}fOs;$`$65vc*(vrchtMpHq@cHJsZ!4ASc9a${A+zK+VVpx~BHN8Su1o^OtIP ze7~bn#ZBDjc>|$ro!d)d7K*Ke>O98P^pm{!tAb}*6E%2GP`qLLs}|S4DnQD7?uonD z)u)LxnR>DQCR{3IokuGg^1f{$d!wNvvXw@%X-O>YgoP@*l_=qq4<6P{QS{{Zvi5f4 zhiy%RQTST6OY6t@2A@#EU{b?T#i9~kSD@&(RQoHPXM(Ml4xvRLpY!eiF@iX5)P>8O zNzhkpF2-ZVpt2Z-@zY;0EJdA!?4SlbEIs)R&!IbxooO6(bIrqndNQf4dlLTLYj|zz z_4l>1ixobB2$%)+xXLA}I*gSJZ#dant&+VCmzDB5XfT+-NS|_|p?GE~cV-xvv@q3c zbENP&@weU9<{sVjjgOKVmfdWcd+Qn{uO%x<%@8FYE}Q6xEgQ{^TMIM1X6Bx6H}In* zq6TZVri0RHI8vEfpZxTwgz^dj-IiT19_K8~`*w3cYGA$H*BwMm*HV{k+ z0}iuOL`b3JYLWgCKa=T>Z|-;;$n~O0IJJOBE8q08%F4mqQ5vQqGUno2{V!Cs6AK8- z)!&>({Rtq6Y8Un5ekxWK+gZpP{=VG__m)yuTijTfBvxS9I$VmeYYH(3!og=Oo-Lep z5SS8!@AT2r?xR8&L+ozIb?@~c2>!U57?7jtU2R!Sqo$-Lo5=}^m%r8-K`J*nENyg< z`a!VH*!|)8VN6go#UNc2m8nhKd@p>PQY9_e(b>xW($Hw!5d zZ5-#J+mg@S`DY0P#;Sk&yi*+h<40#QG)$C*3`+_#N}!*=&xAtVP0eT`bvT_~wVugx zo_s`Dwf1`dFw|`Rs9Z@Q1CL99ih3f`$qT>SD9Nyi&r;{c(T}eMD!X{xj(MIRnZlp^ zwPwX<4;}K}FwF63ux64_(_ZOz`@YfYpEwVj9B7o^Zp=Js&&>C9k3Q6Efrq|>s$+eb zagDV+?`Cu8_%4r`qo|!W`M5RZH&1ufUO%U4YrOl3Qkb@oZ1E&VTNTY&4xS*n_8t0AJk z?Qd{5n*jFhzHo*KX(hRqQ_FH&*VB9ChCeGRx>a`>#Sg|BR-bWa>SLO)`l(bH!CCE7 zgk@`d{4MTUc-9j?QF4`)M@Q6{ve)n_nKJ0`QK$wl8#gFH?<=y*>AxQ2=km@KWh2R$pCl!jv*5aSNGqQ&JBYHO9oa)R6lU!Qt_n@p%P#e{ z(siC^H_RQlHS`CLb3|i=J2q;zWurF(weHBgov=zbf`kRM5g@K~*a?xdX!5^9aOPw?GE2o2>Hj$p(n#Bi_ul0Hx zLl~E&g$V8bG?FJpEXEFt7!brCy|b+Jdufw%Q0mL_M&XF>Q38C7|Th9@_o_K z_P!*%{6uRvT{hCbUi_*OrXu=3oI5AcenxNldox$9Yx*f0mR@>Z8z3mE99vNV^ZLTy zS!ey%AmO~DFyUdJ^*a)x-HxdrvC|#26B%Gp1M=<&%Ebe`!pbfEKi1D!$A%8{VC=z7 zmSb6O>+_~duqLckjz1<33=cJL^bF-$!g`PMn|Au@Du-JrXC3g;T42)?$$(ewMXg;GV+If%1bwfsUrr(`e0=Ic;{7)jo`P7JE$+K+U-b37C z^dKF=Ax^Et1XM3H*Bb79pV-U~IEpuyJA?K@V3%cJ7qGitN$OZ6QNNg5lP#`2o( z92mC!EtGY-Ta)jvl$vFuG%dM$EMJuwV=bD{H)}uh5%JY_uz4n2kY@eV)cR9{`fUk6 zL6US`Y^o)bvn-m@!%$Xje`L%}>3OLQYIVrxB66~5rv=s7U#5CfYWg}8)7X@f15>y) zKQbh$jnfuexO`sBc$VGRrpo>yKTuz7@nvRZVfUwq5`exR9E3`%@E(d$n7)=Zd zfor2o<0{O3yB_L_H{Opr8O}H}^fz=U8#ie6x5PYDo+zoU-4}YvF?KvKcXCzn`uF1x zGbZ9J?~S3+gza#blW8kgaAb|xlL&h!nCWeN`K5MbLT{>@YjYC%-QDZ91R|D5+@qU! zXZL=`%)@f?Bu>()QdmY*V#)dJH3KrX+HrMne|@Fx>c)H4a_0M3cOlF^bEB89@GCKf zY)d2uV>LEze#+f8{B&6P9j-CsN;<<$&+`6lz2iMMw(`%fo0lMo4!EorYOU!f?tXH_ zER~mDoGa!czDY=T&H{@ERY-ZLZA zbGu>=7!IWCqi^g|C0R*k>?SW?N3xz=5VYiR-^61(V}4a%S;2WDZ7sAwv7V3M=vE02 zi!#_s*<~SlS4$eW+%Ae^7y!g1cZbBN^vo*hU7BL1zaS;UE?KwVB)&UvtNFI)OFlng z((gQ>6=0ball_BvDHKX$jscGF;1Z{3guc8-2WtyDMu7-Mr}4wJ(*+^z22l{x7rTSS zcZ+YM;j#3<4$Dd;4LMs|n+Q=4>t%k6(-ra+(2|Xo{1%_r2uNKDxsAu23r=H!)4G_D zVuQ6RqxsoK0jWiOnkm1SU;ko0HZ|#%NBSHFtPbu?`9PzeL?u1a2B$13X>Bk+%YFHL z6GevI#y&=o*y?4gPFHXfvvlLasOHUQ_G;1_3abdppYQXrA)^Mf8p}Tr{^ELd9!tFs z{o_SV!aD3jiBbdBUac*dGF5<+O7-u1KOcc&W};@56yv?NP9Hl5XPw8LZvxt5kB_Jd&Kz5@-TsCaovXcBY!@H*KLIN?&!GCa`E2pIn58?^To|_> zYh!+X15JU2XO(T&Mv_zYnmr6?bcxb=&LKH@Ki1d9vd&_XxK3)f?4ol0({7$q-Bd|{ z+4CylxW3yI-HX<_hwo?$pCqP$6ZtnX>|C-wP4%j_=8W2IefX;&&hxqv)v$P@i!g%x z+eqsVB!U4IOSlcB52F2evU_kOf~kHX>cA%KyeXyDNrN$TXRhbCaduVw50#8-qnNji zcz4lJ?QhmLBl8I)3m(_!Z{@cjL)8Ogz^w$ziH1D99{OGFkEIO-b7o%e+F8VsT7>8` z=2TjA>@)L>bF@W--ZK1IRzZ0D)j8<%_l8?tPYz@1OnG^dv$B=!3M!m$8OI?a__;^L z33!n~BA34zMRf6w?v;NwGNt~o>GV@QSE{Oh1vDAEEV>RSdi1Cw2%O zYNH>%NgXAuzlC8+9XFt%bAmvbEuIQtCgrSd?73ad9{PbRX zPG-3u%KV%pVL0O6OE_s$Z2_*i1fD0F2K;87tOXr?$PX1yyXz8Li2}r7PX@}oLSrk( z3o1YwCgHtWCgOkdW;j-E2}3ELAKS{&&-?uY>bs=j;ihFnxyKWR^mcn;I-T|07H3yH z9mG%xleZ0IhCjDqk1QKWY<@q+)_4tJiuSRqpmxMRPyvq7Z?fF$ zo`9Tr1{$`_@Qp++GRiV+_ikG!4g*agcfVCIg-No?G(-j$09B1%j)CZ9WAa@$b= zrZnGRdND1T&Q!~NJGk3ft|MwY>PfbbP&kai-hL0nt0u z#Fy_Yp2TereA+q&(_9%y{DB5?y!ISU`;?tC|cFLOK~KCDP5U} z)>>~5TXKXQJA_x9yuPne))ashazj3zA$QyQ#KleR^2RvFl*6kmJylPcXg3|vJ{$2J z0RaNuO3D$!bFGxMqxA82_**yf@)KW@YNE;M2AkM*Bjq=8V8P`^^MCZ07YEFOD8hy_ zoTmEc$M^Ya;P&AIdA;ALLul%cNbr!5hf+NoSm{brK^}WHX-+;@!Rw*=QNOi} zsAA$tPGC@=SQD+cw^dr5uKm1PWO-?_7nyVQZ7nj!%04W#WK-R08lVs|t>Z1mYp9n| z*WcJ}v%p%bE;?iBwMOW3#<#40ijr91y4O0Oq(z8Mrh;gMPEi%%S9Dq``aMoeubVt0 zBiBzZFzyXC%*J`$&cwW!c&>;+q1smQpN#EMvq%FF`KHy!Ejkraf@2aE2_9nsb0 z+LmtSy>1A!joCfWaomIJ%xSU~eVV1jtG!cR`~;k7X@!0rUU*nu^zxdj9vHM7Y(6sF zw&z?9qAW5w`M!;fIkNc~fpexi;6Y;9(jGl}Z^bVusx9s4w2jjL)@f7Tv)_?S^>@Lo zm#Ibo8O-$i_0`(-sl}$=mS57CA1&YAtT!DyPYR!v5G(|96h$H!Fte|vg0)Ac-=E!W z`60xH3p*dNZo+eL#uNvV(P5(V3)=^>IVX`oovmGef$b;@e66cHj12>9=}Yo1Nxu#9 z`dYunm|w|OeB;H=nptFFulDtENT({3@z=Km3g-A+)5ir;Ucs)9S}gCo+4Lp*hU`G} zHroEg)E!K9(;sCI7I}EO$!qSOf?EYORTaAuHkgtIz4(E_$;uDdT=p@|%&Q%v!Kto0 zS#Ib`dPCXE0>a;q}RByxk zb30$0*W=?H%v2MI5Q){L%eVWf(?hyj=lo*GRwXc8n;Jh&pHFj`kvwEa|LPQ-pl>gSw|!!Ydhu@s4f32 zsA4s1Gi`PJu3$IsJG#%JB9u)WD^;6ty9`xc|ndNEVxu|NSJ-^`3Je_twe8T(oc!M_OHabzWF4o%!Am%Z@|e z>E6x@l7+0F-O;>b!bMmH;y?^VZQ^6!Q^WgjL#7BA`jZLWbIJpy&p2F$6SH)WOgq!o zDyw*sdzq5!*Viaa5NiUoIJ!3#V^XtP`-x^^;EQYtBH%7w%MhkC`R32=rPqB_!+Tt> zu&~fC^irkY-S%Aky|B|vPJc%Z>1>wtd?ZVyRDYcee@)BQ{mpXyLPq~68i ztOZ3S*iySeN?TCt#BGhN10cMIlb;EIrh(u=T9c+5j_sSV-UJK z-gLvwTsBU@Sm%)!=jWbh;hK)7-yO`Z#zXTZX^CcxNTiBln_c&OtaVx?YEq%=>!7`!0JcZ0J zk!%%I!orND(KpiwX@uQ*#Cuq8rnGI;+r_$);AyHCaK*voGko7n5>zraz;?~3AL_La zV(zJv`=g*C-aDNHKFr`EbAtlc@^cwSTX=5vo7g{k%o}F0oRz$y;tGdb=9F#X-6jSF3y;gv9WmEOXRWg5F5+6}lZS*( zz)sr!OoHWf3`ZxE1TBeTdb$}D#r6^y!(>x|a5m89MIv;> zW)_m|8c2ksNQ4$)jy3=Zi8cZWIRqK7X~=L&w$U~F2X&9dC%PHLYbO?Z~TSnb*3wcck;VrU5hIUzEw>8tz_pcAnMW8!eFS?6Bo$wD<+wVoB<; z4!)jTE=G&m@4DA@=K7M~@A%$n>^96ugTuRB7C1|1_PvD1^3sEgOD~ZmA3;lu+WE?{SW39Hz2m(A6go` zaA4DL*FPa$lK7!ww~8!Wo`p`tVmmGdw4KIKQ2JRVZXH@3aC=C{ev!90VGZ|N{hnW+*J7Ey(L+J% zsCi%7_s(u6+Ay-5am>t`{27wL{9^dKb-sFgFXXn-3bF=<_-bet zQTJANh9M!L(?DlNBbcF49_Khv_;8v(|5)3cdf2V@pWZj&ef@C@}6pShHtzHDJ!Z&^&7 ze=$?MY8cx*2}>VEY0?l4C9p}BhPlPoD27*-xS0*VDdQzqs34_vRk!BvV`vTKHVSZ` z=d^ruKk|6-HNk?*Tz4G(3pSS)$!V7UL4FvH1|Ij3-$y_YQ9g%r(@d{?d-&SiCLA9| zp^xA#s=&8sTitPE<03T_s%%)2+*O$Aj=}d2@F!ry6j|n19m9y1AGMac*Ub0l+-s@d zsm`*q*!|s5YQ1SdTNvf0cTa?quYM@eemjR2MwW_ z{`9sl63)CD4{1z@JaG&Q{q+$DB{tFA{8(@;bR%ncE|7sTRg zK|&#h@^s>X$4ioj$X2eB>OY{Pd`79Y;9yt5^r?Z?d?Zfc+3>j|h3?xcQ#NHoq0gX1 zx1{Jakt`jThfeX_``npu53{@S4q-~2Z)bUh{C;1pqHL5I_$ieQ^(_`2YWnwIQGda| zR#>25DPFf9g1^geHdhx738s-mlElnW$8Yt_G;_#IRO4-!+3&VXpR?cQuVodansj|L z?|#i%{^)N)+ki{?aqRT4pi-|OYx!w9kA7!K750!i78`O7+8~r+6!IUILsWz zZuqahdxCD2XygwS$qcT8{+iBTFwCUJ=Qg=>gVM-g87K5^#3%m&#omIVTMNYxPc#Fx z?!r^dwQKUXv8h^vc@H>lR<=%wf)17P(9@rXyvVu)CYg({J>KhXe*;^8%vdT?{jH68 zAWkg12t(h2Zj#&n2FrB9zo@&r6pD^B41I_=;dNPE%SyW%)~&OO4W>=2C&qd;OrUK7 zBuG~igW(4`&vIF@soGw3<$c7a46ZG{R~Nh6LM^FvIr&RiDs*y>gkj!7W7?g<7;niL zU>h2--+ES^_vET*Fl>|Q`pfJ5@2h)JU3pbEzp)kvtSFWEFiwy0lVnMDI(M}3z}66S zP#(o~DY*0K(%ck0i>lH5geSa6bDN4RWNRamH#A;-oh2(_0A+WZ4+SVKt7z`zgXw_3;VhloV$P`OR+2%E7*Tdt)3 z2-G=l*h~@iX#6=XTFxbq`lFKR+>!SQjt#H_eR}g@y%(X928^cQRKQSBoGO^g$iSOW z9l9GhHmNsR>1%pBx1M4Dw|Jo9U3kSHJP_d$!6el5gHD-cbl>QdU!se})p^6Wh%bG4 zy9GeE4~{xc?|KSyblR9CYivJ$yK{px^)?Bo6=&?bvS-l z9rO<7F;L&3|1Ai11Sczo*f!K5#|@OH{>|Xl`oO)(Hv)2-lFQ1L@s?#4Tj~{5hnE=a z2ntxAO5=bY?$MNcKAFJoXcL=&5oj{cQ#0+#cy!v@w(l!*TAzWm1J-qg4q>yq;_k%nC z1q=J}X6bOHmWvxcyT!13)}*^%lRe&&TIUxG6yvs#>CMPdr+PguncfZ1$A;ci^xj#wDqaj=u(CVb;2I>pLw}4ugsMiTOy&RPvs~=0rFm|@_ zw4J`bZV)>==zPciL$DMxH$C3KShPg%W@PNcgeJVK+22z#q}uewic-#vo|1w~n_uLm zuqj#_hwmT@_~!2Yd3~l~-d`{P+Uz!dx@nz6Hhn>owv(Eic&E*Z)AQUrlN?;im%ZLZ z`CqN?QdM2c6|5-5gMQD<2Stqd-j3*RVdn|du^!idaAQt17G}`L_1mCR zlpLlcDH++mZ?ANhldVo{GB9v4(Yi=f=DK3Jg2}Qv9yQbpj0y@m${>EAJIiwHK2+D?BEqUbC`8ow0~alIS_QQBS_^tw8$7!S`ATN@{?>ZQCj zF53I`MiowS`bm5e8f~g$&=9TG-zXXy4iaNw2U-CBPPwDMLygf}VgV;`VUu5x54Bbu zf7$z>Vb~4bA|=Ypm~jBdg5*{rD4`A#pGOhHRCFl5rb)*WaS;M`s#u}(lwDK8M$vi$ z&=Xt_`aGMPEadRG?e%S(vvH_b_?r%P4snHLi;xOwpi@;7G4onns*1|Z!&wJ(^E+!9 ze{|fADC10eX}ZGC4*E`WZ>$ZnXVNe{P8fAMIVz+Y!-nLNfreIz%q62sAI_u#QVZ9Y z@shh13+c;&0(27HD8A&kvAthF(d^?)#+4P|i^v0r7?}IVV)7s^M*fFFG0T?ld?~g`MF-ATL?V7wO6^<>`d2g7rpZ!P@{( z-Q|62X$g8E*9@MD;Ll`)t=;d`>vduQ&03Hlu_`VMUa|)1wz8XbUwAtXcVh;xkOj+o zHP>aDT9WDqznrBZkaSR*wBX!1Bc+$rUPo5KfVBg4G*-f4lDdS{tF$C zSHAUf%F{^)ULGtDTV52QQU$MrHMO{0^_)jRv;RwbTO6AjMLf4u&_m|oVIkE}pahr` zVB?}mWl(Ueueut&ejIYNnHp;ua=xH(;M8^>bYABY#g$vRfD{D0jT}#31cvw{m>2>v>HbVW40aEWi#|#Axa!<(ro1tC4_XCu8#N$p&aa{RY2+>A zu8IFqQp!FLS{8M0s-G&7fgf&pc+kSH*2}pFLN>AB}4U7f}SqxeDHAOMqGV+3A$ApZlOrXRSXVb-}kJ=kju%=T|+#e zMGo*iuEzj@oTOJj?Ze|lznaq&)`UL9=;2_ZF1`LnwWbC>VdnT+S@%)tDF-n5jqP zjI0B%4^+WJlm@^Hl6SNn$xgTr&zEXxxQhdc@CC(pH?ZM1(8SjoYZD2OE9?E)0eCe) zBw~ibaO(LwJ4T@%3PnTggx;QIH1UO#h6Dhfma)8BPd&GSm6wI^+gtfywiPCVa&!RP zjTg~q5{Z}4swdW?M-{;6I2f&J!;ke$r&UPVq3lky{`;ooHh8%5ZIqOgqA;W!N+Gmz&G>IZw`dgzCSsPRDQ46FTd(hz=jtp6QMD^Le*B0qmwAJh&m;62(9kKQ zJG)V5WQ=&dhe*tGFb5=ljsy>J$W49|Pz_>3B5s0*K2bC#h%80Tk{eM;K&pwrLMyB_ zV#mpWI^{s$xMM+6uqurctbYVv-3nsTD0uK-2nCn0es*0G>RVWa_;%Y1Lu8L`PNh%NEf8jU(lN7Hk{-MQ$LS*i9GW(;2tYh2!>F7QuBV(==8LzDbA!_(Mbq}qTsLh!T-vP@N+ zzlhXNa-M@mW1ylVfFVuG8)H1Hvw0b-3G*7DV5I8yv?e8XL9aN+<6^E6+pzWpPqetA zJgv1u7&K|5fefMd%Ohg8@pxC4-DW0Qn?RPj$L9?|1w5(7yvMCcm&VmW%!`y2^_q+2 zv$l3wgN=g6L41HBTh{olw=Y%j1mdYrwb*Xs+#*@thD#~%?a7sa<+v>65wy?cmHFrr ztSB5Q_Kx)Po)NZsY~7?qKT>0!chBem$o2ny&$;4v7d#k)c(a0nHw04q-;e)_;J;Sz g|F|RUUlZ~{N|WWqo?`I7M2CfkN}3NU6|A2Af1M<@mH+?% literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nest/sdm/doc/nesthello.jpeg b/bundles/org.openhab.binding.nest/sdm/doc/nesthello.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c97180b63ad8f4eb9145ca1998800bf60a705403 GIT binary patch literal 3169 zcma)7c{r478-K@Il0j$2zC?sEV+$csoyZ=Jm@H$NI21)p*^{NLX;R9Xh8z*i$b_E+etrRdFc>Vra)7~5sF08lL=XxQmQauu7L^l&Kn|)M zJgf*qAP_=Q8af(qJq0)d2IAr2+0V-d;Ri$EQW7F?Z8!`LgTYk38#oKXk7&W*nuu@C z?~4`qj#*awn}b;o2SCJmJr!X6<$te?Uf7T_Z3m&pqJg+wR|NPuifDA)2SI zV#_J41Q5`-F8s&(mKP%M3-?EWm&JokfL#DE0Nxnxegs;6lk(cs+h;cMKDHeb3i}jU zFOi6KI-_SF8D$%I=R5-b4qPs|mR2yq1fq?a*H*+W<>O_ID!`_gYdx%?AmKAVO}i; zXZ3mZ8$WT@ZuFSU*3wqUpMTPUyIowTy!|KR>0mI?UQ;e3_$kMYU8+9;yX!=g1J;)& zm$?ZV@Fu}Tr9Xe{k{<{Yx~SmyaE;JmJ|%22KQoxKAqIUJasp0kahzr_5PP1fRLl^n z$KI@$tRv_g^A%n#RtZiz1hIf{lW?-Gw^>=^O`ZD2fH0da+_dM#qsoN|BlR0G&17?m zua@a+Sm;@TisES|ATerHls-_Zi^0DL^)?o;LG|@ZVcyeV#m?Eq(WA{8T|HZ+&3s>6;ao#~Qap^~vq9br%dABq(to5w`FtQnjF*kbU4rv1DV;ePaCh2L)*{6vGINSIW7I<n-F}@0HL;Z*M(7HfLqnZJAv4R0^gAl~syd zU7#BC7AJmHNqB!BQ#jHZB}#$xXB9MJO!a5fnLvO@7RD((l>P56E!^ZtkVB$Yfg+}6 zq*BBo6Jv{-Os`qD_tK`T0#&sAOJy6?$L@xw$>-HVn1Hr1R^`PmtzE!%w?up1qjT(# z-}IdlRJ0^!L;4scf9FtDId6LpO3;ZM$B{J%p`qT=tOyZ0S(jpNOs5Ts9s4bFJ4MG` zodHF~Z;e?t#;3ZT$OV_Ib9Vi8IX05i{W)>|@ecwKyS09TBhDTM9;{ljAxA9G1|EFh zs!rwCeyiHiW5B93lvVieSBV6eHWExBBWxYW!(DcyOdnTSOad(0|o zS|ySK_Y=B2gDh18lCg;d(<)=-iJlbS#ZheVB6}fPWNfgzs_3LPlBaJZ4P%E5XwyR$ zmSaD!1bYS^aGqPFB0q02qM*Ouli?)&P`~s))x$_WbsnhcS8Z}}_U8v9TFOY*(hOHS zwa3E9DQFMk49#fGU!|&w zaB=(c-9?JieV)N9XVUZAtgSSmL_$~Gpo^1bN_e|m&J=mXBGvWu-T%LZJd=)AL;V{I zEAbxpiLa#ag{z}e{KqyD8M#_lG;)O&k9)DRDM!n{SCxLRekqzwIbQzveVJ#+c$h#Y zu2qO~;>(Xg{g}x=?757wh?d;0YiVZ6B^6EE$x=Cs3Q>mcH8Z3v-sz^Tjua`do|h;! zrckFsxIwYXq;@Z;;l291vd8NZQhSCA@wIUQrf2J|sT1frsLNt{aKom5o;U4Ejbtny zMN_liT+Fh;Zbdm0c=3Z4Yw(>?72JzbgOP?YYN@pkY{5+6gE%xbX5qupF$;y~MdKrF6oQZe`W}Q?fpb&Lpf;CS33%Y&x2;};slNvqivpQ}#xr_$8?SWhgDq;+$ zS$lIfHmS$K2VEXr?$!FX)Lmy+WQYx9Es@<5 z0cjCMfjE|}Z??LB+4Ub2}$g4&@$PQx>XSF`N@hYR?OC+=R=< z)mkYSHH#T2*C4|m-72Z!S~_nGIp@;Z`o=<#Mbpo^y-I6x)3Cos$4yYx$WAj#saV4& zf9tgIgeG8Hu@1f7O@g zXcN5Js#1iba+{aWVhk2*V1@m|iU_s=1QT916~n4Cl|xCUz!gg`sw=-uwL z;(LxrzpRi+&5;4CFTNpq-QHgz*TpU+pSR`8UDF~>iJKeX4^zf-{V*;B-QV!EBKtId zyVK8}GK_|3n~JsJR?(NEy>k33&0Y+=?3=4DLicuR*~?9R#{W znsBiOQ%bAKsth4b?ky2jvwH_FRJCqt+2<+zSIEQa!D+{_1a;VlZHZR{IP=k1t4Lgv zEpc)*qn#A>%1W>?TZ&W_D25 zh~;?}0l1SCD4)8cMzh7=6HlvZ%OvVC+3G#C*-LT{d6bjZQV@zB%F!gZ;+;N}AA_;b zRyI2wS1xwDc)z_fmR9hduFtsRl@=WPG^p_C3;;a>C5gPiH}Z}`@eq@Q5T4q;6& zGKRm{rhw5E3-rLG&0OTk7eljKKEqk(&ep!ch3Vz)HIB5k2+&&M&U{IKhOf{$rcoxu z$D?jHN{jjABIucyG+TR|*tc}FAbm3wUAJN;JBXoB3_=tc%gbs7PpFiaRc0IPtw$pg z=k|%(jjP#Q-qy#XkS2$caVLG!C9YaQbeB@Yy(@ed)7PmbN*W%K#wT;HMwa>RXB!-qV0iFdUV%o2-Usz7K1Uqj2D^f{Qv*} literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nest/sdm/doc/nestthermostat.jpeg b/bundles/org.openhab.binding.nest/sdm/doc/nestthermostat.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a398312e514b92454e2bdc12314c6166262c470f GIT binary patch literal 3849 zcmZuy2T&8()(#+|tVj(dGzCI1A(0N!RHPWH^rf#N(yK;FD2kP;AOT@1B7_z?gc7QN zV5ky0(pgG?AdwdOk30XoS>L{M@674^Kb)!JiLMumv}+q{6OGkX;JYj2q^@D z_mZrVtfZm@908+aW@f&~auLYN3Y6v(;#2&aV2W_qsQ{CbQj$i%lwgvwr|Rj3pK_T~ zK?){=fSq!g)3}pn0Q)%rjt;^=Cjy{nr(2R02Bxz=ow{rQ zdOCXgvpZ$xS;uz? zEkJHDu-q+8Q*)<><)qWIcoARp zm%DY-{#ol6nSmL@!2l6m6yn6et=)FMK@uFtkNJ%VDvZ$eb!tw!cW}nJ!#)Fz=Z?U0 zV6H0gk^GMrx6y(X^&)i{q)>Zo*Jx zKGSted*u;JYeMI)-K$YosCC4k59;cEhuAP4dOW4XwX;!uvKh8?Q5i~YGcSK! zmq;*}#EuXejXuS1q^W<$dd2L!s?m6tm6P;OeY!gQPTJvS+ZY@jH}jNx`%= zY#q6_7WkM&AJweYuWXSh&ip7l?V#cS9%AJOmeqgq%S`@X0j(G|2jfiI8-!m3L?-r_ zzA2_{gfAXN)x5IyOU*Lm>on}A%>*dNU_5B z(fEld)w6;&B%^y{A<5psuWPK8NmZ1i7nb^RO^0wrCnvIJtB30@PO)>s3Zq57n2K7+ zXL3U>968#65w<+&-to$!_wK?1PYgBn4a_iqk>u}Dt5#YS8`d2ZEX?~9wljp2Sl@!m zMwgGw7ABoCT+IzJZk-jm=f-3BhVzR|rVDZ<^4-P}Kk5_hNfwaU>8L#s9qk{n*TTM) z{njw_ktwz$0zJ3u;Yu03g}J|@PKi#ka>{UAAN_*QpLR1^>WB|*KO0eD<2;@)J`bko z%`Zh1&sqd=>JPS>Z6$0PjlB4g8p`@?;;|?1Rm$r>4Bx+j1rkBlfMjO4hjRbo7q_si_YW)Zw|XYO02=(KoJ zV9yCaHz9ioQglV(FB-9~%JxWvY_z;VMSGjrMd297R%xO{lVZlsJk9pG#%z1^G-GS! z8%sVM_aP5YEt{xiR2>@azju5CN!VQKYeYcU5^bzX*dwd-yff4v;!12;-*@YwJN3|T4(W|Rvto^9a%CyOub~<1`H^) zz$5!8hJHu+id6PupbAf9eL?`N$&dEF&;WC|(7IId)EK&(TD_Rqz5|}DIxsr5XH~mf z_I__Tq!%0d>>wlPag)QG7$W$ogEK1gW$KX9U_NiJCl+VqG43t4ZCb5DN8bqc@I?Fo zTcuTG3*Uav!vFdN0J-}p1XL5G%Uqcyv5o(Th1=sy-Qae_#r3qRbW1WibIZkXgWB9K zHy@*ClF}(%xS}m+zI|_dhEh9S#w3>-BNI2ZuSK$D)oHl+re!8GB7b>ac^eGoLsktf zz%3TU2h+-pEvgw88x!T!S=pbqkh>Yd8uC-vAm2zmyH!+*l{ohN=gJB~NG7pMl0~om z+eq!&=7dH3qu?SHUjx7Sj3*przd6pV{g}tpdJ$gugqXls| zl#|3!vi!`r)YemnyQ&Mp`=oE@iJlu$?Zd7!3Bi)RxP|G zdXer>%RuX~uJGByWJSHLMrFNhCFom1rXJ_3T#c1p@Nv18qhNkP&BC5Cv3D-dI?1+?LJ zalEyGr=dEq3EBTL_G^vWd9tWUT@tp$8LeaeFjz1RISwxz8$32~a?H6P!->#0;hk$pM4U%I`J}lT!hQm{how0l zizck3p>LQBwvgVOyBTmZxDDz97`XJyEa`se49wG?y&{CZt!CA3E8HYXniIL>>Sh_m z+}J!qkau;)boMYs)teu5+1^Ke=*Goug@fLw;2tKHucZv{R?k@Oo9yQDqIH3Z2Q79u zrGtf;K<7K>%@#%|w%<`b4;t!N23}U$2JCpEg{&M>h?vGCIU2Xw5(>3=5KLQt3uNK< z2_Pio6!fQ(4X?HP{2Bd`;-0S~ce~^3{_1A%;UVSBZ`A`VS55$T+QWj2P5}K(x5>eV zqLy3HTDICn+aX}r!J&o{T;XJi(~Jhd+NCjWzO>(`^&akPRKq;;K(yEHO0N6nxK@ch z7D>Q3Ln{*fQvX^Ir`4*C37wndVj7H~fvg;2`xct6`-`Xx#a2{DxMbD&xArt)@XyRL zA185_q>mO(P93GCWjEXsfU8s^7O0^MlXZ6?W-tK-zFyy)#`-K_9{05HR8Ov?3aUj>;%f41WpVK$w;N)#r(>wA6xZkQdx4XTTOT9=n?Dvyv?{UL{bC zB}nH71r(d}FqP>wId5Lt)GQQ4-{0|CSn8>YyTK!!lmVNqdtd-v^Hj=5p+^`f7fh{G zhu76Z#y}A9$*B!#WyPBW##e1$V}eh=ik@oFqwRZHj1aCBL*)35S7-0A+wL_Y>IvCi zIfm(Fmc1ES&8xteF#HV%bJy`&e0ZF$+CWW5G`I`)7Vgy?kW!Qa9ATy{IFsl2%YO#8 zKxO>V-e5?m(<2tS9sUn!-@XzVuX&UEw#G;>u3~!W>f8?nt!rxLQ?#oY>3$L6CT?Be zj#vDFQ?$f{OY!B$4=`11NGrZdks!pYWq<2jZ2Zd2h1~*Ay)w^`iIxV4ww(8e4|q&2 zqOulQnB-R-(j}`L-v^KC>1NTE_~pX?t$f-3a9cW_J_)F~Su>&X96vvW25m)lTYzS* zA6dkiSlkb}o@Mqf)qKDXMWn!1XBP~u`$w%^j>J^OJNq-kqx?c^4K|Q%)5`Ay0|AKoXxUmXyhS$1K0d3{&(e*k*hzmODmS$}I1* z7J{EgV7KeNWP}R<`fnrv0IAg5_`G-ePHb1CdEFCeX+i__GHOn`V%9Z2C5o@wJ1_Q$ zJ{EZat=*5Gt5P5QeWW=T&j!f?ZQIp)iGw!xO|!}pQJ2G3FKvEDAM`Jk336u~
m zhmsy_Q*@f5p&kCu)RB-?)1z5q9F5yrz}?h+b(H_zI@-RM#7Fy?s8!kB{JOMhyxW@p|+Q6H&kN#lxn(bj(naN~br zaB8E=I)9^^?xR_Z*Hg*8KQ4zxzgosURcL9}iskcB)Q(j6!1S4O`CqwVCu9EyYIOn2 literal 0 HcmV?d00001 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..0ff272fb46ab6 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/SDMThingHandlerFactory.java @@ -0,0 +1,77 @@ +/** + * 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.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; + + @Activate + public SDMThingHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference OAuthFactory oAuthFactory) { + this.httpClientFactory = httpClientFactory; + this.oAuthFactory = oAuthFactory; + } + + @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); + } else if (thingTypeUID.equals(THING_TYPE_DISPLAY)) { + return new SDMCameraHandler(thing); + } else if (thingTypeUID.equals(THING_TYPE_DOORBELL)) { + return new SDMCameraHandler(thing); + } else if (thingTypeUID.equals(THING_TYPE_THERMOSTAT)) { + return new SDMThermostatHandler(thing); + } + + 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..835710551b6bf --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/dto/SDMResourceName.java @@ -0,0 +1,105 @@ +/** + * 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; + } + SDMResourceName other = (SDMResourceName) obj; + if (!name.equals(other.name)) { + return false; + } + return true; + } + + @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..75d2a88f4bf11 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMBaseHandler.java @@ -0,0 +1,316 @@ +/** + * 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.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; + + public SDMBaseHandler(Thing thing) { + super(thing); + } + + @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..8d91e853895f0 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMCameraHandler.java @@ -0,0 +1,199 @@ +/** + * 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.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) { + super(thing); + } + + 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; + updateState(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, new StringType(results.streamToken)); + updateState(CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP, new DateTimeType(results.expiresAt)); + 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)); + + 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..5ad11f71ed053 --- /dev/null +++ b/bundles/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/sdm/handler/SDMThermostatHandler.java @@ -0,0 +1,356 @@ +/** + * 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.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) { + super(thing); + } + + @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)); + } + + 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 95% 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..b53a844b490da 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; } 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 94% 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..72fc8467380f7 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; } 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..634f7001fbe60 --- /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 OAuth 2.0 client used for accessing the Pub/Sub project + + + password + + The OAuth 2.0 client secret used for accessing the Pub/Sub project + + + + 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 92% 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..3cabe3b720bc6 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,7 +4,7 @@ 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 @@ -39,13 +39,13 @@ - + - + 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..e8673ef537070 --- /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 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