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