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