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.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;
48 * Abstract class as base for TAPO-Device device implementations.
50 * @author Christian Wild - Initial contribution
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;
67 * @param thing Thing object representing device
69 protected TapoDevice(Thing thing) {
71 this.deviceInfo = new TapoDeviceInfo();
72 this.uid = getThing().getUID().getAsString();
75 /***********************************
79 ************************************/
85 public void initialize() {
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);
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);
136 * @return TapoErrorHandler with configuration-errors
138 protected TapoErrorHandler checkSettings() {
139 TapoErrorHandler configErr = new TapoErrorHandler();
142 if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
143 configErr.raiseError(ERR_NO_BRIDGE);
146 /* check ip-address */
147 if (!config.ipAddress.matches(IPV4_REGEX)) {
148 configErr.raiseError(ERR_CONF_IP);
151 /* check credentials */
152 if (!bridge.getCredentials().areSet()) {
153 configErr.raiseError(ERR_CONF_CREDENTIALS);
160 * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
162 * @throws IOException if an error code was set in the response object
164 protected void checkErrors() throws IOException {
165 final Integer errorCode = deviceError.getCode();
167 if (errorCode != 0) {
168 throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
172 /***********************************
176 ************************************/
178 * delayed OneTime StartupJob
180 private void delayedStartUp() {
182 startPollingScheduler();
188 protected void startPollingScheduler() {
189 int pollingInterval = this.config.pollingInterval;
190 TimeUnit timeUnit = TimeUnit.SECONDS;
192 if (pollingInterval > 0) {
193 if (pollingInterval < POLLING_MIN_INTERVAL_S) {
194 pollingInterval = POLLING_MIN_INTERVAL_S;
196 logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
197 this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
198 pollingInterval, timeUnit);
200 logger.debug("({}) scheduler disabled with config '0'", uid);
201 stopScheduler(this.pollingJob);
208 * @param scheduler ScheduledFeature<?> which schould be stopped
210 protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
211 if (scheduler != null) {
212 scheduler.cancel(true);
220 protected void pollingSchedulerAction() {
221 logger.trace("({}) schedulerAction", uid);
225 /***********************************
229 ************************************/
231 * return device Error
235 public TapoErrorHandler getError() {
236 return this.deviceError;
242 * @param tapoError TapoErrorHandler-Object
244 public void setError(TapoErrorHandler tapoError) {
245 this.deviceError.set(tapoError);
246 handleConnectionState();
249 /***********************************
253 ************************************/
256 * Check if ThingType is model
261 protected Boolean isThingModel(String model) {
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());
273 * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
277 * @return true if is the expected device
279 protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
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);
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());
300 public ThingUID getThingUID() {
301 return getThing().getUID();
304 /***********************************
308 ************************************/
311 * query device Properties
313 public void queryDeviceInfo() {
314 queryDeviceInfo(false);
318 * query device Properties
320 * @param ignoreGap ignore gap to last query. query anyway (force)
322 public void queryDeviceInfo(boolean ignoreGap) {
324 if (connector.loggedIn()) {
325 connector.queryInfo(ignoreGap);
326 // query energy usage
327 if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
328 connector.getEnergyUsage();
331 if (SUPPORTED_CHILDS_DATA_UIDS.contains(getThing().getThingTypeUID())) {
332 connector.queryChildDevices();
335 logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
341 * SET DEVICE INFOs to device
345 public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
346 this.deviceInfo = deviceInfo;
347 if (isExpectedThing(deviceInfo)) {
348 devicePropertiesChanged(deviceInfo);
349 handleConnectionState();
351 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
352 "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
353 + "'. Check IP-Address");
358 * Set Device EnergyData to device
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));
372 * Set Device Child data to device
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()));
384 * Handle full responsebody received from connector
386 * @param responseBody
388 public void responsePasstrough(String responseBody) {
394 * If only one property must be changed, there is also a convenient method
395 * updateProperty(String name, String value).
397 * @param TapoDeviceInfo
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);
411 * update channel state
416 public void publishState(String channelID, State value) {
417 updateState(channelID, value);
420 /***********************************
424 ************************************/
427 * Connect (login) to device
430 public Boolean connect() {
432 Boolean loginSuccess = false;
435 loginSuccess = connector.login();
437 connector.queryInfo();
439 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
441 } catch (Exception e) {
442 updateStatus(ThingStatus.UNKNOWN);
450 public void disconnect() {
455 * handle device state by connector error
457 public void handleConnectionState() {
458 ThingStatus deviceState = getThing().getStatus();
459 Integer errorCode = deviceError.getCode();
461 if (errorCode == 0) {
462 if (deviceState != ThingStatus.ONLINE) {
463 updateStatus(ThingStatus.ONLINE);
465 } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
467 } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
468 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
470 } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
471 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
473 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
478 * Return IP-Address of device
480 public String getIpAddress() {
481 return this.config.ipAddress;
484 /***********************************
488 ************************************/
490 * Get ChannelID including group
492 * @param group String channel-group
493 * @param channel String channel-name
494 * @return String channelID
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;
505 * Get Channel from ChannelID
507 * @param channelID String channelID
508 * @return String channel-name
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 + "#", "");