]> git.basschouten.com Git - openhab-addons.git/blob
1d61419d873f37e6c76803f09bf9162a93ec7fc6
[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.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.EcobeeDiscoveryService;
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 DEFAULT_REFRESH_INTERVAL_NORMAL_SECONDS = 20;
65     private static final int DEFAULT_REFRESH_INTERVAL_QUICK_SECONDS = 5;
66     private static final int DEFAULT_API_TIMEOUT_SECONDS = 20;
67
68     private final Logger logger = LoggerFactory.getLogger(EcobeeAccountBridgeHandler.class);
69
70     private final OAuthFactory oAuthFactory;
71     private final HttpClient httpClient;
72
73     private @NonNullByDefault({}) EcobeeApi api;
74     private @NonNullByDefault({}) String apiKey;
75     private int refreshIntervalNormal;
76     private int refreshIntervalQuick;
77     private int apiTimeout;
78     private boolean discoveryEnabled;
79
80     private final Map<String, EcobeeThermostatBridgeHandler> thermostatHandlers = new ConcurrentHashMap<>();
81     private final Set<String> thermostatIds = new CopyOnWriteArraySet<>();
82
83     private @Nullable Future<?> refreshThermostatsJob;
84     private final AtomicInteger refreshThermostatsCounter = new AtomicInteger(REFRESH_STARTUP_DELAY_SECONDS);
85
86     private @Nullable SummaryResponseDTO previousSummary;
87
88     public EcobeeAccountBridgeHandler(final Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
89         super(bridge);
90         this.oAuthFactory = oAuthFactory;
91         this.httpClient = httpClient;
92     }
93
94     @Override
95     public void initialize() {
96         logger.debug("AccountBridge: Initializing");
97
98         EcobeeAccountConfiguration config = getConfigAs(EcobeeAccountConfiguration.class);
99         apiKey = config.apiKey;
100
101         Integer value;
102         value = config.refreshIntervalNormal;
103         refreshIntervalNormal = value == null ? DEFAULT_REFRESH_INTERVAL_NORMAL_SECONDS : value;
104
105         value = config.refreshIntervalQuick;
106         refreshIntervalQuick = value == null ? DEFAULT_REFRESH_INTERVAL_QUICK_SECONDS : value;
107
108         value = config.apiTimeout;
109         apiTimeout = (value == null ? DEFAULT_API_TIMEOUT_SECONDS : value) * 1000;
110
111         Boolean booleanValue = config.discoveryEnabled;
112         discoveryEnabled = booleanValue == null ? false : booleanValue.booleanValue();
113         logger.debug("AccountBridge: Thermostat and sensor discovery is {}", discoveryEnabled ? "enabled" : "disabled");
114
115         api = new EcobeeApi(this, apiKey, apiTimeout, oAuthFactory, httpClient);
116
117         scheduleRefreshJob();
118         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Checking authorization");
119     }
120
121     @Override
122     public void dispose() {
123         cancelRefreshJob();
124         api.closeOAuthClientService();
125         logger.debug("AccountBridge: Disposing");
126     }
127
128     @Override
129     public void handleRemoval() {
130         oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
131         super.handleRemoval();
132     }
133
134     @Override
135     public void handleCommand(ChannelUID channelUID, Command command) {
136     }
137
138     @Override
139     public Collection<Class<? extends ThingHandlerService>> getServices() {
140         return Collections.singleton(EcobeeDiscoveryService.class);
141     }
142
143     @Override
144     public void childHandlerInitialized(ThingHandler thermostatHandler, Thing thermostatThing) {
145         String thermostatId = (String) thermostatThing.getConfiguration().get(CONFIG_THERMOSTAT_ID);
146         thermostatHandlers.put(thermostatId, (EcobeeThermostatBridgeHandler) thermostatHandler);
147         thermostatIds.add(thermostatId);
148         scheduleQuickPoll();
149         logger.debug("AccountBridge: Adding thermostat handler for {} with id {}", thermostatThing.getUID(),
150                 thermostatId);
151     }
152
153     @Override
154     public void childHandlerDisposed(ThingHandler thermostatHandler, Thing thermostatThing) {
155         String thermostatId = (String) thermostatThing.getConfiguration().get(CONFIG_THERMOSTAT_ID);
156         thermostatHandlers.remove(thermostatId);
157         thermostatIds.remove(thermostatId);
158         logger.debug("AccountBridge: Removing thermostat handler for {} with id {}", thermostatThing.getUID(),
159                 thermostatId);
160     }
161
162     public boolean isBackgroundDiscoveryEnabled() {
163         return discoveryEnabled;
164     }
165
166     public void updateBridgeStatus(ThingStatus status) {
167         updateStatus(status);
168     }
169
170     public void updateBridgeStatus(ThingStatus status, ThingStatusDetail statusDetail, String statusMessage) {
171         updateStatus(status, statusDetail, statusMessage);
172     }
173
174     public boolean performThermostatFunction(FunctionRequest request) {
175         boolean success = api.performThermostatFunction(request);
176         if (success) {
177             scheduleQuickPoll();
178         }
179         return success;
180     }
181
182     public boolean performThermostatUpdate(ThermostatUpdateRequestDTO request) {
183         boolean success = api.performThermostatUpdate(request);
184         if (success) {
185             scheduleQuickPoll();
186         }
187         return success;
188     }
189
190     public SelectionDTO getSelection() {
191         SelectionDTO mergedSelection = new SelectionDTO();
192         for (EcobeeThermostatBridgeHandler handler : new ArrayList<EcobeeThermostatBridgeHandler>(
193                 thermostatHandlers.values())) {
194             SelectionDTO selection = handler.getSelection();
195             logger.trace("AccountBridge: Thermostat {} selection: {}", handler.getThing().getUID(), selection);
196             mergedSelection.mergeSelection(selection);
197         }
198         return mergedSelection;
199     }
200
201     public void markOnline() {
202         updateStatus(ThingStatus.ONLINE);
203     }
204
205     public List<ThermostatDTO> getRegisteredThermostats() {
206         return api.queryRegisteredThermostats();
207     }
208
209     /*
210      * The refresh job updates the thermostat channels on the refresh interval set in the thermostat thing config.
211      * The thermostat update process involves first running a thermostat summary transaction to
212      * determine if any thermostat data has changed since the last summary. If any change is detected,
213      * a full query of the thermostats is performed.
214      */
215     private void refresh() {
216         refreshThermostats();
217     }
218
219     @SuppressWarnings("null")
220     private void refreshThermostats() {
221         if (refreshThermostatsCounter.getAndDecrement() == 0) {
222             refreshThermostatsCounter.set(refreshIntervalNormal);
223             SummaryResponseDTO summary = api.performThermostatSummaryQuery();
224             if (summary != null && summary.hasChanged(previousSummary) && !thermostatIds.isEmpty()) {
225                 for (ThermostatDTO thermostat : api.performThermostatQuery(thermostatIds)) {
226                     EcobeeThermostatBridgeHandler handler = thermostatHandlers.get(thermostat.identifier);
227                     if (handler != null) {
228                         handler.updateChannels(thermostat);
229                     }
230                 }
231             }
232             previousSummary = summary;
233         }
234     }
235
236     private void scheduleQuickPoll() {
237         if (refreshThermostatsCounter.get() > refreshIntervalQuick) {
238             logger.debug("AccountBridge: Scheduling quick poll");
239             refreshThermostatsCounter.set(refreshIntervalQuick);
240             forceFullNextPoll();
241         }
242     }
243
244     private void scheduleRefreshJob() {
245         logger.debug("AccountBridge: Scheduling thermostat refresh job");
246         cancelRefreshJob();
247         refreshThermostatsCounter.set(0);
248         refreshThermostatsJob = scheduler.scheduleWithFixedDelay(this::refresh, REFRESH_STARTUP_DELAY_SECONDS,
249                 REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
250     }
251
252     private void cancelRefreshJob() {
253         Future<?> localRefreshThermostatsJob = refreshThermostatsJob;
254         if (localRefreshThermostatsJob != null) {
255             forceFullNextPoll();
256             localRefreshThermostatsJob.cancel(true);
257             logger.debug("AccountBridge: Canceling thermostat refresh job");
258         }
259     }
260
261     private void forceFullNextPoll() {
262         previousSummary = null;
263     }
264 }