]> git.basschouten.com Git - openhab-addons.git/blob
2992e4ece72703c2609bc4476aa4fa2608f1a142
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.netatmo.internal.handler;
14
15 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
16
17 import java.lang.reflect.InvocationTargetException;
18 import java.lang.reflect.Method;
19 import java.math.BigDecimal;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
29 import org.openhab.binding.netatmo.internal.RefreshStrategy;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.i18n.TimeZoneProvider;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.PointType;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.types.State;
38 import org.openhab.core.types.UnDefType;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import io.swagger.client.ApiException;
43 import io.swagger.client.model.NAPlace;
44
45 /**
46  * {@link NetatmoDeviceHandler} is the handler for a given
47  * device accessed through the Netatmo Bridge
48  *
49  * @author GaĆ«l L'hopital - Initial contribution
50  */
51 @NonNullByDefault
52 public abstract class NetatmoDeviceHandler<DEVICE> extends AbstractNetatmoThingHandler {
53
54     private static final int MIN_REFRESH_INTERVAL = 2000;
55     private static final int DEFAULT_REFRESH_INTERVAL = 300000;
56
57     private final Logger logger = LoggerFactory.getLogger(NetatmoDeviceHandler.class);
58     private final Object updateLock = new Object();
59     private @Nullable ScheduledFuture<?> refreshJob;
60     private @Nullable RefreshStrategy refreshStrategy;
61     private @Nullable DEVICE device;
62     protected Map<String, Object> childs = new ConcurrentHashMap<>();
63
64     public NetatmoDeviceHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
65         super(thing, timeZoneProvider);
66     }
67
68     @Override
69     protected void initializeThing() {
70         defineRefreshInterval();
71         updateStatus(ThingStatus.ONLINE);
72         scheduleRefreshJob();
73     }
74
75     private void scheduleRefreshJob() {
76         RefreshStrategy strategy = refreshStrategy;
77         if (strategy == null) {
78             return;
79         }
80         long delay = strategy.nextRunDelayInS();
81         logger.debug("Scheduling update channel thread in {} s", delay);
82         refreshJob = scheduler.schedule(() -> {
83             updateChannels(false);
84             ScheduledFuture<?> job = refreshJob;
85             if (job != null) {
86                 job.cancel(false);
87                 refreshJob = null;
88             }
89             scheduleRefreshJob();
90         }, delay, TimeUnit.SECONDS);
91     }
92
93     @Override
94     public void dispose() {
95         logger.debug("Running dispose()");
96         ScheduledFuture<?> job = refreshJob;
97         if (job != null) {
98             job.cancel(true);
99             refreshJob = null;
100         }
101     }
102
103     protected abstract Optional<DEVICE> updateReadings();
104
105     protected void updateProperties(DEVICE deviceData) {
106     }
107
108     @Override
109     protected void updateChannels() {
110         updateChannels(true);
111     }
112
113     private void updateChannels(boolean requireDefinedRefreshInterval) {
114         // Avoid concurrent data readings
115         synchronized (updateLock) {
116             RefreshStrategy strategy = refreshStrategy;
117             if (strategy != null) {
118                 logger.debug("Data aged of {} s", strategy.dataAge() / 1000);
119                 boolean dataOutdated = (requireDefinedRefreshInterval && strategy.isSearchingRefreshInterval()) ? false
120                         : strategy.isDataOutdated();
121                 if (dataOutdated) {
122                     logger.debug("Trying to update channels on device {}", getId());
123                     childs.clear();
124
125                     Optional<DEVICE> newDeviceReading = Optional.empty();
126                     try {
127                         newDeviceReading = updateReadings();
128                     } catch (ApiException e) {
129                         if (logger.isDebugEnabled()) {
130                             // we also attach the stack trace
131                             logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
132                         } else {
133                             logger.error("Unable to connect Netatmo API : {}", e.getMessage());
134                         }
135                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
136                                 "Unable to connect Netatmo API : " + e.getLocalizedMessage());
137                     }
138                     if (newDeviceReading.isPresent()) {
139                         logger.debug("Successfully updated device {} readings! Now updating channels", getId());
140                         DEVICE theDevice = newDeviceReading.get();
141                         this.device = theDevice;
142                         updateStatus(isReachable() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
143                         updateProperties(theDevice);
144                         getDataTimestamp().ifPresent(dataTimeStamp -> {
145                             strategy.setDataTimeStamp(dataTimeStamp, timeZoneProvider.getTimeZone());
146                         });
147                         getRadioHelper().ifPresent(helper -> helper.setModule(theDevice));
148                         getBridgeHandler().ifPresent(handler -> {
149                             handler.checkForNewThings(theDevice);
150                         });
151                     } else {
152                         logger.debug("Failed to update device {} readings! Skip updating channels", getId());
153                     }
154                     // Be sure that all channels for the modules will be updated with refreshed data
155                     childs.forEach((childId, moduleData) -> {
156                         findNAThing(childId).map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> {
157                             naChildModule.setRefreshRequired(true);
158                         });
159                     });
160                 } else {
161                     logger.debug("Data still valid for device {}", getId());
162                 }
163                 super.updateChannels();
164                 updateChildModules();
165             }
166         }
167     }
168
169     @Override
170     protected State getNAThingProperty(String channelId) {
171         try {
172             Optional<DEVICE> dev = getDevice();
173             switch (channelId) {
174                 case CHANNEL_LAST_STATUS_STORE:
175                     if (dev.isPresent()) {
176                         Method getLastStatusStore = dev.get().getClass().getMethod("getLastStatusStore");
177                         Integer lastStatusStore = (Integer) getLastStatusStore.invoke(dev.get());
178                         return ChannelTypeUtils.toDateTimeType(lastStatusStore, timeZoneProvider.getTimeZone());
179                     } else {
180                         return UnDefType.UNDEF;
181                     }
182                 case CHANNEL_LOCATION:
183                     if (dev.isPresent()) {
184                         Method getPlace = dev.get().getClass().getMethod("getPlace");
185                         NAPlace place = (NAPlace) getPlace.invoke(dev.get());
186                         PointType point = new PointType(new DecimalType(place.getLocation().get(1)),
187                                 new DecimalType(place.getLocation().get(0)));
188                         if (place.getAltitude() != null) {
189                             point.setAltitude(new DecimalType(place.getAltitude()));
190                         }
191                         return point;
192                     } else {
193                         return UnDefType.UNDEF;
194                     }
195             }
196         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
197             logger.debug("The device has no method to access {} property ", channelId);
198             return UnDefType.NULL;
199         }
200
201         return super.getNAThingProperty(channelId);
202     }
203
204     private void updateChildModules() {
205         logger.debug("Updating child modules of {}", getId());
206         childs.forEach((childId, moduleData) -> {
207             findNAThing(childId).map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> {
208                 logger.debug("Updating child module {}", naChildModule.getId());
209                 naChildModule.updateChannels(moduleData);
210             });
211         });
212     }
213
214     /*
215      * Sets the refresh rate of the device depending whether it's a property
216      * of the thing or if it's defined by configuration
217      */
218     private void defineRefreshInterval() {
219         BigDecimal dataValidityPeriod;
220         if (thing.getProperties().containsKey(PROPERTY_REFRESH_PERIOD)) {
221             String refreshPeriodProperty = thing.getProperties().get(PROPERTY_REFRESH_PERIOD);
222             if ("auto".equalsIgnoreCase(refreshPeriodProperty)) {
223                 dataValidityPeriod = new BigDecimal(-1);
224             } else {
225                 dataValidityPeriod = new BigDecimal(refreshPeriodProperty);
226             }
227         } else {
228             Configuration conf = config;
229             Object interval = conf != null ? conf.get(REFRESH_INTERVAL) : null;
230             if (interval instanceof BigDecimal) {
231                 dataValidityPeriod = (BigDecimal) interval;
232                 if (dataValidityPeriod.intValue() < MIN_REFRESH_INTERVAL) {
233                     logger.info(
234                             "Refresh interval setting is too small for thing {}, {} ms is considered as refresh interval.",
235                             thing.getUID(), MIN_REFRESH_INTERVAL);
236                     dataValidityPeriod = new BigDecimal(MIN_REFRESH_INTERVAL);
237                 }
238             } else {
239                 dataValidityPeriod = new BigDecimal(DEFAULT_REFRESH_INTERVAL);
240             }
241         }
242         refreshStrategy = new RefreshStrategy(dataValidityPeriod.intValue());
243     }
244
245     protected abstract Optional<Integer> getDataTimestamp();
246
247     public void expireData() {
248         RefreshStrategy strategy = refreshStrategy;
249         if (strategy != null) {
250             strategy.expireData();
251         }
252     }
253
254     protected Optional<DEVICE> getDevice() {
255         return Optional.ofNullable(device);
256     }
257 }