2 * Copyright (c) 2010-2023 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 = 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;
66 * @param thing Thing object representing device
68 protected TapoDevice(Thing thing) {
70 this.deviceInfo = new TapoDeviceInfo();
71 this.uid = getThing().getUID().getAsString();
74 /***********************************
78 ************************************/
84 public void initialize() {
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);
95 } catch (Exception e) {
96 logger.debug("({}) configuration error : {}", uid, e.getMessage());
98 TapoErrorHandler configError = checkSettings();
99 if (!configError.hasError()) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
110 public void dispose() {
112 stopScheduler(this.startupJob);
113 stopScheduler(this.pollingJob);
115 } catch (Exception e) {
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);
128 // background initialization (delay it a little bit):
129 this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
135 * @return TapoErrorHandler with configuration-errors
137 protected TapoErrorHandler checkSettings() {
138 TapoErrorHandler configErr = new TapoErrorHandler();
141 if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
142 configErr.raiseError(ERR_NO_BRIDGE);
145 /* check ip-address */
146 if (!config.ipAddress.matches(IPV4_REGEX)) {
147 configErr.raiseError(ERR_CONF_IP);
150 /* check credentials */
151 if (!bridge.getCredentials().areSet()) {
152 configErr.raiseError(ERR_CONF_CREDENTIALS);
159 * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
161 * @throws IOException if an error code was set in the response object
163 protected void checkErrors() throws IOException {
164 final Integer errorCode = deviceError.getCode();
166 if (errorCode != 0) {
167 throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
171 /***********************************
175 ************************************/
177 * delayed OneTime StartupJob
179 private void delayedStartUp() {
181 startPollingScheduler();
187 protected void startPollingScheduler() {
188 int pollingInterval = this.config.pollingInterval;
189 TimeUnit timeUnit = TimeUnit.SECONDS;
191 if (pollingInterval > 0) {
192 if (pollingInterval < POLLING_MIN_INTERVAL_S) {
193 pollingInterval = POLLING_MIN_INTERVAL_S;
195 logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
196 this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
197 pollingInterval, timeUnit);
199 logger.debug("({}) scheduler disabled with config '0'", uid);
200 stopScheduler(this.pollingJob);
207 * @param scheduler ScheduledFeature<?> which schould be stopped
209 protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
210 if (scheduler != null) {
211 scheduler.cancel(true);
219 protected void pollingSchedulerAction() {
220 logger.trace("({}) schedulerAction", uid);
224 /***********************************
228 ************************************/
230 * return device Error
234 public TapoErrorHandler getError() {
235 return this.deviceError;
241 * @param tapoError TapoErrorHandler-Object
243 public void setError(TapoErrorHandler tapoError) {
244 this.deviceError.set(tapoError);
245 handleConnectionState();
248 /***********************************
252 ************************************/
255 * Check if ThingType is model
260 protected Boolean isThingModel(String model) {
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());
272 * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
276 * @return true if is the expected device
278 protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
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);
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());
299 public ThingUID getThingUID() {
300 return getThing().getUID();
303 /***********************************
307 ************************************/
310 * query device Properties
312 public void queryDeviceInfo() {
313 queryDeviceInfo(false);
317 * query device Properties
319 * @param ignoreGap ignore gap to last query. query anyway (force)
321 public void queryDeviceInfo(boolean ignoreGap) {
323 if (connector.loggedIn()) {
324 connector.queryInfo(ignoreGap);
325 // query energy usage
326 if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
327 connector.getEnergyUsage();
330 logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
336 * SET DEVICE INFOs to device
340 public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
341 this.deviceInfo = deviceInfo;
342 if (isExpectedThing(deviceInfo)) {
343 devicePropertiesChanged(deviceInfo);
344 handleConnectionState();
346 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
347 "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
348 + "'. Check IP-Address");
353 * Set Device EnergyData to device
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));
367 * Handle full responsebody received from connector
369 * @param responseBody
371 public void responsePasstrough(String responseBody) {
377 * If only one property must be changed, there is also a convenient method
378 * updateProperty(String name, String value).
380 * @param TapoDeviceInfo
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);
394 * update channel state
399 public void publishState(String channelID, State value) {
400 updateState(channelID, value);
403 /***********************************
407 ************************************/
410 * Connect (login) to device
413 public Boolean connect() {
415 Boolean loginSuccess = false;
418 loginSuccess = connector.login();
420 connector.queryInfo();
422 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
424 } catch (Exception e) {
425 updateStatus(ThingStatus.UNKNOWN);
433 public void disconnect() {
438 * handle device state by connector error
440 public void handleConnectionState() {
441 ThingStatus deviceState = getThing().getStatus();
442 Integer errorCode = deviceError.getCode();
444 if (errorCode == 0) {
445 if (deviceState != ThingStatus.ONLINE) {
446 updateStatus(ThingStatus.ONLINE);
448 } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
450 } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
451 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
453 } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
454 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
456 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
461 * Return IP-Address of device
463 public String getIpAddress() {
464 return this.config.ipAddress;
467 /***********************************
471 ************************************/
473 * Get ChannelID including group
475 * @param group String channel-group
476 * @param channel String channel-name
477 * @return String channelID
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;
488 * Get Channel from ChannelID
490 * @param channelID String channelID
491 * @return String channel-name
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 + "#", "");