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