2 * Copyright (c) 2010-2020 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.ecobee.internal.handler;
15 import static org.openhab.binding.ecobee.internal.EcobeeBindingConstants.CONFIG_THERMOSTAT_ID;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.List;
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;
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;
54 * The {@link EcobeeAccountBridgeHandler} is responsible for managing
55 * communication with the Ecobee API.
57 * @author Mark Hilbush - Initial contribution
60 public class EcobeeAccountBridgeHandler extends BaseBridgeHandler {
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;
70 private final Logger logger = LoggerFactory.getLogger(EcobeeAccountBridgeHandler.class);
72 private final OAuthFactory oAuthFactory;
73 private final HttpClient httpClient;
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;
83 private final Map<String, EcobeeThermostatBridgeHandler> thermostatHandlers = new ConcurrentHashMap<>();
84 private final Set<String> thermostatIds = new CopyOnWriteArraySet<>();
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;
91 private @Nullable SummaryResponseDTO previousSummary;
93 public EcobeeAccountBridgeHandler(final Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
95 this.oAuthFactory = oAuthFactory;
96 this.httpClient = httpClient;
100 public void initialize() {
101 logger.debug("AccountBridge: Initializing");
103 EcobeeAccountConfiguration config = getConfigAs(EcobeeAccountConfiguration.class);
104 apiKey = config.apiKey;
107 value = config.refreshIntervalNormal;
108 refreshIntervalNormal = value == null ? DEFAULT_REFRESH_INTERVAL_NORMAL_SECONDS : value;
110 value = config.refreshIntervalQuick;
111 refreshIntervalQuick = value == null ? DEFAULT_REFRESH_INTERVAL_QUICK_SECONDS : value;
113 value = config.apiTimeout;
114 apiTimeout = (value == null ? DEFAULT_API_TIMEOUT_SECONDS : value) * 1000;
116 Boolean booleanValue = config.discoveryEnabled;
117 discoveryEnabled = booleanValue == null ? false : booleanValue.booleanValue();
118 logger.debug("AccountBridge: Thermostat and sensor discovery is {}", discoveryEnabled ? "enabled" : "disabled");
120 value = config.discoveryInterval;
121 discoveryInterval = value == null ? DISCOVERY_INTERVAL_SECONDS : value;
123 api = new EcobeeApi(this, apiKey, apiTimeout, oAuthFactory, httpClient);
125 scheduleRefreshJob();
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Checking authorization");
130 public void dispose() {
132 api.closeOAuthClientService();
133 logger.debug("AccountBridge: Disposing");
137 public void handleCommand(ChannelUID channelUID, Command command) {
141 public Collection<Class<? extends ThingHandlerService>> getServices() {
142 return Collections.singleton(ThermostatDiscoveryService.class);
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);
151 logger.debug("AccountBridge: Adding thermostat handler for {} with id {}", thermostatThing.getUID(),
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(),
164 public void setDiscoveryService(ThermostatDiscoveryService discoveryService) {
165 this.discoveryService = discoveryService;
168 public boolean isDiscoveryEnabled() {
169 return discoveryEnabled;
172 public void updateBridgeStatus(ThingStatus status) {
173 updateStatus(status);
176 public void updateBridgeStatus(ThingStatus status, ThingStatusDetail statusDetail, String statusMessage) {
177 updateStatus(status, statusDetail, statusMessage);
180 public boolean performThermostatFunction(FunctionRequest request) {
181 boolean success = api.performThermostatFunction(request);
188 public boolean performThermostatUpdate(ThermostatUpdateRequestDTO request) {
189 boolean success = api.performThermostatUpdate(request);
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);
204 return mergedSelection;
207 public void markOnline() {
208 updateStatus(ThingStatus.ONLINE);
211 public List<ThermostatDTO> getRegisteredThermostats() {
212 return api.queryRegisteredThermostats();
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
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.
224 private void refresh() {
225 refreshThermostats();
226 discoverThermostats();
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);
242 previousSummary = summary;
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();
259 private void scheduleQuickPoll() {
260 if (refreshThermostatsCounter.get() > refreshIntervalQuick) {
261 logger.debug("AccountBridge: Scheduling quick poll");
262 refreshThermostatsCounter.set(refreshIntervalQuick);
267 private void scheduleRefreshJob() {
268 logger.debug("AccountBridge: Scheduling thermostat refresh job");
270 refreshThermostatsCounter.set(0);
271 refreshThermostatsJob = scheduler.scheduleWithFixedDelay(this::refresh, REFRESH_STARTUP_DELAY_SECONDS,
272 REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
275 private void cancelRefreshJob() {
276 Future<?> localRefreshThermostatsJob = refreshThermostatsJob;
277 if (localRefreshThermostatsJob != null) {
279 localRefreshThermostatsJob.cancel(true);
280 logger.debug("AccountBridge: Canceling thermostat refresh job");
284 private void forceFullNextPoll() {
285 previousSummary = null;