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