]> git.basschouten.com Git - openhab-addons.git/blob
9b35e8521017de44031a0ab99602c97cb3c39701
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.ecobee.internal.handler;
14
15 import static org.openhab.binding.ecobee.internal.EcobeeBindingConstants.CONFIG_THERMOSTAT_ID;
16
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.CopyOnWriteArraySet;
25 import java.util.concurrent.Future;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.atomic.AtomicInteger;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.ecobee.internal.api.EcobeeApi;
33 import org.openhab.binding.ecobee.internal.config.EcobeeAccountConfiguration;
34 import org.openhab.binding.ecobee.internal.discovery.ThermostatDiscoveryService;
35 import org.openhab.binding.ecobee.internal.dto.SelectionDTO;
36 import org.openhab.binding.ecobee.internal.dto.thermostat.ThermostatDTO;
37 import org.openhab.binding.ecobee.internal.dto.thermostat.ThermostatUpdateRequestDTO;
38 import org.openhab.binding.ecobee.internal.dto.thermostat.summary.SummaryResponseDTO;
39 import org.openhab.binding.ecobee.internal.function.FunctionRequest;
40 import org.openhab.core.auth.client.oauth2.OAuthFactory;
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.thing.binding.ThingHandlerService;
49 import org.openhab.core.types.Command;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link EcobeeAccountBridgeHandler} is responsible for managing
55  * communication with the Ecobee API.
56  *
57  * @author Mark Hilbush - Initial contribution
58  */
59 @NonNullByDefault
60 public class EcobeeAccountBridgeHandler extends BaseBridgeHandler {
61
62     private static final int REFRESH_STARTUP_DELAY_SECONDS = 3;
63     private static final int REFRESH_INTERVAL_SECONDS = 1;
64     private static final int DISCOVERY_INTERVAL_SECONDS = 300;
65     private static final int DISCOVERY_INITIAL_DELAY_SECONDS = 10;
66     private static final int DEFAULT_REFRESH_INTERVAL_NORMAL_SECONDS = 20;
67     private static final int DEFAULT_REFRESH_INTERVAL_QUICK_SECONDS = 5;
68     private static final int DEFAULT_API_TIMEOUT_SECONDS = 20;
69
70     private final Logger logger = LoggerFactory.getLogger(EcobeeAccountBridgeHandler.class);
71
72     private final OAuthFactory oAuthFactory;
73     private final HttpClient httpClient;
74
75     private @NonNullByDefault({}) EcobeeApi api;
76     private @NonNullByDefault({}) String apiKey;
77     private int refreshIntervalNormal;
78     private int refreshIntervalQuick;
79     private int apiTimeout;
80     private boolean discoveryEnabled;
81     private int discoveryInterval;
82
83     private final Map<String, EcobeeThermostatBridgeHandler> thermostatHandlers = new ConcurrentHashMap<>();
84     private final Set<String> thermostatIds = new CopyOnWriteArraySet<>();
85
86     private @Nullable Future<?> refreshThermostatsJob;
87     private final AtomicInteger refreshThermostatsCounter = new AtomicInteger(REFRESH_STARTUP_DELAY_SECONDS);
88     private final AtomicInteger discoveryCounter = new AtomicInteger(DISCOVERY_INITIAL_DELAY_SECONDS);
89     private @Nullable ThermostatDiscoveryService discoveryService;
90
91     private @Nullable SummaryResponseDTO previousSummary;
92
93     public EcobeeAccountBridgeHandler(final Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
94         super(bridge);
95         this.oAuthFactory = oAuthFactory;
96         this.httpClient = httpClient;
97     }
98
99     @Override
100     public void initialize() {
101         logger.debug("AccountBridge: Initializing");
102
103         EcobeeAccountConfiguration config = getConfigAs(EcobeeAccountConfiguration.class);
104         apiKey = config.apiKey;
105
106         Integer value;
107         value = config.refreshIntervalNormal;
108         refreshIntervalNormal = value == null ? DEFAULT_REFRESH_INTERVAL_NORMAL_SECONDS : value;
109
110         value = config.refreshIntervalQuick;
111         refreshIntervalQuick = value == null ? DEFAULT_REFRESH_INTERVAL_QUICK_SECONDS : value;
112
113         value = config.apiTimeout;
114         apiTimeout = (value == null ? DEFAULT_API_TIMEOUT_SECONDS : value) * 1000;
115
116         Boolean booleanValue = config.discoveryEnabled;
117         discoveryEnabled = booleanValue == null ? false : booleanValue.booleanValue();
118         logger.debug("AccountBridge: Thermostat and sensor discovery is {}", discoveryEnabled ? "enabled" : "disabled");
119
120         value = config.discoveryInterval;
121         discoveryInterval = value == null ? DISCOVERY_INTERVAL_SECONDS : value;
122
123         api = new EcobeeApi(this, apiKey, apiTimeout, oAuthFactory, httpClient);
124
125         scheduleRefreshJob();
126         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Checking authorization");
127     }
128
129     @Override
130     public void dispose() {
131         cancelRefreshJob();
132         api.closeOAuthClientService();
133         logger.debug("AccountBridge: Disposing");
134     }
135
136     @Override
137     public void handleCommand(ChannelUID channelUID, Command command) {
138     }
139
140     @Override
141     public Collection<Class<? extends ThingHandlerService>> getServices() {
142         return Collections.singleton(ThermostatDiscoveryService.class);
143     }
144
145     @Override
146     public void childHandlerInitialized(ThingHandler thermostatHandler, Thing thermostatThing) {
147         String thermostatId = (String) thermostatThing.getConfiguration().get(CONFIG_THERMOSTAT_ID);
148         thermostatHandlers.put(thermostatId, (EcobeeThermostatBridgeHandler) thermostatHandler);
149         thermostatIds.add(thermostatId);
150         scheduleQuickPoll();
151         logger.debug("AccountBridge: Adding thermostat handler for {} with id {}", thermostatThing.getUID(),
152                 thermostatId);
153     }
154
155     @Override
156     public void childHandlerDisposed(ThingHandler thermostatHandler, Thing thermostatThing) {
157         String thermostatId = (String) thermostatThing.getConfiguration().get(CONFIG_THERMOSTAT_ID);
158         thermostatHandlers.remove(thermostatId);
159         thermostatIds.remove(thermostatId);
160         logger.debug("AccountBridge: Removing thermostat handler for {} with id {}", thermostatThing.getUID(),
161                 thermostatId);
162     }
163
164     public void setDiscoveryService(ThermostatDiscoveryService discoveryService) {
165         this.discoveryService = discoveryService;
166     }
167
168     public boolean isDiscoveryEnabled() {
169         return discoveryEnabled;
170     }
171
172     public void updateBridgeStatus(ThingStatus status) {
173         updateStatus(status);
174     }
175
176     public void updateBridgeStatus(ThingStatus status, ThingStatusDetail statusDetail, String statusMessage) {
177         updateStatus(status, statusDetail, statusMessage);
178     }
179
180     public boolean performThermostatFunction(FunctionRequest request) {
181         boolean success = api.performThermostatFunction(request);
182         if (success) {
183             scheduleQuickPoll();
184         }
185         return success;
186     }
187
188     public boolean performThermostatUpdate(ThermostatUpdateRequestDTO request) {
189         boolean success = api.performThermostatUpdate(request);
190         if (success) {
191             scheduleQuickPoll();
192         }
193         return success;
194     }
195
196     public SelectionDTO getSelection() {
197         SelectionDTO mergedSelection = new SelectionDTO();
198         for (EcobeeThermostatBridgeHandler handler : new ArrayList<EcobeeThermostatBridgeHandler>(
199                 thermostatHandlers.values())) {
200             SelectionDTO selection = handler.getSelection();
201             logger.trace("AccountBridge: Thermostat {} selection: {}", handler.getThing().getUID(), selection);
202             mergedSelection.mergeSelection(selection);
203         }
204         return mergedSelection;
205     }
206
207     public void markOnline() {
208         updateStatus(ThingStatus.ONLINE);
209     }
210
211     public List<ThermostatDTO> getRegisteredThermostats() {
212         return api.queryRegisteredThermostats();
213     }
214
215     /*
216      * The refresh job
217      * - updates the thermostat channels on the refresh interval set in the thermostat thing config, and
218      * - runs the thermostat discovery on the refresh interval set in the thing config
219      *
220      * The thermostat update process involves first running a thermostat summary transaction to
221      * determine if any thermostat data has changed since the last summary. If any change is detected,
222      * a full query of the thermostats is performed.
223      */
224     private void refresh() {
225         refreshThermostats();
226         discoverThermostats();
227     }
228
229     @SuppressWarnings("null")
230     private void refreshThermostats() {
231         if (refreshThermostatsCounter.getAndDecrement() == 0) {
232             refreshThermostatsCounter.set(refreshIntervalNormal);
233             SummaryResponseDTO summary = api.performThermostatSummaryQuery();
234             if (summary != null && summary.hasChanged(previousSummary) && !thermostatIds.isEmpty()) {
235                 for (ThermostatDTO thermostat : api.performThermostatQuery(thermostatIds)) {
236                     EcobeeThermostatBridgeHandler handler = thermostatHandlers.get(thermostat.identifier);
237                     if (handler != null) {
238                         handler.updateChannels(thermostat);
239                     }
240                 }
241             }
242             previousSummary = summary;
243         }
244     }
245
246     private void discoverThermostats() {
247         if (isDiscoveryEnabled()) {
248             if (discoveryCounter.getAndDecrement() == 0) {
249                 discoveryCounter.set(discoveryInterval);
250                 ThermostatDiscoveryService localDiscoveryService = discoveryService;
251                 if (localDiscoveryService != null) {
252                     logger.debug("AccountBridge: Running thermostat discovery");
253                     localDiscoveryService.startBackgroundDiscovery();
254                 }
255             }
256         }
257     }
258
259     private void scheduleQuickPoll() {
260         if (refreshThermostatsCounter.get() > refreshIntervalQuick) {
261             logger.debug("AccountBridge: Scheduling quick poll");
262             refreshThermostatsCounter.set(refreshIntervalQuick);
263             forceFullNextPoll();
264         }
265     }
266
267     private void scheduleRefreshJob() {
268         logger.debug("AccountBridge: Scheduling thermostat refresh job");
269         cancelRefreshJob();
270         refreshThermostatsCounter.set(0);
271         refreshThermostatsJob = scheduler.scheduleWithFixedDelay(this::refresh, REFRESH_STARTUP_DELAY_SECONDS,
272                 REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
273     }
274
275     private void cancelRefreshJob() {
276         Future<?> localRefreshThermostatsJob = refreshThermostatsJob;
277         if (localRefreshThermostatsJob != null) {
278             forceFullNextPoll();
279             localRefreshThermostatsJob.cancel(true);
280             logger.debug("AccountBridge: Canceling thermostat refresh job");
281         }
282     }
283
284     private void forceFullNextPoll() {
285         previousSummary = null;
286     }
287 }