2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.salus.internal.aws.http;
15 import static java.nio.charset.StandardCharsets.UTF_8;
16 import static java.time.ZoneOffset.UTC;
17 import static java.util.Objects.requireNonNull;
19 import java.time.Clock;
20 import java.time.LocalDateTime;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.util.List;
24 import java.util.SortedSet;
25 import java.util.TreeSet;
26 import java.util.concurrent.ExecutionException;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.salus.internal.rest.AbstractSalusApi;
31 import org.openhab.binding.salus.internal.rest.Device;
32 import org.openhab.binding.salus.internal.rest.DeviceProperty;
33 import org.openhab.binding.salus.internal.rest.GsonMapper;
34 import org.openhab.binding.salus.internal.rest.RestClient;
35 import org.openhab.binding.salus.internal.rest.exceptions.AuthSalusApiException;
36 import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
37 import org.openhab.binding.salus.internal.rest.exceptions.UnsuportedSalusApiException;
38 import org.openhab.core.io.net.http.HttpClientFactory;
40 import software.amazon.awssdk.crt.auth.credentials.StaticCredentialsProvider;
41 import software.amazon.awssdk.crt.auth.signing.AwsSigner;
42 import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
43 import software.amazon.awssdk.crt.auth.signing.AwsSigningResult;
44 import software.amazon.awssdk.crt.http.HttpHeader;
45 import software.amazon.awssdk.crt.http.HttpRequest;
48 * The SalusApi class is responsible for interacting with a REST API to perform various operations related to the Salus
49 * system. It handles authentication, token management, and provides methods to retrieve and manipulate device
50 * information and properties.
52 * @author Martin GrzeĊlowski - Initial contribution
55 public class AwsSalusApi extends AbstractSalusApi<Authentication> {
56 private final AuthenticationHelper authenticationHelper;
57 private final String companyCode;
58 private final String awsService;
59 private final String region;
61 CogitoCredentials cogitoCredentials;
63 private AwsSalusApi(String username, byte[] password, String baseUrl, RestClient restClient, GsonMapper mapper,
64 Clock clock, AuthenticationHelper authenticationHelper, String companyCode, String awsService,
66 super(username, password, baseUrl, restClient, mapper, clock);
67 this.authenticationHelper = authenticationHelper;
68 this.companyCode = companyCode;
69 this.awsService = awsService;
73 public AwsSalusApi(HttpClientFactory httpClientFactory, String username, byte[] password, String baseUrl,
74 RestClient restClient, GsonMapper gsonMapper, String userPoolId, String identityPoolId, String clientId,
75 String region, String companyCode, String awsService) {
76 this(username, password, baseUrl, restClient, gsonMapper, Clock.systemDefaultZone(),
77 new AuthenticationHelper(httpClientFactory, userPoolId, clientId, region, identityPoolId), companyCode,
82 protected void login() throws AuthSalusApiException {
83 logger.debug("Login with username '{}'", username);
84 var result = authenticationHelper.performSrpAuthentication(username, new String(password, UTF_8));
85 var localAuth = authentication = new Authentication(result.getAccessToken(), result.getExpiresIn(),
86 result.getTokenType(), result.getRefreshToken(), result.getIdToken());
87 var local = LocalDateTime.now(clock).plusSeconds(localAuth.expiresIn())
88 // this is to account that there is a delay between server setting `expires_in`
89 // and client (OpenHAB) receiving it
90 .minusSeconds(TOKEN_EXPIRE_TIME_ADJUSTMENT_SECONDS);
91 var localExpireTime = authTokenExpireTime = ZonedDateTime.of(local, UTC);
93 var id = authenticationHelper.getId(result);
95 var cogito = authenticationHelper.getCredentialsForIdentity(result, id.getIdentityId());
96 cogitoCredentials = new CogitoCredentials(//
97 cogito.getCredentials().getAccessKeyId(), //
98 cogito.getCredentials().getSecretKey(), //
99 cogito.getCredentials().getSessionToken());
101 var cogitoExpirationTime = cogito.getCredentials().getExpiration();
102 if (cogitoExpirationTime.isBefore(localExpireTime.toInstant())) {
103 authTokenExpireTime = ZonedDateTime.ofInstant(cogitoExpirationTime, UTC);
108 protected void cleanAuth() {
110 cogitoCredentials = null;
114 public SortedSet<Device> findDevices() throws AuthSalusApiException, SalusApiException {
115 var result = new TreeSet<Device>();
116 var gateways = findGateways();
117 for (var gatewayId : gateways) {
118 var response = get(url("/api/v1/occupants/slider_details?id=%s&type=gateway".formatted(gatewayId)),
120 if (response == null) {
123 result.addAll(mapper.parseAwsDevices(response));
128 private List<String> findGateways() throws SalusApiException, AuthSalusApiException {
129 var response = get(url("/api/v1/occupants/slider_list"), authHeaders());
130 if (response == null) {
133 return mapper.parseAwsGatewayIds(response);
136 private RestClient.Header[] authHeaders() throws AuthSalusApiException {
137 refreshAccessToken();
138 return new RestClient.Header[] {
139 new RestClient.Header("x-access-token", requireNonNull(authentication).accessToken()),
140 new RestClient.Header("x-auth-token", requireNonNull(authentication).idToken()),
141 new RestClient.Header("x-company-code", companyCode) };
145 public SortedSet<DeviceProperty<?>> findDeviceProperties(String dsn)
146 throws SalusApiException, AuthSalusApiException {
147 var path = "https://%s.iot.%s.amazonaws.com/things/%s/shadow".formatted(awsService, region, dsn);
148 var time = ZonedDateTime.now(clock).withZoneSameInstant(ZoneId.of("UTC"));
149 var signingResult = buildSigningResult(dsn, time);
150 var headers = signingResult.getSignedRequest()//
153 .map(header -> new RestClient.Header(header.getName(), header.getValue()))//
155 .toArray(new RestClient.Header[0]);
156 var response = get(path, headers);
157 if (response == null) {
158 return new TreeSet<>();
161 return new TreeSet<>(mapper.parseAwsDeviceProperties(response));
164 private AwsSigningResult buildSigningResult(String dsn, ZonedDateTime time)
165 throws SalusApiException, AuthSalusApiException {
166 refreshAccessToken();
167 HttpRequest httpRequest = new HttpRequest("GET", "/things/%s/shadow".formatted(dsn),
168 new HttpHeader[] { new HttpHeader("host", "") }, null);
169 var localCredentials = requireNonNull(cogitoCredentials);
170 try (var config = new AwsSigningConfig()) {
171 config.setRegion(region);
172 config.setService("iotdevicegateway");
173 config.setCredentialsProvider(new StaticCredentialsProvider.StaticCredentialsProviderBuilder()
174 .withAccessKeyId(localCredentials.accessKeyId().getBytes(UTF_8))
175 .withSecretAccessKey(localCredentials.secretKey().getBytes(UTF_8))
176 .withSessionToken(localCredentials.sessionToken().getBytes(UTF_8)).build());
177 config.setTime(time.toInstant().toEpochMilli());
178 return AwsSigner.sign(httpRequest, config).get();
179 } catch (ExecutionException | InterruptedException e) {
180 throw new SalusApiException("Cannot build AWS signature!", e);
185 public Object setValueForProperty(String dsn, String propertyName, Object value) throws SalusApiException {
186 throw new UnsuportedSalusApiException("Setting value is not supported for AWS bridge");