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