]> git.basschouten.com Git - openhab-addons.git/blob
73d7edd564a0cc574b6ddbcd198e676804c90e79
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.wemo.internal.handler;
14
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
17
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
28 import org.openhab.core.config.core.Configuration;
29 import org.openhab.core.io.transport.upnp.UpnpIOService;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.StringType;
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.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.openhab.core.types.State;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * The {@link WemoCrockpotHandler} is responsible for handling commands, which are
45  * sent to one of the channels and to update their states.
46  *
47  * @author Hans-Jörg Merk - Initial contribution;
48  */
49 @NonNullByDefault
50 public class WemoCrockpotHandler extends WemoBaseThingHandler {
51
52     private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
53
54     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
55
56     private final Object upnpLock = new Object();
57     private final Object jobLock = new Object();
58
59     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
60
61     private Map<String, Boolean> subscriptionState = new HashMap<>();
62
63     private @Nullable ScheduledFuture<?> pollingJob;
64
65     public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
66         super(thing, upnpIOService, wemoHttpCaller);
67
68         logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
69     }
70
71     @Override
72     public void initialize() {
73         Configuration configuration = getConfig();
74
75         if (configuration.get(UDN) != null) {
76             logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get(UDN));
77             UpnpIOService localService = service;
78             if (localService != null) {
79                 localService.registerParticipant(this);
80             }
81             host = getHost();
82             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
83                     TimeUnit.SECONDS);
84             updateStatus(ThingStatus.ONLINE);
85         } else {
86             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87                     "@text/config-status.error.missing-udn");
88             logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
89         }
90     }
91
92     @Override
93     public void dispose() {
94         logger.debug("WeMoCrockpotHandler disposed.");
95         ScheduledFuture<?> job = this.pollingJob;
96         if (job != null && !job.isCancelled()) {
97             job.cancel(true);
98         }
99         this.pollingJob = null;
100         removeSubscription();
101     }
102
103     private void poll() {
104         synchronized (jobLock) {
105             if (pollingJob == null) {
106                 return;
107             }
108             try {
109                 logger.debug("Polling job");
110                 host = getHost();
111                 // Check if the Wemo device is set in the UPnP service registry
112                 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
113                 if (!isUpnpDeviceRegistered()) {
114                     logger.debug("UPnP device {} not yet registered", getUDN());
115                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
116                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
117                     synchronized (upnpLock) {
118                         subscriptionState = new HashMap<>();
119                     }
120                     return;
121                 }
122                 updateStatus(ThingStatus.ONLINE);
123                 updateWemoState();
124                 addSubscription();
125             } catch (Exception e) {
126                 logger.debug("Exception during poll: {}", e.getMessage(), e);
127             }
128         }
129     }
130
131     @Override
132     public void handleCommand(ChannelUID channelUID, Command command) {
133         String localHost = getHost();
134         if (localHost.isEmpty()) {
135             logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
136                     getThing().getUID());
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138                     "@text/config-status.error.missing-ip");
139             return;
140         }
141         String wemoURL = getWemoURL(localHost, BASICACTION);
142         if (wemoURL == null) {
143             logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
144                     getThing().getUID());
145             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
146                     "@text/config-status.error.missing-url");
147             return;
148         }
149         String mode = "0";
150         String time = null;
151
152         if (command instanceof RefreshType) {
153             updateWemoState();
154         } else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
155             String commandString = command.toString();
156             switch (commandString) {
157                 case "OFF":
158                     mode = "0";
159                     time = "0";
160                     break;
161                 case "WARM":
162                     mode = "50";
163                     break;
164                 case "LOW":
165                     mode = "51";
166                     break;
167                 case "HIGH":
168                     mode = "52";
169                     break;
170             }
171             try {
172                 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
173                 String content = "<?xml version=\"1.0\"?>"
174                         + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
175                         + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
176                         + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
177                         + "</s:Envelope>";
178                 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
179                 if (wemoCallResponse != null && logger.isTraceEnabled()) {
180                     logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
181                     logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
182                     logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
183                     logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
184                 }
185             } catch (RuntimeException e) {
186                 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
187                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
188             }
189             updateStatus(ThingStatus.ONLINE);
190         }
191     }
192
193     @Override
194     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
195         if (service != null) {
196             logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
197                     succeeded ? "succeeded" : "failed");
198             subscriptionState.put(service, succeeded);
199         }
200     }
201
202     @Override
203     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
204         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
205                 this.getThing().getUID());
206
207         updateStatus(ThingStatus.ONLINE);
208         if (variable != null && value != null) {
209             this.stateMap.put(variable, value);
210         }
211     }
212
213     private synchronized void addSubscription() {
214         synchronized (upnpLock) {
215             UpnpIOService localService = service;
216             if (localService != null) {
217                 if (localService.isRegistered(this)) {
218                     logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
219
220                     String subscription = BASICEVENT;
221
222                     if (subscriptionState.get(subscription) == null) {
223                         logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
224                                 subscription);
225                         localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
226                         subscriptionState.put(subscription, true);
227                     }
228                 } else {
229                     logger.debug(
230                             "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
231                             getThing().getUID());
232                 }
233             }
234         }
235     }
236
237     private synchronized void removeSubscription() {
238         synchronized (upnpLock) {
239             UpnpIOService localService = service;
240             if (localService != null) {
241                 if (localService.isRegistered(this)) {
242                     logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
243                     String subscription = BASICEVENT;
244
245                     if (subscriptionState.get(subscription) != null) {
246                         logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
247                         localService.removeSubscription(this, subscription);
248                     }
249                     subscriptionState.remove(subscription);
250                     localService.unregisterParticipant(this);
251                 }
252             }
253         }
254     }
255
256     /**
257      * The {@link updateWemoState} polls the actual state of a WeMo device and
258      * calls {@link onValueReceived} to update the statemap and channels..
259      *
260      */
261     protected void updateWemoState() {
262         String localHost = getHost();
263         if (localHost.isEmpty()) {
264             logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
265             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
266                     "@text/config-status.error.missing-ip");
267             return;
268         }
269         String actionService = BASICEVENT;
270         String wemoURL = getWemoURL(localHost, actionService);
271         if (wemoURL == null) {
272             logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
273             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
274                     "@text/config-status.error.missing-url");
275             return;
276         }
277         try {
278             String action = "GetCrockpotState";
279             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
280             String content = createStateRequestContent(action, actionService);
281             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
282             if (wemoCallResponse != null) {
283                 if (logger.isTraceEnabled()) {
284                     logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
285                     logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
286                     logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
287                     logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
288                 }
289                 String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
290                 String time = substringBetween(wemoCallResponse, "<time>", "</time>");
291                 String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
292
293                 State newMode = new StringType(mode);
294                 State newCoockedTime = DecimalType.valueOf(coockedTime);
295                 switch (mode) {
296                     case "0":
297                         newMode = new StringType("OFF");
298                         break;
299                     case "50":
300                         newMode = new StringType("WARM");
301                         State warmTime = DecimalType.valueOf(time);
302                         updateState(CHANNEL_WARMCOOKTIME, warmTime);
303                         break;
304                     case "51":
305                         newMode = new StringType("LOW");
306                         State lowTime = DecimalType.valueOf(time);
307                         updateState(CHANNEL_LOWCOOKTIME, lowTime);
308                         break;
309                     case "52":
310                         newMode = new StringType("HIGH");
311                         State highTime = DecimalType.valueOf(time);
312                         updateState(CHANNEL_HIGHCOOKTIME, highTime);
313                         break;
314                 }
315                 updateState(CHANNEL_COOKMODE, newMode);
316                 updateState(CHANNEL_COOKEDTIME, newCoockedTime);
317             }
318         } catch (RuntimeException e) {
319             logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
320             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
321         }
322         updateStatus(ThingStatus.ONLINE);
323     }
324 }