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.core.thing.Bridge;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.thing.ThingUID;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.binding.BridgeHandler;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * Abstract class as base for TAPO-Device device implementations.
47 * @author Christian Wild - Initial contribution
50 public abstract class TapoDevice extends BaseThingHandler {
51 private final Logger logger = LoggerFactory.getLogger(TapoDevice.class);
52 protected final TapoErrorHandler deviceError = new TapoErrorHandler();
53 protected final String uid;
54 protected TapoDeviceConfiguration config;
55 protected TapoDeviceInfo deviceInfo;
56 protected @Nullable ScheduledFuture<?> startupJob;
57 protected @Nullable ScheduledFuture<?> pollingJob;
58 protected @NonNullByDefault({}) TapoDeviceConnector connector;
59 protected @NonNullByDefault({}) TapoBridgeHandler bridge;
64 * @param thing Thing object representing device
66 protected TapoDevice(Thing thing) {
68 this.config = new TapoDeviceConfiguration(thing);
69 this.deviceInfo = new TapoDeviceInfo();
70 this.uid = getThing().getUID().getAsString();
73 /***********************************
77 ************************************/
83 public void initialize() {
85 this.config.loadSettings();
86 Bridge bridgeThing = getBridge();
87 if (bridgeThing != null) {
88 BridgeHandler bridgeHandler = bridgeThing.getHandler();
89 if (bridgeHandler != null) {
90 this.bridge = (TapoBridgeHandler) bridgeHandler;
91 this.connector = new TapoDeviceConnector(this, bridge);
94 } catch (Exception e) {
95 logger.debug("({}) configuration error : {}", uid, e.getMessage());
97 TapoErrorHandler configError = checkSettings();
98 if (!configError.hasError()) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
109 public void dispose() {
111 stopScheduler(this.startupJob);
112 stopScheduler(this.pollingJob);
114 } catch (Exception e) {
123 private void activateDevice() {
124 // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
125 updateStatus(ThingStatus.UNKNOWN);
127 // background initialization (delay it a little bit):
128 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() {
186 protected void startScheduler() {
187 Integer pollingInterval = this.config.pollingInterval;
189 if (pollingInterval > 0) {
190 if (pollingInterval < POLLING_MIN_INTERVAL_S) {
191 pollingInterval = POLLING_MIN_INTERVAL_S;
193 logger.trace("({}) starScheduler: create job with interval : {}", uid, pollingInterval);
194 this.pollingJob = scheduler.scheduleWithFixedDelay(this::schedulerAction, pollingInterval, pollingInterval,
197 stopScheduler(this.pollingJob);
204 * @param scheduler ScheduledFeature<?> which schould be stopped
206 protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
207 if (scheduler != null) {
208 scheduler.cancel(true);
216 protected void schedulerAction() {
217 logger.trace("({}) schedulerAction", uid);
221 /***********************************
225 ************************************/
227 * return device Error
231 public TapoErrorHandler getError() {
232 return this.deviceError;
238 * @param tapoError TapoErrorHandler-Object
240 public void setError(TapoErrorHandler tapoError) {
241 this.deviceError.set(tapoError);
242 handleConnectionState();
245 /***********************************
249 ************************************/
252 * Check if ThingType is model
257 protected Boolean isThingModel(String model) {
259 ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
260 ThingTypeUID expectedType = getThing().getThingTypeUID();
261 return expectedType.equals(foundType);
262 } catch (Exception e) {
263 logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
269 * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
273 * @return true if is the expected device
275 protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
277 String expectedThingUID = getThing().getProperties().get(DEVICE_REPRASENTATION_PROPERTY);
278 String foundThingUID = deviceInfo.getRepresentationProperty();
279 String foundModel = deviceInfo.getModel();
280 if (expectedThingUID == null || expectedThingUID.isBlank()) {
281 return isThingModel(foundModel);
283 /* sometimes received mac was with and sometimes without "-" from device */
284 expectedThingUID = unformatMac(expectedThingUID);
285 foundThingUID = unformatMac(foundThingUID);
286 return expectedThingUID.equals(foundThingUID);
287 } catch (Exception e) {
288 logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
296 public ThingUID getThingUID() {
297 return getThing().getUID();
300 /***********************************
304 ************************************/
307 * query device Properties
309 public void queryDeviceInfo() {
310 queryDeviceInfo(false);
314 * query device Properties
316 * @param ignoreGap ignore gap to last query. query anyway (force)
318 public void queryDeviceInfo(boolean ignoreGap) {
320 if (connector.loggedIn()) {
321 connector.queryInfo(ignoreGap);
323 logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
329 * SET DEVICE INFOs to device
333 public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
334 this.deviceInfo = deviceInfo;
335 if (isExpectedThing(deviceInfo)) {
336 devicePropertiesChanged(deviceInfo);
337 handleConnectionState();
339 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
340 "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
341 + "'. Check IP-Address");
346 * Handle full responsebody received from connector
348 * @param responseBody
350 public void responsePasstrough(String responseBody) {
356 * If only one property must be changed, there is also a convenient method
357 * updateProperty(String name, String value).
359 * @param TapoDeviceInfo
361 protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
362 /* device properties */
363 Map<String, String> properties = editProperties();
364 properties.put(Thing.PROPERTY_MAC_ADDRESS, deviceInfo.getMAC());
365 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getFirmwareVersion());
366 properties.put(Thing.PROPERTY_HARDWARE_VERSION, deviceInfo.getHardwareVersion());
367 properties.put(Thing.PROPERTY_MODEL_ID, deviceInfo.getModel());
368 properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
369 updateProperties(properties);
373 * update channel state
378 public void publishState(String channelID, State value) {
379 updateState(channelID, value);
382 /***********************************
386 ************************************/
389 * Connect (login) to device
392 public Boolean connect() {
394 Boolean loginSuccess = false;
397 loginSuccess = connector.login();
399 connector.queryInfo();
401 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
403 } catch (Exception e) {
404 updateStatus(ThingStatus.UNKNOWN);
412 public void disconnect() {
417 * handle device state by connector error
419 public void handleConnectionState() {
420 ThingStatus deviceState = getThing().getStatus();
421 Integer errorCode = deviceError.getCode();
423 if (errorCode == 0) {
424 if (deviceState != ThingStatus.ONLINE) {
425 updateStatus(ThingStatus.ONLINE);
427 } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
429 } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
430 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
432 } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
433 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
435 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
440 * Return IP-Address of device
442 public String getIpAddress() {
443 return this.config.ipAddress;
446 /***********************************
450 ************************************/
452 * Get ChannelID including group
454 * @param group String channel-group
455 * @param channel String channel-name
456 * @return String channelID
458 protected String getChannelID(String group, String channel) {
459 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
460 if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
461 return group + "#" + channel;
467 * Get Channel from ChannelID
469 * @param channelID String channelID
470 * @return String channel-name
472 protected String getChannelFromID(ChannelUID channelID) {
473 String channel = channelID.getIdWithoutGroup();
474 channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
475 channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
476 channel = channel.replace(CHANNEL_GROUP_EFFECTS + "#", "");