]> git.basschouten.com Git - openhab-addons.git/blob
f4a5a0a500561f61bb2197011f80c23089af19f0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.sensorcommunity.internal.handler;
14
15 import java.time.LocalDateTime;
16 import java.util.Optional;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.sensorcommunity.internal.SensorCommunityConfiguration;
23 import org.openhab.binding.sensorcommunity.internal.utils.Constants;
24 import org.openhab.binding.sensorcommunity.internal.utils.DateTimeUtils;
25 import org.openhab.core.thing.ChannelUID;
26 import org.openhab.core.thing.Thing;
27 import org.openhab.core.thing.ThingStatus;
28 import org.openhab.core.thing.ThingStatusDetail;
29 import org.openhab.core.thing.binding.BaseThingHandler;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.RefreshType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * The {@link PMHandler} is responsible for handling commands, which are
37  * sent to one of the channels.
38  *
39  * @author Bernd Weymann - Initial contribution
40  */
41 @NonNullByDefault
42 public abstract class BaseSensorHandler extends BaseThingHandler {
43     private static final SensorCommunityConfiguration DEFAULT_CONFIG = new SensorCommunityConfiguration();
44     private static final String EMPTY = "";
45
46     protected static final int REFRESH_INTERVAL_MIN = 5;
47     protected final Logger logger = LoggerFactory.getLogger(BaseSensorHandler.class);
48     protected SensorCommunityConfiguration config = DEFAULT_CONFIG;
49     protected ConfigStatus configStatus = ConfigStatus.UNKNOWN;
50     protected ThingStatus myThingStatus = ThingStatus.UNKNOWN;
51     protected UpdateStatus lastUpdateStatus = UpdateStatus.UNKNOWN;
52     protected @Nullable ScheduledFuture<?> refreshJob;
53     private Optional<String> sensorUrl = Optional.empty();
54     private boolean firstUpdate = true;
55
56     public enum ConfigStatus {
57         INTERNAL_SENSOR_OK,
58         EXTERNAL_SENSOR_OK,
59         IS_NULL,
60         SENSOR_IS_NULL,
61         SENSOR_ID_NEGATIVE,
62         UNKNOWN
63     };
64
65     public enum UpdateStatus {
66         OK,
67         CONNECTION_ERROR,
68         CONNECTION_EXCEPTION,
69         VALUE_ERROR,
70         VALUE_EMPTY,
71         UNKNOWN
72     }
73
74     protected LifecycleStatus lifecycleStatus = LifecycleStatus.UNKNOWN;
75
76     public enum LifecycleStatus {
77         UNKNOWN,
78         RUNNING,
79         INITIALIZING,
80         DISPOSED
81     }
82
83     public BaseSensorHandler(Thing thing) {
84         super(thing);
85     }
86
87     @Override
88     public void handleCommand(ChannelUID channelUID, Command command) {
89         if (command instanceof RefreshType) {
90             updateFromCache();
91         }
92     }
93
94     @Override
95     public void initialize() {
96         firstUpdate = true;
97         lifecycleStatus = LifecycleStatus.INITIALIZING;
98         scheduler.execute(this::startUp);
99     }
100
101     private void startUp() {
102         config = getConfigAs(SensorCommunityConfiguration.class);
103         configStatus = checkConfig(config);
104         if (configStatus == ConfigStatus.INTERNAL_SENSOR_OK || configStatus == ConfigStatus.EXTERNAL_SENSOR_OK) {
105             // start getting values
106             dataUpdate();
107         } else {
108             // config error, no further actions triggered - Thing Status visible in UI
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "Configuration not valid. Sensor ID as a number is mandatory!");
111         }
112         lifecycleStatus = LifecycleStatus.RUNNING;
113     }
114
115     private void startSchedule() {
116         ScheduledFuture<?> localRefreshJob = refreshJob;
117         if (localRefreshJob != null) {
118             if (localRefreshJob.isCancelled()) {
119                 refreshJob = scheduler.scheduleWithFixedDelay(this::dataUpdate, 5, REFRESH_INTERVAL_MIN,
120                         TimeUnit.MINUTES);
121             } // else - scheduler is already running!
122         } else {
123             refreshJob = scheduler.scheduleWithFixedDelay(this::dataUpdate, 5, REFRESH_INTERVAL_MIN, TimeUnit.MINUTES);
124         }
125     }
126
127     @Override
128     public void dispose() {
129         ScheduledFuture<?> localRefreshJob = refreshJob;
130         if (localRefreshJob != null) {
131             localRefreshJob.cancel(true);
132         }
133         lifecycleStatus = LifecycleStatus.DISPOSED;
134     }
135
136     /**
137      * Checks if config is valid - a) not null and b) sensorid is a number
138      *
139      * @param c
140      * @return
141      */
142     private ConfigStatus checkConfig(@Nullable SensorCommunityConfiguration c) {
143         if (c != null) {
144             if (c.ipAddress != null && !Constants.EMPTY.equals(c.ipAddress)) {
145                 sensorUrl = Optional.of("http://" + c.ipAddress + "/data.json");
146                 return ConfigStatus.INTERNAL_SENSOR_OK;
147             } else {
148                 if (c.sensorid >= 0) {
149                     sensorUrl = Optional.of("http://data.sensor.community/airrohr/v1/sensor/" + c.sensorid + "/");
150                     return ConfigStatus.EXTERNAL_SENSOR_OK;
151                 } else {
152                     return ConfigStatus.SENSOR_ID_NEGATIVE;
153                 }
154             }
155         } else {
156             return ConfigStatus.IS_NULL;
157         }
158     }
159
160     public LifecycleStatus getLifecycleStatus() {
161         return lifecycleStatus;
162     }
163
164     protected void dataUpdate() {
165         if (sensorUrl.isPresent()) {
166             HTTPHandler.getHandler().request(sensorUrl.get(), this);
167         }
168     }
169
170     public void onResponse(String data) {
171         if (firstUpdate) {
172             logger.debug("{} delivers {}", sensorUrl.get(), data);
173             firstUpdate = false;
174         }
175         if (configStatus == ConfigStatus.INTERNAL_SENSOR_OK) {
176             lastUpdateStatus = updateChannels("[" + data + "]");
177         } else {
178             lastUpdateStatus = updateChannels(data);
179         }
180         statusUpdate(lastUpdateStatus, EMPTY);
181     }
182
183     public void onError(String errorReason) {
184         statusUpdate(UpdateStatus.CONNECTION_EXCEPTION,
185                 errorReason + " / " + LocalDateTime.now().format(DateTimeUtils.DTF));
186     }
187
188     protected void statusUpdate(UpdateStatus updateStatus, String details) {
189         if (updateStatus == UpdateStatus.OK) {
190             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
191             startSchedule();
192         } else {
193             switch (updateStatus) {
194                 case CONNECTION_ERROR:
195                     // start job even first update delivers no data - recovery is possible
196                     startSchedule();
197                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
198                             "Update failed due to Connection error. Trying to recover in next refresh");
199                     break;
200                 case CONNECTION_EXCEPTION:
201                     // start job even first update delivers a Connection Exception - recovery is possible
202                     startSchedule();
203                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, details);
204                     break;
205                 case VALUE_EMPTY:
206                     // start job even if first update delivers no values - recovery possible
207                     startSchedule();
208                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE,
209                             "No values delivered by Sensor. Trying to recover in next refresh");
210                     break;
211                 case VALUE_ERROR:
212                     // final status - values from sensor are wrong and manual check is needed
213                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
214                             "Sensor values doesn't match - please check if Sensor ID is delivering the correct Thing channel values");
215                     break;
216                 default:
217                     // final status - Configuration is wrong
218                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
219                             "Error during update - please check your config data");
220                     break;
221             }
222         }
223     }
224
225     @Override
226     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
227         myThingStatus = status;
228         super.updateStatus(status, statusDetail, description);
229     }
230
231     protected abstract UpdateStatus updateChannels(@Nullable String json);
232
233     protected abstract void updateFromCache();
234 }