2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tapocontrol.internal.device;
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.*;
20 import java.io.IOException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
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;
47 * Abstract class as base for TAPO-Device device implementations.
49 * @author Christian Wild - Initial contribution
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;
66 * @param thing Thing object representing device
68 protected TapoDevice(Thing thing) {
70 this.config = new TapoDeviceConfiguration(thing);
71 this.deviceInfo = new TapoDeviceInfo();
72 this.uid = getThing().getUID().getAsString();
75 /***********************************
79 ************************************/
85 public void initialize() {
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);
96 } catch (Exception e) {
97 logger.debug("({}) configuration error : {}", uid, e.getMessage());
99 TapoErrorHandler configError = checkSettings();
100 if (!configError.hasError()) {
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
111 public void dispose() {
113 stopScheduler(this.startupJob);
114 stopScheduler(this.pollingJob);
116 } catch (Exception e) {
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);
129 // background initialization (delay it a little bit):
130 this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
137 * @return TapoErrorHandler with configuration-errors
139 protected TapoErrorHandler checkSettings() {
140 TapoErrorHandler configErr = new TapoErrorHandler();
143 if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
144 configErr.raiseError(ERR_NO_BRIDGE);
147 /* check ip-address */
148 if (!config.ipAddress.matches(IPV4_REGEX)) {
149 configErr.raiseError(ERR_CONF_IP);
152 /* check credentials */
153 if (!bridge.getCredentials().areSet()) {
154 configErr.raiseError(ERR_CONF_CREDENTIALS);
161 * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
163 * @throws IOException if an error code was set in the response object
165 protected void checkErrors() throws IOException {
166 final Integer errorCode = deviceError.getCode();
168 if (errorCode != 0) {
169 throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
173 /***********************************
177 ************************************/
179 * delayed OneTime StartupJob
181 private void delayedStartUp() {
188 protected void startScheduler() {
189 Integer pollingInterval = this.config.pollingInterval;
191 if (pollingInterval > 0) {
192 if (pollingInterval < POLLING_MIN_INTERVAL_S) {
193 pollingInterval = POLLING_MIN_INTERVAL_S;
195 logger.trace("({}) starScheduler: create job with interval : {}", uid, pollingInterval);
196 this.pollingJob = scheduler.scheduleWithFixedDelay(this::schedulerAction, pollingInterval, pollingInterval,
199 stopScheduler(this.pollingJob);
206 * @param scheduler ScheduledFeature<?> which schould be stopped
208 protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
209 if (scheduler != null) {
210 scheduler.cancel(true);
218 protected void schedulerAction() {
219 logger.trace("({}) schedulerAction", uid);
223 /***********************************
227 ************************************/
229 * return device Error
233 public TapoErrorHandler getError() {
234 return this.deviceError;
240 * @param tapoError TapoErrorHandler-Object
242 public void setError(TapoErrorHandler tapoError) {
243 this.deviceError.set(tapoError);
244 handleConnectionState();
247 /***********************************
251 ************************************/
254 * Check if ThingType is model
259 protected Boolean isThingModel(String model) {
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());
271 * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
275 * @return true if is the expected device
277 protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
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);
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());
298 public ThingUID getThingUID() {
299 return getThing().getUID();
302 /***********************************
306 ************************************/
309 * query device Properties
311 public void queryDeviceInfo() {
312 queryDeviceInfo(false);
316 * query device Properties
318 * @param ignoreGap ignore gap to last query. query anyway (force)
320 public void queryDeviceInfo(boolean ignoreGap) {
322 if (connector.loggedIn()) {
323 connector.queryInfo(ignoreGap);
324 // query energy usage
325 if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
326 connector.getEnergyUsage();
329 logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
335 * SET DEVICE INFOs to device
339 public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
340 this.deviceInfo = deviceInfo;
341 if (isExpectedThing(deviceInfo)) {
342 devicePropertiesChanged(deviceInfo);
343 handleConnectionState();
345 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
346 "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
347 + "'. Check IP-Address");
352 * Set Device EnergyData to device
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));
366 * Handle full responsebody received from connector
368 * @param responseBody
370 public void responsePasstrough(String responseBody) {
376 * If only one property must be changed, there is also a convenient method
377 * updateProperty(String name, String value).
379 * @param TapoDeviceInfo
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);
393 * update channel state
398 public void publishState(String channelID, State value) {
399 updateState(channelID, value);
402 /***********************************
406 ************************************/
409 * Connect (login) to device
412 public Boolean connect() {
414 Boolean loginSuccess = false;
417 loginSuccess = connector.login();
419 connector.queryInfo();
421 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
423 } catch (Exception e) {
424 updateStatus(ThingStatus.UNKNOWN);
432 public void disconnect() {
437 * handle device state by connector error
439 public void handleConnectionState() {
440 ThingStatus deviceState = getThing().getStatus();
441 Integer errorCode = deviceError.getCode();
443 if (errorCode == 0) {
444 if (deviceState != ThingStatus.ONLINE) {
445 updateStatus(ThingStatus.ONLINE);
447 } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
449 } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
452 } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
453 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
455 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
460 * Return IP-Address of device
462 public String getIpAddress() {
463 return this.config.ipAddress;
466 /***********************************
470 ************************************/
472 * Get ChannelID including group
474 * @param group String channel-group
475 * @param channel String channel-name
476 * @return String channelID
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;
487 * Get Channel from ChannelID
489 * @param channelID String channelID
490 * @return String channel-name
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 + "#", "");