]> git.basschouten.com Git - openhab-addons.git/blob
784f5b2d9cd187f725cdc18e87c5581ab861f410
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.tapocontrol.internal.device;
14
15 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
16 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
17 import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
18 import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
19
20 import java.io.IOException;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
28 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
29 import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceConfiguration;
30 import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.thing.ThingUID;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.binding.BridgeHandler;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * Abstract class as base for TAPO-Device device implementations.
46  *
47  * @author Christian Wild - Initial contribution
48  */
49 @NonNullByDefault
50 public abstract class TapoDevice extends BaseThingHandler {
51     private final Logger logger = LoggerFactory.getLogger(TapoDevice.class);
52     protected final TapoErrorHandler deviceError = new TapoErrorHandler();
53     protected final String uid;
54     protected TapoDeviceConfiguration config;
55     protected TapoDeviceInfo deviceInfo;
56     protected @Nullable ScheduledFuture<?> startupJob;
57     protected @Nullable ScheduledFuture<?> pollingJob;
58     protected @NonNullByDefault({}) TapoDeviceConnector connector;
59     protected @NonNullByDefault({}) TapoBridgeHandler bridge;
60
61     /**
62      * Constructor
63      *
64      * @param thing Thing object representing device
65      */
66     protected TapoDevice(Thing thing) {
67         super(thing);
68         this.config = new TapoDeviceConfiguration(thing);
69         this.deviceInfo = new TapoDeviceInfo();
70         this.uid = getThing().getUID().getAsString();
71     }
72
73     /***********************************
74      *
75      * INIT AND SETTINGS
76      *
77      ************************************/
78
79     /**
80      * INITIALIZE DEVICE
81      */
82     @Override
83     public void initialize() {
84         try {
85             this.config.loadSettings();
86             Bridge bridgeThing = getBridge();
87             if (bridgeThing != null) {
88                 BridgeHandler bridgeHandler = bridgeThing.getHandler();
89                 if (bridgeHandler != null) {
90                     this.bridge = (TapoBridgeHandler) bridgeHandler;
91                     this.connector = new TapoDeviceConnector(this, bridge);
92                 }
93             }
94         } catch (Exception e) {
95             logger.debug("({}) configuration error : {}", uid, e.getMessage());
96         }
97         TapoErrorHandler configError = checkSettings();
98         if (!configError.hasError()) {
99             activateDevice();
100         } else {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
102         }
103     }
104
105     /**
106      * DISPOSE
107      */
108     @Override
109     public void dispose() {
110         try {
111             stopScheduler(this.startupJob);
112             stopScheduler(this.pollingJob);
113             connector.logout();
114         } catch (Exception e) {
115             // handle exception
116         }
117         super.dispose();
118     }
119
120     /**
121      * ACTIVATE DEVICE
122      */
123     private void activateDevice() {
124         // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
125         updateStatus(ThingStatus.UNKNOWN);
126
127         // background initialization (delay it a little bit):
128         this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
129         startScheduler();
130     }
131
132     /**
133      * CHECK SETTINGS
134      * 
135      * @return TapoErrorHandler with configuration-errors
136      */
137     protected TapoErrorHandler checkSettings() {
138         TapoErrorHandler configErr = new TapoErrorHandler();
139
140         /* check bridge */
141         if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
142             configErr.raiseError(ERR_NO_BRIDGE);
143             return configErr;
144         }
145         /* check ip-address */
146         if (!config.ipAddress.matches(IPV4_REGEX)) {
147             configErr.raiseError(ERR_CONF_IP);
148             return configErr;
149         }
150         /* check credentials */
151         if (!bridge.getCredentials().areSet()) {
152             configErr.raiseError(ERR_CONF_CREDENTIALS);
153             return configErr;
154         }
155         return configErr;
156     }
157
158     /**
159      * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
160      *
161      * @throws IOException if an error code was set in the response object
162      */
163     protected void checkErrors() throws IOException {
164         final Integer errorCode = deviceError.getCode();
165
166         if (errorCode != 0) {
167             throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
168         }
169     }
170
171     /***********************************
172      *
173      * SCHEDULER
174      *
175      ************************************/
176     /**
177      * delayed OneTime StartupJob
178      */
179     private void delayedStartUp() {
180         connect();
181     }
182
183     /**
184      * Start scheduler
185      */
186     protected void startScheduler() {
187         Integer pollingInterval = this.config.pollingInterval;
188
189         if (pollingInterval > 0) {
190             if (pollingInterval < POLLING_MIN_INTERVAL_S) {
191                 pollingInterval = POLLING_MIN_INTERVAL_S;
192             }
193             logger.trace("({}) starScheduler: create job with interval : {}", uid, pollingInterval);
194             this.pollingJob = scheduler.scheduleWithFixedDelay(this::schedulerAction, pollingInterval, pollingInterval,
195                     TimeUnit.SECONDS);
196         } else {
197             stopScheduler(this.pollingJob);
198         }
199     }
200
201     /**
202      * Stop scheduler
203      * 
204      * @param scheduler ScheduledFeature<?> which schould be stopped
205      */
206     protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
207         if (scheduler != null) {
208             scheduler.cancel(true);
209             scheduler = null;
210         }
211     }
212
213     /**
214      * Scheduler Action
215      */
216     protected void schedulerAction() {
217         logger.trace("({}) schedulerAction", uid);
218         queryDeviceInfo();
219     }
220
221     /***********************************
222      *
223      * ERROR HANDLER
224      *
225      ************************************/
226     /**
227      * return device Error
228      * 
229      * @return
230      */
231     public TapoErrorHandler getError() {
232         return this.deviceError;
233     }
234
235     /**
236      * set device error
237      * 
238      * @param tapoError TapoErrorHandler-Object
239      */
240     public void setError(TapoErrorHandler tapoError) {
241         this.deviceError.set(tapoError);
242         handleConnectionState();
243     }
244
245     /***********************************
246      *
247      * THING
248      *
249      ************************************/
250
251     /***
252      * Check if ThingType is model
253      * 
254      * @param model
255      * @return
256      */
257     protected Boolean isThingModel(String model) {
258         try {
259             ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
260             ThingTypeUID expectedType = getThing().getThingTypeUID();
261             return expectedType.equals(foundType);
262         } catch (Exception e) {
263             logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
264             return false;
265         }
266     }
267
268     /**
269      * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
270      * Compare MAC-Adress
271      * 
272      * @param deviceInfo
273      * @return true if is the expected device
274      */
275     protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
276         try {
277             String expectedThingUID = getThing().getProperties().get(DEVICE_REPRASENTATION_PROPERTY);
278             String foundThingUID = deviceInfo.getRepresentationProperty();
279             String foundModel = deviceInfo.getModel();
280             if (expectedThingUID == null || expectedThingUID.isBlank()) {
281                 return isThingModel(foundModel);
282             }
283             /* sometimes received mac was with and sometimes without "-" from device */
284             expectedThingUID = unformatMac(expectedThingUID);
285             foundThingUID = unformatMac(foundThingUID);
286             return expectedThingUID.equals(foundThingUID);
287         } catch (Exception e) {
288             logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
289             return false;
290         }
291     }
292
293     /**
294      * Return ThingUID
295      */
296     public ThingUID getThingUID() {
297         return getThing().getUID();
298     }
299
300     /***********************************
301      *
302      * DEVICE PROPERTIES
303      *
304      ************************************/
305
306     /**
307      * query device Properties
308      */
309     public void queryDeviceInfo() {
310         queryDeviceInfo(false);
311     }
312
313     /**
314      * query device Properties
315      * 
316      * @param ignoreGap ignore gap to last query. query anyway (force)
317      */
318     public void queryDeviceInfo(boolean ignoreGap) {
319         deviceError.reset();
320         if (connector.loggedIn()) {
321             connector.queryInfo(ignoreGap);
322         } else {
323             logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
324             connect();
325         }
326     }
327
328     /**
329      * SET DEVICE INFOs to device
330      * 
331      * @param deviceInfo
332      */
333     public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
334         this.deviceInfo = deviceInfo;
335         if (isExpectedThing(deviceInfo)) {
336             devicePropertiesChanged(deviceInfo);
337             handleConnectionState();
338         } else {
339             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
340                     "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
341                             + "'. Check IP-Address");
342         }
343     }
344
345     /**
346      * Handle full responsebody received from connector
347      * 
348      * @param responseBody
349      */
350     public void responsePasstrough(String responseBody) {
351     }
352
353     /**
354      * UPDATE PROPERTIES
355      * 
356      * If only one property must be changed, there is also a convenient method
357      * updateProperty(String name, String value).
358      * 
359      * @param TapoDeviceInfo
360      */
361     protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
362         /* device properties */
363         Map<String, String> properties = editProperties();
364         properties.put(Thing.PROPERTY_MAC_ADDRESS, deviceInfo.getMAC());
365         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getFirmwareVersion());
366         properties.put(Thing.PROPERTY_HARDWARE_VERSION, deviceInfo.getHardwareVersion());
367         properties.put(Thing.PROPERTY_MODEL_ID, deviceInfo.getModel());
368         properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
369         updateProperties(properties);
370     }
371
372     /**
373      * update channel state
374      * 
375      * @param channelID
376      * @param value
377      */
378     public void publishState(String channelID, State value) {
379         updateState(channelID, value);
380     }
381
382     /***********************************
383      *
384      * CONNECTION
385      *
386      ************************************/
387
388     /**
389      * Connect (login) to device
390      * 
391      */
392     public Boolean connect() {
393         deviceError.reset();
394         Boolean loginSuccess = false;
395
396         try {
397             loginSuccess = connector.login();
398             if (loginSuccess) {
399                 connector.queryInfo();
400             } else {
401                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
402             }
403         } catch (Exception e) {
404             updateStatus(ThingStatus.UNKNOWN);
405         }
406         return loginSuccess;
407     }
408
409     /**
410      * disconnect device
411      */
412     public void disconnect() {
413         connector.logout();
414     }
415
416     /**
417      * handle device state by connector error
418      */
419     public void handleConnectionState() {
420         ThingStatus deviceState = getThing().getStatus();
421         Integer errorCode = deviceError.getCode();
422
423         if (errorCode == 0) {
424             if (deviceState != ThingStatus.ONLINE) {
425                 updateStatus(ThingStatus.ONLINE);
426             }
427         } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
428             connect();
429         } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
430             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
431             disconnect();
432         } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
433             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
434         } else {
435             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
436         }
437     }
438
439     /**
440      * Return IP-Address of device
441      */
442     public String getIpAddress() {
443         return this.config.ipAddress;
444     }
445
446     /***********************************
447      *
448      * CHANNELS
449      *
450      ************************************/
451     /**
452      * Get ChannelID including group
453      * 
454      * @param group String channel-group
455      * @param channel String channel-name
456      * @return String channelID
457      */
458     protected String getChannelID(String group, String channel) {
459         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
460         if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
461             return group + "#" + channel;
462         }
463         return channel;
464     }
465
466     /**
467      * Get Channel from ChannelID
468      * 
469      * @param channelID String channelID
470      * @return String channel-name
471      */
472     protected String getChannelFromID(ChannelUID channelID) {
473         String channel = channelID.getIdWithoutGroup();
474         channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
475         channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
476         channel = channel.replace(CHANNEL_GROUP_EFFECTS + "#", "");
477         return channel;
478     }
479 }