]> git.basschouten.com Git - openhab-addons.git/blob
e472ce968065c56cf0b0740b067d9d216b2cc21c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.evohome.internal.handler;
14
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Set;
20 import java.util.concurrent.CopyOnWriteArrayList;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.evohome.internal.RunnableWithTimeout;
29 import org.openhab.binding.evohome.internal.api.EvohomeApiClient;
30 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Gateway;
31 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.GatewayStatus;
32 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Location;
33 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationStatus;
34 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
35 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationsStatus;
36 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystem;
37 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystemStatus;
38 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Zone;
39 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.ZoneStatus;
40 import org.openhab.binding.evohome.internal.configuration.EvohomeAccountConfiguration;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseBridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandler;
48 import org.openhab.core.types.Command;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * Provides the bridge for this binding. Controls the authentication sequence.
54  * Manages the scheduler for getting updates from the API and updates the Things it contains.
55  *
56  * @author Jasper van Zuijlen - Initial contribution
57  *
58  */
59 @NonNullByDefault
60 public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
61
62     private final Logger logger = LoggerFactory.getLogger(EvohomeAccountBridgeHandler.class);
63     private final HttpClient httpClient;
64     private EvohomeAccountConfiguration configuration = new EvohomeAccountConfiguration();
65     private @Nullable EvohomeApiClient apiClient;
66     private List<AccountStatusListener> listeners = new CopyOnWriteArrayList<>();
67
68     protected @Nullable ScheduledFuture<?> refreshTask;
69
70     public EvohomeAccountBridgeHandler(Bridge thing, HttpClient httpClient) {
71         super(thing);
72         this.httpClient = httpClient;
73     }
74
75     @Override
76     public void initialize() {
77         configuration = getConfigAs(EvohomeAccountConfiguration.class);
78
79         if (checkConfig(configuration)) {
80             apiClient = new EvohomeApiClient(configuration, this.httpClient);
81
82             // Initialization can take a while, so kick it off on a separate thread
83             scheduler.schedule(() -> {
84                 EvohomeApiClient localApiCLient = apiClient;
85                 if (localApiCLient != null && localApiCLient.login()) {
86                     if (checkInstallationInfoHasDuplicateIds(localApiCLient.getInstallationInfo())) {
87                         startRefreshTask();
88                     } else {
89                         updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
90                                 "System Information Sanity Check failed");
91                     }
92                 } else {
93                     updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
94                             "Authentication failed");
95                 }
96             }, 0, TimeUnit.SECONDS);
97         }
98     }
99
100     @Override
101     public void dispose() {
102         disposeRefreshTask();
103         disposeApiClient();
104         listeners.clear();
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109     }
110
111     public @Nullable Locations getEvohomeConfig() {
112         EvohomeApiClient localApiCLient = apiClient;
113         if (localApiCLient != null) {
114             return localApiCLient.getInstallationInfo();
115         }
116         return null;
117     }
118
119     public @Nullable LocationsStatus getEvohomeStatus() {
120         EvohomeApiClient localApiCLient = apiClient;
121         if (localApiCLient != null) {
122             return localApiCLient.getInstallationStatus();
123         }
124         return null;
125     }
126
127     public void setTcsMode(String tcsId, String mode) {
128         EvohomeApiClient localApiCLient = apiClient;
129         if (localApiCLient != null) {
130             tryToCall(() -> localApiCLient.setTcsMode(tcsId, mode));
131         }
132     }
133
134     public void setPermanentSetPoint(String zoneId, double doubleValue) {
135         EvohomeApiClient localApiCLient = apiClient;
136         if (localApiCLient != null) {
137             tryToCall(() -> localApiCLient.setHeatingZoneOverride(zoneId, doubleValue));
138         }
139     }
140
141     public void cancelSetPointOverride(String zoneId) {
142         EvohomeApiClient localApiCLient = apiClient;
143         if (localApiCLient != null) {
144             tryToCall(() -> localApiCLient.cancelHeatingZoneOverride(zoneId));
145         }
146     }
147
148     public void addAccountStatusListener(AccountStatusListener listener) {
149         listeners.add(listener);
150         listener.accountStatusChanged(getThing().getStatus());
151     }
152
153     public void removeAccountStatusListener(AccountStatusListener listener) {
154         listeners.remove(listener);
155     }
156
157     private void tryToCall(RunnableWithTimeout action) {
158         try {
159             action.run();
160         } catch (TimeoutException e) {
161             updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162                     "Timeout on executing request");
163         }
164     }
165
166     private boolean checkInstallationInfoHasDuplicateIds(Locations locations) {
167         boolean result = true;
168
169         // Make sure that there are no duplicate IDs
170         Set<String> ids = new HashSet<>();
171
172         for (Location location : locations) {
173             result &= ids.add(location.getLocationInfo().getLocationId());
174             for (Gateway gateway : location.getGateways()) {
175                 result &= ids.add(gateway.getGatewayInfo().getGatewayId());
176                 for (TemperatureControlSystem tcs : gateway.getTemperatureControlSystems()) {
177                     result &= ids.add(tcs.getSystemId());
178                     for (Zone zone : tcs.getZones()) {
179                         result &= ids.add(zone.getZoneId());
180                     }
181                 }
182             }
183         }
184         return result;
185     }
186
187     private void disposeApiClient() {
188         EvohomeApiClient localApiClient = apiClient;
189         if (localApiClient != null) {
190             localApiClient.logout();
191             this.apiClient = null;
192         }
193     }
194
195     private void disposeRefreshTask() {
196         ScheduledFuture<?> localRefreshTask = refreshTask;
197         if (localRefreshTask != null) {
198             localRefreshTask.cancel(true);
199             this.refreshTask = null;
200         }
201     }
202
203     private boolean checkConfig(EvohomeAccountConfiguration configuration) {
204         String errorMessage = "";
205
206         if (configuration.username.isBlank()) {
207             errorMessage = "Username not configured";
208         } else if (configuration.password.isBlank()) {
209             errorMessage = "Password not configured";
210         } else {
211             return true;
212         }
213
214         updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
215         return false;
216     }
217
218     private void startRefreshTask() {
219         disposeRefreshTask();
220
221         refreshTask = scheduler.scheduleWithFixedDelay(this::update, 0, configuration.refreshInterval,
222                 TimeUnit.SECONDS);
223     }
224
225     private void update() {
226         try {
227             EvohomeApiClient localApiCLient = apiClient;
228             if (localApiCLient != null) {
229                 localApiCLient.update();
230             }
231             updateAccountStatus(ThingStatus.ONLINE);
232             updateThings();
233         } catch (Exception e) {
234             updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
235             logger.debug("Failed to update installation status", e);
236         }
237     }
238
239     private void updateAccountStatus(ThingStatus newStatus) {
240         updateAccountStatus(newStatus, ThingStatusDetail.NONE, null);
241     }
242
243     private void updateAccountStatus(ThingStatus newStatus, ThingStatusDetail detail, @Nullable String message) {
244         // Prevent spamming the log file
245         if (!newStatus.equals(getThing().getStatus())) {
246             updateStatus(newStatus, detail, message);
247             updateListeners(newStatus);
248         }
249     }
250
251     private void updateListeners(ThingStatus status) {
252         for (AccountStatusListener listener : listeners) {
253             listener.accountStatusChanged(status);
254         }
255     }
256
257     private void updateThings() {
258         Map<String, TemperatureControlSystemStatus> idToTcsMap = new HashMap<>();
259         Map<String, ZoneStatus> idToZoneMap = new HashMap<>();
260         Map<String, GatewayStatus> tcsIdToGatewayMap = new HashMap<>();
261         Map<String, String> zoneIdToTcsIdMap = new HashMap<>();
262         Map<String, ThingStatus> idToTcsThingsStatusMap = new HashMap<>();
263
264         EvohomeApiClient localApiClient = apiClient;
265         if (localApiClient != null) {
266             // First, create a lookup table
267             LocationsStatus localLocationsStatus = localApiClient.getInstallationStatus();
268             if (localLocationsStatus != null) {
269                 for (LocationStatus location : localLocationsStatus) {
270                     for (GatewayStatus gateway : location.getGateways()) {
271                         if (gateway == null) {
272                             continue;
273                         }
274                         for (TemperatureControlSystemStatus tcs : gateway.getTemperatureControlSystems()) {
275                             String systemId = tcs.getSystemId();
276                             if (systemId != null) {
277                                 idToTcsMap.put(systemId, tcs);
278                                 tcsIdToGatewayMap.put(systemId, gateway);
279                             }
280                             for (ZoneStatus zone : tcs.getZones()) {
281                                 String zoneId = zone.getZoneId();
282                                 if (zoneId != null) {
283                                     idToZoneMap.put(zoneId, zone);
284                                     if (systemId != null) {
285                                         zoneIdToTcsIdMap.put(zoneId, systemId);
286                                     }
287                                 }
288                             }
289                         }
290                     }
291                 }
292             }
293         }
294
295         // Then update the things by type, with pre-filtered info
296         for (Thing handler : getThing().getThings()) {
297             ThingHandler thingHandler = handler.getHandler();
298
299             if (thingHandler instanceof EvohomeTemperatureControlSystemHandler) {
300                 EvohomeTemperatureControlSystemHandler tcsHandler = (EvohomeTemperatureControlSystemHandler) thingHandler;
301                 tcsHandler.update(tcsIdToGatewayMap.get(tcsHandler.getId()), idToTcsMap.get(tcsHandler.getId()));
302                 idToTcsThingsStatusMap.put(tcsHandler.getId(), tcsHandler.getThing().getStatus());
303             }
304             if (thingHandler instanceof EvohomeHeatingZoneHandler) {
305                 EvohomeHeatingZoneHandler zoneHandler = (EvohomeHeatingZoneHandler) thingHandler;
306                 zoneHandler.update(idToTcsThingsStatusMap.get(zoneIdToTcsIdMap.get(zoneHandler.getId())),
307                         idToZoneMap.get(zoneHandler.getId()));
308             }
309         }
310     }
311 }