2 * Copyright (c) 2010-2023 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.evohome.internal.handler;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.List;
20 import java.util.concurrent.CopyOnWriteArrayList;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
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;
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.
56 * @author Jasper van Zuijlen - Initial contribution
60 public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
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<>();
68 protected @Nullable ScheduledFuture<?> refreshTask;
70 public EvohomeAccountBridgeHandler(Bridge thing, HttpClient httpClient) {
72 this.httpClient = httpClient;
76 public void initialize() {
77 configuration = getConfigAs(EvohomeAccountConfiguration.class);
79 if (checkConfig(configuration)) {
80 apiClient = new EvohomeApiClient(configuration, this.httpClient);
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())) {
89 updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
90 "System Information Sanity Check failed");
93 updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
94 "Authentication failed");
96 }, 0, TimeUnit.SECONDS);
101 public void dispose() {
102 disposeRefreshTask();
108 public void handleCommand(ChannelUID channelUID, Command command) {
111 public @Nullable Locations getEvohomeConfig() {
112 EvohomeApiClient localApiCLient = apiClient;
113 if (localApiCLient != null) {
114 return localApiCLient.getInstallationInfo();
119 public @Nullable LocationsStatus getEvohomeStatus() {
120 EvohomeApiClient localApiCLient = apiClient;
121 if (localApiCLient != null) {
122 return localApiCLient.getInstallationStatus();
127 public void setTcsMode(String tcsId, String mode) {
128 EvohomeApiClient localApiCLient = apiClient;
129 if (localApiCLient != null) {
130 tryToCall(() -> localApiCLient.setTcsMode(tcsId, mode));
134 public void setPermanentSetPoint(String zoneId, double doubleValue) {
135 EvohomeApiClient localApiCLient = apiClient;
136 if (localApiCLient != null) {
137 tryToCall(() -> localApiCLient.setHeatingZoneOverride(zoneId, doubleValue));
141 public void cancelSetPointOverride(String zoneId) {
142 EvohomeApiClient localApiCLient = apiClient;
143 if (localApiCLient != null) {
144 tryToCall(() -> localApiCLient.cancelHeatingZoneOverride(zoneId));
148 public void addAccountStatusListener(AccountStatusListener listener) {
149 listeners.add(listener);
150 listener.accountStatusChanged(getThing().getStatus());
153 public void removeAccountStatusListener(AccountStatusListener listener) {
154 listeners.remove(listener);
157 private void tryToCall(RunnableWithTimeout action) {
160 } catch (TimeoutException e) {
161 updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "Timeout on executing request");
166 private boolean checkInstallationInfoHasDuplicateIds(Locations locations) {
167 boolean result = true;
169 // Make sure that there are no duplicate IDs
170 Set<String> ids = new HashSet<>();
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());
187 private void disposeApiClient() {
188 EvohomeApiClient localApiClient = apiClient;
189 if (localApiClient != null) {
190 localApiClient.logout();
191 this.apiClient = null;
195 private void disposeRefreshTask() {
196 ScheduledFuture<?> localRefreshTask = refreshTask;
197 if (localRefreshTask != null) {
198 localRefreshTask.cancel(true);
199 this.refreshTask = null;
203 private boolean checkConfig(EvohomeAccountConfiguration configuration) {
204 String errorMessage = "";
206 if (configuration.username.isBlank()) {
207 errorMessage = "Username not configured";
208 } else if (configuration.password.isBlank()) {
209 errorMessage = "Password not configured";
214 updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
218 private void startRefreshTask() {
219 disposeRefreshTask();
221 refreshTask = scheduler.scheduleWithFixedDelay(this::update, 0, configuration.refreshInterval,
225 private void update() {
227 EvohomeApiClient localApiCLient = apiClient;
228 if (localApiCLient != null) {
229 localApiCLient.update();
231 updateAccountStatus(ThingStatus.ONLINE);
233 } catch (Exception e) {
234 updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
235 logger.debug("Failed to update installation status", e);
239 private void updateAccountStatus(ThingStatus newStatus) {
240 updateAccountStatus(newStatus, ThingStatusDetail.NONE, null);
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);
251 private void updateListeners(ThingStatus status) {
252 for (AccountStatusListener listener : listeners) {
253 listener.accountStatusChanged(status);
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<>();
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) {
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);
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);
295 // Then update the things by type, with pre-filtered info
296 for (Thing handler : getThing().getThings()) {
297 ThingHandler thingHandler = handler.getHandler();
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());
304 if (thingHandler instanceof EvohomeHeatingZoneHandler) {
305 EvohomeHeatingZoneHandler zoneHandler = (EvohomeHeatingZoneHandler) thingHandler;
306 zoneHandler.update(idToTcsThingsStatusMap.get(zoneIdToTcsIdMap.get(zoneHandler.getId())),
307 idToZoneMap.get(zoneHandler.getId()));