2 * Copyright (c) 2010-2021 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.netatmo.internal.handler;
15 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
17 import java.lang.reflect.InvocationTargetException;
18 import java.lang.reflect.Method;
19 import java.math.BigDecimal;
21 import java.util.Optional;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
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;
42 import io.swagger.client.ApiException;
43 import io.swagger.client.model.NAPlace;
46 * {@link NetatmoDeviceHandler} is the handler for a given
47 * device accessed through the Netatmo Bridge
49 * @author Gaƫl L'hopital - Initial contribution
52 public abstract class NetatmoDeviceHandler<DEVICE> extends AbstractNetatmoThingHandler {
54 private static final int MIN_REFRESH_INTERVAL = 2000;
55 private static final int DEFAULT_REFRESH_INTERVAL = 300000;
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<>();
64 public NetatmoDeviceHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
65 super(thing, timeZoneProvider);
69 protected void initializeThing() {
70 defineRefreshInterval();
71 updateStatus(ThingStatus.ONLINE);
75 private void scheduleRefreshJob() {
76 RefreshStrategy strategy = refreshStrategy;
77 if (strategy == null) {
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;
90 }, delay, TimeUnit.SECONDS);
94 public void dispose() {
95 logger.debug("Running dispose()");
96 ScheduledFuture<?> job = refreshJob;
103 protected abstract Optional<DEVICE> updateReadings();
105 protected void updateProperties(DEVICE deviceData) {
109 protected void updateChannels() {
110 updateChannels(true);
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();
122 logger.debug("Trying to update channels on device {}", getId());
125 Optional<DEVICE> newDeviceReading = Optional.empty();
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);
133 logger.error("Unable to connect Netatmo API : {}", e.getMessage());
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
136 "Unable to connect Netatmo API : " + e.getLocalizedMessage());
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());
147 getRadioHelper().ifPresent(helper -> helper.setModule(theDevice));
148 getBridgeHandler().ifPresent(handler -> {
149 handler.checkForNewThings(theDevice);
152 logger.debug("Failed to update device {} readings! Skip updating channels", getId());
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);
161 logger.debug("Data still valid for device {}", getId());
163 super.updateChannels();
164 updateChildModules();
170 protected State getNAThingProperty(String channelId) {
172 Optional<DEVICE> dev = getDevice();
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());
180 return UnDefType.UNDEF;
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()));
193 return UnDefType.UNDEF;
196 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
197 logger.debug("The device has no method to access {} property ", channelId);
198 return UnDefType.NULL;
201 return super.getNAThingProperty(channelId);
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);
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
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);
225 dataValidityPeriod = new BigDecimal(refreshPeriodProperty);
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) {
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);
239 dataValidityPeriod = new BigDecimal(DEFAULT_REFRESH_INTERVAL);
242 refreshStrategy = new RefreshStrategy(dataValidityPeriod.intValue());
245 protected abstract Optional<Integer> getDataTimestamp();
247 public void expireData() {
248 RefreshStrategy strategy = refreshStrategy;
249 if (strategy != null) {
250 strategy.expireData();
254 protected Optional<DEVICE> getDevice() {
255 return Optional.ofNullable(device);