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