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.TapoErrorCode.*;
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.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;
49 * Abstract class as base for TAPO-Device device implementations.
51 * @author Christian Wild - Initial contribution
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;
68 * @param thing Thing object representing device
70 protected TapoDevice(Thing thing) {
72 this.deviceInfo = new TapoDeviceInfo();
73 this.uid = getThing().getUID().getAsString();
76 /***********************************
80 ************************************/
86 public void initialize() {
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);
97 } catch (Exception e) {
98 logger.debug("({}) configuration error : {}", uid, e.getMessage());
100 TapoErrorHandler configError = checkSettings();
101 if (!configError.hasError()) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
112 public void dispose() {
114 stopScheduler(this.startupJob);
115 stopScheduler(this.pollingJob);
117 } catch (Exception e) {
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);
130 // background initialization (delay it a little bit):
131 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_CONFIG_NO_BRIDGE);
147 /* check ip-address */
148 if (!config.ipAddress.matches(IPV4_REGEX)) {
149 configErr.raiseError(ERR_CONFIG_IP);
152 /* check credentials */
153 if (!bridge.getCredentials().areSet()) {
154 configErr.raiseError(ERR_CONFIG_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() {
183 startPollingScheduler();
189 protected void startPollingScheduler() {
190 int pollingInterval = this.config.pollingInterval;
191 TimeUnit timeUnit = TimeUnit.SECONDS;
193 if (pollingInterval > 0) {
194 if (pollingInterval < POLLING_MIN_INTERVAL_S) {
195 pollingInterval = POLLING_MIN_INTERVAL_S;
197 logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
198 this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
199 pollingInterval, timeUnit);
201 logger.debug("({}) scheduler disabled with config '0'", uid);
202 stopScheduler(this.pollingJob);
209 * @param scheduler ScheduledFeature<?> which schould be stopped
211 protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
212 if (scheduler != null) {
213 scheduler.cancel(true);
221 protected void pollingSchedulerAction() {
222 logger.trace("({}) schedulerAction", uid);
226 /***********************************
230 ************************************/
232 * return device Error
236 public TapoErrorHandler getErrorHandler() {
237 return this.deviceError;
240 public TapoErrorCode getError() {
241 return this.deviceError.getError();
247 * @param tapoError TapoErrorHandler-Object
249 public void setError(TapoErrorHandler tapoError) {
250 this.deviceError.set(tapoError);
251 handleConnectionState();
254 /***********************************
258 ************************************/
261 * Check if ThingType is model
266 protected Boolean isThingModel(String model) {
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());
278 * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
282 * @return true if is the expected device
284 protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
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);
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());
305 public ThingUID getThingUID() {
306 return getThing().getUID();
309 /***********************************
313 ************************************/
316 * query device Properties
318 public void queryDeviceInfo() {
319 queryDeviceInfo(false);
323 * query device Properties
325 * @param ignoreGap ignore gap to last query. query anyway (force)
327 public void queryDeviceInfo(boolean ignoreGap) {
329 if (connector.loggedIn()) {
330 connector.queryInfo(ignoreGap);
331 // query energy usage
332 if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
333 connector.getEnergyUsage();
336 if (SUPPORTED_CHILDS_DATA_UIDS.contains(getThing().getThingTypeUID())) {
337 connector.queryChildDevices();
340 logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
346 * SET DEVICE INFOs to device
350 public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
351 this.deviceInfo = deviceInfo;
352 if (isExpectedThing(deviceInfo)) {
353 devicePropertiesChanged(deviceInfo);
354 handleConnectionState();
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
357 "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
358 + "'. Check IP-Address");
363 * Set Device EnergyData to device
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));
377 * Set Device Child data to device
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()));
389 * Handle full responsebody received from connector
391 * @param responseBody
393 public void responsePasstrough(String responseBody) {
399 * If only one property must be changed, there is also a convenient method
400 * updateProperty(String name, String value).
402 * @param TapoDeviceInfo
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);
416 * update channel state
421 public void publishState(String channelID, State value) {
422 updateState(channelID, value);
425 /***********************************
429 ************************************/
432 * Connect (login) to device
435 public Boolean connect() {
437 Boolean loginSuccess = false;
440 loginSuccess = connector.login();
442 queryDeviceInfo(true);
444 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
446 } catch (Exception e) {
447 updateStatus(ThingStatus.UNKNOWN);
455 public void disconnect() {
460 * handle device state by connector error
462 public void handleConnectionState() {
463 ThingStatus deviceState = getThing().getStatus();
464 TapoErrorCode errorCode = deviceError.getError();
466 if (errorCode == TapoErrorCode.NO_ERROR) {
467 if (deviceState != ThingStatus.ONLINE) {
468 updateStatus(ThingStatus.ONLINE);
471 switch (errorCode.getType()) {
472 case COMMUNICATION_RETRY:
475 case COMMUNICATION_ERROR:
476 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
479 case CONFIGURATION_ERROR:
480 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
483 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
489 * Return IP-Address of device
491 public String getIpAddress() {
492 return this.config.ipAddress;
495 /***********************************
499 ************************************/
501 * Get ChannelID including group
503 * @param group String channel-group
504 * @param channel String channel-name
505 * @return String channelID
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;
516 * Get Channel from ChannelID
518 * @param channelID String channelID
519 * @return String channel-name
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 + "#", "");