]> git.basschouten.com Git - openhab-addons.git/blob
5ae338eccd8d9f8c1f82aa106680f1992cbb19c4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.magentatv.internal.handler;
14
15 import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
16 import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
17
18 import java.text.DateFormat;
19 import java.text.MessageFormat;
20 import java.text.ParseException;
21 import java.text.SimpleDateFormat;
22 import java.time.Instant;
23 import java.time.ZoneId;
24 import java.time.ZonedDateTime;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.TimeZone;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31
32 import javax.measure.Unit;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager;
37 import org.openhab.binding.magentatv.internal.MagentaTVException;
38 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEvent;
39 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEventInstanceCreator;
40 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEvent;
41 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEventInstanceCreator;
42 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatus;
43 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatusInstanceCreator;
44 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfo;
45 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfoInstanceCreator;
46 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse;
47 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponse;
48 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentials;
49 import org.openhab.binding.magentatv.internal.config.MagentaTVDynamicConfig;
50 import org.openhab.binding.magentatv.internal.config.MagentaTVThingConfiguration;
51 import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork;
52 import org.openhab.core.config.core.Configuration;
53 import org.openhab.core.library.types.DateTimeType;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.NextPreviousType;
56 import org.openhab.core.library.types.OnOffType;
57 import org.openhab.core.library.types.PlayPauseType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.RewindFastforwardType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.Units;
62 import org.openhab.core.thing.ChannelUID;
63 import org.openhab.core.thing.Thing;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.binding.BaseThingHandler;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.openhab.core.types.State;
70 import org.openhab.core.types.UnDefType;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73
74 import com.google.gson.Gson;
75 import com.google.gson.GsonBuilder;
76
77 /**
78  * The {@link MagentaTVHandler} is responsible for handling commands, which are
79  * sent to one of the channels.
80  *
81  * @author Markus Michels - Initial contribution
82  */
83 @NonNullByDefault
84 public class MagentaTVHandler extends BaseThingHandler implements MagentaTVListener {
85     private final Logger logger = LoggerFactory.getLogger(MagentaTVHandler.class);
86
87     protected MagentaTVDynamicConfig config = new MagentaTVDynamicConfig();
88     private final Gson gson;
89     protected final MagentaTVNetwork network;
90     protected final MagentaTVDeviceManager manager;
91     protected MagentaTVControl control = new MagentaTVControl();
92
93     private String thingId = "";
94     private volatile int idRefresh = 0;
95     private @Nullable ScheduledFuture<?> initializeJob;
96     private @Nullable ScheduledFuture<?> pairingWatchdogJob;
97     private @Nullable ScheduledFuture<?> renewEventJob;
98
99     /**
100      * Constructor, save bindingConfig (services as default for thingConfig)
101      *
102      * @param thing
103      * @param bindingConfig
104      */
105     public MagentaTVHandler(MagentaTVDeviceManager manager, Thing thing, MagentaTVNetwork network) {
106         super(thing);
107         this.manager = manager;
108         this.network = network;
109         gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new MRProgramInfoEventInstanceCreator())
110                 .registerTypeAdapter(OAuthTokenResponse.class, new MRProgramStatusInstanceCreator())
111                 .registerTypeAdapter(OAuthAuthenticateResponse.class, new MRShortProgramInfoInstanceCreator())
112                 .registerTypeAdapter(OAuthAuthenticateResponse.class, new MRPayEventInstanceCreator()).create();
113     }
114
115     /**
116      * Thing initialization:
117      * - initialize thing status from UPnP discovery, thing config, local network settings
118      * - initiate OAuth if userId is not configured and credentials are available
119      * - wait for NotifyServlet to initialize (solves timing issues on fast startup)
120      */
121     @Override
122     public void initialize() {
123         // The framework requires you to return from this method quickly. For that the initialization itself is executed
124         // asynchronously
125         String label = getThing().getLabel();
126         thingId = label != null ? label : getThing().getUID().toString();
127         resetEventChannels();
128         updateStatus(ThingStatus.UNKNOWN);
129         config = new MagentaTVDynamicConfig(getConfigAs(MagentaTVThingConfiguration.class));
130         try {
131             initializeJob = scheduler.schedule(this::initializeThing, 5, TimeUnit.SECONDS);
132         } catch (RuntimeException e) {
133             logger.warn("Unable to schedule thing initialization", e);
134         }
135     }
136
137     private void initializeThing() {
138         String errorMessage = "";
139         try {
140             if (config.getUDN().isEmpty()) {
141                 // get UDN from device name
142                 String uid = this.getThing().getUID().getAsString();
143                 config.setUDN(substringAfterLast(uid, ":"));
144             }
145             if (config.getMacAddress().isEmpty()) {
146                 // get MAC address from UDN (last 12 digits)
147                 String macAddress = substringAfterLast(config.getUDN(), "_");
148                 if (macAddress.isEmpty()) {
149                     macAddress = substringAfterLast(config.getUDN(), "-");
150                 }
151                 config.setMacAddress(macAddress);
152             }
153             control = new MagentaTVControl(config, network);
154             config.updateNetwork(control.getConfig()); // get network parameters from control
155
156             // Check for emoty credentials (e.g. missing in .things file)
157             String account = config.getAccountName();
158             if (config.getUserId().isEmpty()) {
159                 if (account.isEmpty()) {
160                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161                             "Credentials missing or invalid! Fill credentials into thing configuration or generate UID on the openHAB console - see README");
162                     return;
163                 }
164
165                 getUserId();
166             }
167
168             connectReceiver(); // throws MagentaTVException on error
169
170             // setup background device check
171             renewEventJob = scheduler.scheduleWithFixedDelay(this::renewEventSubscription, 2, 5, TimeUnit.MINUTES);
172
173             // change to ThingStatus.ONLINE will be done when the pairing result is received
174             // (see onPairingResult())
175         } catch (MagentaTVException e) {
176             errorMessage = e.toString();
177         } catch (RuntimeException e) {
178             logger.warn("{}: Exception on initialization", thingId, e);
179         } finally {
180             if (!errorMessage.isEmpty()) {
181                 logger.debug("{}: {}", thingId, errorMessage);
182                 setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
183             }
184         }
185     }
186
187     /**
188      * This routine is called every time the Thing configuration has been changed (e.g. PaperUI)
189      */
190     @Override
191     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
192         logger.debug("{}: Thing config updated, re-initialize", thingId);
193         cancelAllJobs();
194         if (configurationParameters.containsKey(PROPERTY_ACCT_NAME)) {
195             @Nullable
196             String newAccount = (String) configurationParameters.get(PROPERTY_ACCT_NAME);
197             if ((newAccount != null) && !newAccount.isEmpty()) {
198                 // new account info, need to renew userId
199                 config.setUserId("");
200             }
201         }
202
203         super.handleConfigurationUpdate(configurationParameters);
204     }
205
206     /**
207      * Handle channel commands
208      *
209      * @param channelUID - the channel, which received the command
210      * @param command - the actual command (could be instance of StringType,
211      *            DecimalType or OnOffType)
212      */
213     @Override
214     public void handleCommand(ChannelUID channelUID, Command command) {
215         if (command == RefreshType.REFRESH) {
216             // currently no channels to be refreshed
217             return;
218         }
219
220         try {
221             if (!isOnline() || command.toString().equalsIgnoreCase("PAIR")) {
222                 logger.debug("{}: Receiver {} is offline, try to (re-)connect", thingId, deviceName());
223                 connectReceiver(); // reconnect to MR, throws an exception if this fails
224             }
225
226             logger.debug("{}: Channel command for device {}: {} for channel {}", thingId, config.getFriendlyName(),
227                     command, channelUID.getId());
228             switch (channelUID.getId()) {
229                 case CHANNEL_POWER: // toggle power
230                     logger.debug("{}: Toggle power, new state={}", thingId, command);
231                     control.sendKey("POWER");
232                     break;
233                 case CHANNEL_PLAYER:
234                     logger.debug("{}: Player command: {}", thingId, command);
235                     if (command instanceof OnOffType) {
236                         control.sendKey("POWER");
237                     } else if (command instanceof PlayPauseType) {
238                         if (command == PlayPauseType.PLAY) {
239                             control.sendKey("PLAY");
240                         } else if (command == PlayPauseType.PAUSE) {
241                             control.sendKey("PAUSE");
242                         }
243                     } else if (command instanceof NextPreviousType) {
244                         if (command == NextPreviousType.NEXT) {
245                             control.sendKey("NEXTCH");
246                         } else if (command == NextPreviousType.PREVIOUS) {
247                             control.sendKey("PREVCH");
248                         }
249                     } else if (command instanceof RewindFastforwardType) {
250                         if (command == RewindFastforwardType.FASTFORWARD) {
251                             control.sendKey("FORWARD");
252                         } else if (command == RewindFastforwardType.REWIND) {
253                             control.sendKey("REWIND");
254                         }
255                     } else {
256                         logger.debug("{}: Unknown media command: {}", thingId, command);
257                     }
258                     break;
259                 case CHANNEL_CHANNEL:
260                     String chan = command.toString();
261                     control.selectChannel(chan);
262                     break;
263                 case CHANNEL_MUTE:
264                     if (command == OnOffType.ON) {
265                         control.sendKey("MUTE");
266                     } else {
267                         control.sendKey("VOLUP");
268                     }
269                     break;
270                 case CHANNEL_KEY:
271                     if (command.toString().equalsIgnoreCase("PAIR")) { // special key to re-pair receiver (already done
272                                                                        // above)
273                         logger.debug("{}: PAIRing key received, reconnect receiver {}", thingId, deviceName());
274                     } else {
275                         control.sendKey(command.toString());
276                         mapKeyToMediateState(command.toString());
277                     }
278                     break;
279                 default:
280                     logger.debug("{}: Command {} for unknown channel {}", thingId, command, channelUID.getAsString());
281             }
282         } catch (MagentaTVException e) {
283             String errorMessage = MessageFormat.format("Channel operation failed (command={0}, value={1}): {2}",
284                     command, channelUID.getId(), e.getMessage());
285             logger.debug("{}: {}", thingId, errorMessage);
286             setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
287         }
288     }
289
290     private void mapKeyToMediateState(String key) {
291         State state = null;
292         switch (key.toUpperCase()) {
293             case "PLAY":
294                 state = PlayPauseType.PLAY;
295                 break;
296             case "PAUSE":
297                 state = PlayPauseType.PAUSE;
298                 break;
299             case "FORWARD":
300                 state = RewindFastforwardType.FASTFORWARD;
301                 break;
302             case "REWIND":
303                 updateState(CHANNEL_PLAYER, RewindFastforwardType.REWIND);
304                 break;
305         }
306         if (state != null) {
307             logger.debug("{}: Setting Player state to {}", thingId, state);
308             updateState(CHANNEL_PLAYER, state);
309         }
310     }
311
312     /**
313      * Connect to the receiver
314      *
315      * @throws MagentaTVException something failed
316      */
317     protected void connectReceiver() throws MagentaTVException {
318         if (control.checkDev()) {
319             updateThingProperties();
320             manager.registerDevice(config.getUDN(), config.getTerminalID(), config.getIpAddress(), this);
321             control.subscribeEventChannel();
322             control.sendPairingRequest();
323
324             // check for pairing timeout
325             final int iRefresh = ++idRefresh;
326             pairingWatchdogJob = scheduler.schedule(() -> {
327                 if (iRefresh == idRefresh) { // Make a best effort to not run multiple deferred refresh
328                     if (config.getVerificationCode().isEmpty()) {
329                         setOnlineStatus(ThingStatus.OFFLINE, "Timeout on pairing request!");
330                     }
331                 }
332             }, 15, TimeUnit.SECONDS);
333         }
334     }
335
336     /**
337      * If userId is empty and credentials are given the Telekom OAuth service is
338      * used to query the userId
339      *
340      * @throws MagentaTVException
341      */
342     private void getUserId() throws MagentaTVException {
343         String userId = config.getUserId();
344         if (userId.isEmpty()) {
345             // run OAuth authentication, this finally provides the userId
346             logger.debug("{}: Login with account {}", thingId, config.getAccountName());
347             userId = control.getUserId(config.getAccountName(), config.getAccountPassword());
348
349             // Update thing configuration (persistent) - remove credentials, add userId
350             Configuration configuration = this.getConfig();
351             configuration.remove(PROPERTY_ACCT_NAME);
352             configuration.remove(PROPERTY_ACCT_PWD);
353             configuration.remove(PROPERTY_USERID);
354             configuration.put(PROPERTY_ACCT_NAME, "");
355             configuration.put(PROPERTY_ACCT_PWD, "");
356             configuration.put(PROPERTY_USERID, userId);
357             this.updateConfiguration(configuration);
358             config.setAccountName("");
359             config.setAccountPassword("");
360         } else {
361             logger.debug("{}: Skip OAuth, use existing userId {}", thingId, config.getUserId());
362         }
363         if (!userId.isEmpty()) {
364             config.setUserId(userId);
365         } else {
366             logger.warn("{}: Unable to obtain userId from OAuth", thingId);
367         }
368     }
369
370     /**
371      * Update thing status
372      *
373      * @param mode new thing status
374      * @return ON = power on, OFF=power off
375      */
376     public void setOnlineStatus(ThingStatus newStatus, String errorMessage) {
377         ThingStatus status = this.getThing().getStatus();
378         if (status != newStatus) {
379             if (newStatus == ThingStatus.ONLINE) {
380                 updateStatus(newStatus);
381                 updateState(CHANNEL_POWER, OnOffType.ON);
382             } else {
383                 if (!errorMessage.isEmpty()) {
384                     logger.debug("{}: Communication Error - {}, switch Thing offline", thingId, errorMessage);
385                     updateStatus(newStatus, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
386                 } else {
387                     updateStatus(newStatus);
388                 }
389                 updateState(CHANNEL_POWER, OnOffType.OFF);
390             }
391         }
392     }
393
394     /**
395      * A wakeup of the MR was detected (e.g. UPnP received)
396      *
397      * @throws MagentaTVException
398      */
399     @Override
400     public void onWakeup(Map<String, String> discoveredProperties) throws MagentaTVException {
401         if ((this.getThing().getStatus() == ThingStatus.OFFLINE) || config.getVerificationCode().isEmpty()) {
402             // Device sent a UPnP discovery information, trigger to reconnect
403             connectReceiver();
404         } else {
405             logger.debug("{}: Refesh device status for {} (UDN={}", thingId, deviceName(), config.getUDN());
406             setOnlineStatus(ThingStatus.ONLINE, "");
407         }
408     }
409
410     /**
411      * The pairing result has been received. The pairing code will be used to generate the verification code and
412      * complete pairing with the MR. Finally if pairing was completed successful the thing status will change to ONLINE
413      *
414      * @param pairingCode pairing code received from MR (NOTIFY event data)
415      * @throws MagentaTVException
416      */
417     @Override
418     public void onPairingResult(String pairingCode) throws MagentaTVException {
419         if (control.isInitialized()) {
420             if (control.generateVerificationCode(pairingCode)) {
421                 config.setPairingCode(pairingCode);
422                 logger.debug(
423                         "{}: Pairing code received (UDN {}, terminalID {}, pairingCode={}, verificationCode={}, userId={})",
424                         thingId, config.getUDN(), config.getTerminalID(), config.getPairingCode(),
425                         config.getVerificationCode(), config.getUserId());
426
427                 // verify pairing completes the pairing process
428                 if (control.verifyPairing()) {
429                     logger.debug("{}: Pairing completed for device {} ({}), Thing now ONLINE", thingId,
430                             config.getFriendlyName(), config.getTerminalID());
431                     setOnlineStatus(ThingStatus.ONLINE, "");
432                     cancelPairingCheck(); // stop timeout check
433                 }
434             }
435             updateThingProperties(); // persist pairing and verification code
436         } else {
437             logger.debug("{}: control not yet initialized!", thingId);
438         }
439     }
440
441     @Override
442     public void onMREvent(String jsonInput) {
443         logger.trace("{}: Process MR event for device {}, json={}", thingId, deviceName(), jsonInput);
444         boolean flUpdatePower = false;
445         String jsonEvent = fixEventJson(jsonInput);
446         if (jsonEvent.contains(MR_EVENT_EIT_CHANGE)) {
447             logger.debug("{}: EVENT_EIT_CHANGE event received.", thingId);
448
449             MRProgramInfoEvent pinfo = gson.fromJson(jsonEvent, MRProgramInfoEvent.class);
450             if (!pinfo.channelNum.isEmpty()) {
451                 logger.debug("{}: EVENT_EIT_CHANGE for channel {}/{}", thingId, pinfo.channelNum, pinfo.channelCode);
452                 updateState(CHANNEL_CHANNEL, new DecimalType(pinfo.channelNum));
453                 updateState(CHANNEL_CHANNEL_CODE, new DecimalType(pinfo.channelCode));
454             }
455             if (pinfo.programInfo != null) {
456                 int i = 0;
457                 for (MRProgramStatus ps : pinfo.programInfo) {
458                     if ((ps.startTime == null) || ps.startTime.isEmpty()) {
459                         logger.debug("{}: EVENT_EIT_CHANGE: empty event data = {}", thingId, jsonEvent);
460                         continue; // empty program_info
461                     }
462                     updateState(CHANNEL_RUN_STATUS, new StringType(control.getRunStatus(ps.runningStatus)));
463
464                     if (ps.shortEvent != null) {
465                         for (MRShortProgramInfo se : ps.shortEvent) {
466                             if ((ps.startTime == null) || ps.startTime.isEmpty()) {
467                                 logger.debug("{}: EVENT_EIT_CHANGE: empty program info", thingId);
468                                 continue;
469                             }
470                             // Convert UTC to local time
471                             // 2018/11/04 21:45:00 -> "2018-11-04T10:15:30.00Z"
472                             String tsLocal = ps.startTime.replace('/', '-').replace(" ", "T") + "Z";
473                             Instant timestamp = Instant.parse(tsLocal);
474                             ZonedDateTime localTime = timestamp.atZone(ZoneId.of("Europe/Berlin"));
475                             tsLocal = substringBeforeLast(localTime.toString(), "[");
476                             tsLocal = substringBefore(tsLocal.replace('-', '/').replace('T', ' '), "+");
477
478                             logger.debug("{}: Info for channel {} / {} - {} {}.{}, start time={}, duration={}", thingId,
479                                     pinfo.channelNum, pinfo.channelCode, control.getRunStatus(ps.runningStatus),
480                                     se.eventName, se.textChar, tsLocal, ps.duration);
481                             if (ps.runningStatus != EV_EITCHG_RUNNING_NOT_RUNNING) {
482                                 updateState(CHANNEL_PROG_TITLE, new StringType(se.eventName));
483                                 updateState(CHANNEL_PROG_TEXT, new StringType(se.textChar));
484                                 updateState(CHANNEL_PROG_START, new DateTimeType(localTime));
485
486                                 try {
487                                     DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
488                                     dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
489                                     Date date = dateFormat.parse(ps.duration);
490                                     long minutes = date.getTime() / 1000L / 60l;
491                                     updateState(CHANNEL_PROG_DURATION, toQuantityType(minutes, Units.MINUTE));
492                                 } catch (ParseException e) {
493                                     logger.debug("{}: Unable to parse programDuration: {}", thingId, ps.duration);
494                                 }
495
496                                 if (i++ == 0) {
497                                     flUpdatePower = true;
498                                 }
499                             }
500                         }
501                     }
502                 }
503             }
504         } else if (jsonEvent.contains("new_play_mode")) {
505             MRPayEvent event = gson.fromJson(jsonEvent, MRPayEvent.class);
506             if (event.duration == null) {
507                 event.duration = -1;
508             }
509             if (event.playPostion == null) {
510                 event.playPostion = -1;
511             }
512             logger.debug("{}: STB event playContent: playMode={}, duration={}, playPosition={}", thingId,
513                     control.getPlayStatus(event.newPlayMode), event.duration, event.playPostion);
514
515             // If we get a playConfig event there MR must be online. However it also sends a
516             // plyMode stop before powering off the device, so we filter this.
517             if ((event.newPlayMode != EV_PLAYCHG_STOP) && this.isInitialized()) {
518                 flUpdatePower = true;
519             }
520             if (event.newPlayMode != -1) {
521                 String playMode = control.getPlayStatus(event.newPlayMode);
522                 updateState(CHANNEL_PLAY_MODE, new StringType(playMode));
523                 mapPlayModeToMediaControl(playMode);
524             }
525             if (event.duration > 0) {
526                 updateState(CHANNEL_PROG_DURATION, new StringType(event.duration.toString()));
527             }
528             if (event.playPostion != -1) {
529                 updateState(CHANNEL_PROG_POS, toQuantityType(event.playPostion / 6, Units.MINUTE));
530             }
531         } else {
532             logger.debug("{}: Unknown MR event, JSON={}", thingId, jsonEvent);
533         }
534         if (flUpdatePower) {
535             // We received a non-stopped event -> MR must be on
536             updateState(CHANNEL_POWER, OnOffType.ON);
537         }
538     }
539
540     private void mapPlayModeToMediaControl(String playMode) {
541         switch (playMode) {
542             case "playing":
543             case "playing (MC)":
544             case "playing (UC)":
545             case "buffering":
546                 logger.debug("{}: Setting Player state to PLAY", thingId);
547                 updateState(CHANNEL_PLAYER, PlayPauseType.PLAY);
548                 break;
549             case "paused":
550             case "stopped":
551                 logger.debug("{}: Setting Player state to PAUSE", thingId);
552                 updateState(CHANNEL_PLAYER, PlayPauseType.PAUSE);
553                 break;
554         }
555     }
556
557     /**
558      * When the MR powers off it send a UPnP message, which is catched by the binding.
559      */
560     @Override
561     public void onPowerOff() throws MagentaTVException {
562         logger.debug("{}: Power-Off received for device {}", thingId, deviceName());
563         // MR was powered off -> update power status, reset items
564         resetEventChannels();
565     }
566
567     private void resetEventChannels() {
568         updateState(CHANNEL_POWER, OnOffType.OFF);
569         updateState(CHANNEL_PROG_TITLE, StringType.EMPTY);
570         updateState(CHANNEL_PROG_TEXT, StringType.EMPTY);
571         updateState(CHANNEL_PROG_START, StringType.EMPTY);
572         updateState(CHANNEL_PROG_DURATION, DecimalType.ZERO);
573         updateState(CHANNEL_PROG_POS, DecimalType.ZERO);
574         updateState(CHANNEL_CHANNEL, DecimalType.ZERO);
575         updateState(CHANNEL_CHANNEL_CODE, DecimalType.ZERO);
576     }
577
578     private String fixEventJson(String jsonEvent) {
579         // MR401: channel_num is a string -> ok
580         // MR201: channel_num is an int -> fix JSON formatting to String
581         if (jsonEvent.contains(MR_EVENT_CHAN_TAG) && !jsonEvent.contains(MR_EVENT_CHAN_TAG + "\"")) {
582             // hack: reformat the JSON string to make it compatible with the GSON parsing
583             logger.trace("{}: malformed JSON->fix channel_num", thingId);
584             String start = substringBefore(jsonEvent, MR_EVENT_CHAN_TAG); // up to "channel_num":
585             String end = substringAfter(jsonEvent, MR_EVENT_CHAN_TAG); // behind "channel_num":
586             String chan = substringBetween(jsonEvent, MR_EVENT_CHAN_TAG, ",").trim();
587             return start + "\"channel_num\":" + "\"" + chan + "\"" + end;
588         }
589         return jsonEvent;
590     }
591
592     private boolean isOnline() {
593         return this.getThing().getStatus() == ThingStatus.ONLINE;
594     }
595
596     /**
597      * Renew the event subscription. The periodic refresh is required, otherwise the receive will stop sending events.
598      * Reconnect if nessesary.
599      */
600     private void renewEventSubscription() {
601         if (!control.isInitialized()) {
602             return;
603         }
604         logger.debug("{}: Check receiver status, current state  {}/{}", thingId,
605                 this.getThing().getStatusInfo().getStatus(), this.getThing().getStatusInfo().getStatusDetail());
606
607         try {
608             // when pairing is completed re-new event channel subscription
609             if ((this.getThing().getStatus() != ThingStatus.OFFLINE) && !config.getVerificationCode().isEmpty()) {
610                 logger.debug("{}: Renew MR event subscription for device {}", thingId, deviceName());
611                 control.subscribeEventChannel();
612             }
613         } catch (MagentaTVException e) {
614             logger.warn("{}: Re-new event subscription failed: {}", deviceName(), e.toString());
615         }
616
617         // another try: if the above SUBSCRIBE fails, try a re-connect immediatly
618         try {
619             if ((this.getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR)
620                     && !config.getUserId().isEmpty()) {
621                 // if we have no userId the OAuth is not completed or pairing process got stuck
622                 logger.debug("{}: Reconnect media receiver", deviceName());
623                 connectReceiver(); // throws MagentaTVException on error
624             }
625         } catch (MagentaTVException | RuntimeException e) {
626             logger.debug("{}: Re-connect to receiver failed: {}", deviceName(), e.toString());
627         }
628     }
629
630     public void updateThingProperties() {
631         Map<String, String> properties = new HashMap<String, String>();
632         properties.put(PROPERTY_FRIENDLYNAME, config.getFriendlyName());
633         properties.put(PROPERTY_MODEL_NUMBER, config.getModel());
634         properties.put(PROPERTY_DESC_URL, config.getDescriptionUrl());
635         properties.put(PROPERTY_PAIRINGCODE, config.getPairingCode());
636         properties.put(PROPERTY_VERIFICATIONCODE, config.getVerificationCode());
637         properties.put(PROPERTY_LOCAL_IP, config.getLocalIP());
638         properties.put(PROPERTY_TERMINALID, config.getLocalIP());
639         properties.put(PROPERTY_LOCAL_MAC, config.getLocalMAC());
640         properties.put(PROPERTY_WAKEONLAN, config.getWakeOnLAN());
641         updateProperties(properties);
642     }
643
644     public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
645         return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
646     }
647
648     private String deviceName() {
649         return config.getFriendlyName() + "(" + config.getTerminalID() + ")";
650     }
651
652     private void cancelJob(@Nullable ScheduledFuture<?> job) {
653         if ((job != null) && !job.isCancelled()) {
654             job.cancel(true);
655         }
656     }
657
658     protected void cancelInitialize() {
659         cancelJob(initializeJob);
660     }
661
662     protected void cancelPairingCheck() {
663         cancelJob(pairingWatchdogJob);
664     }
665
666     protected void cancelRenewEvent() {
667         cancelJob(renewEventJob);
668     }
669
670     private void cancelAllJobs() {
671         cancelInitialize();
672         cancelPairingCheck();
673         cancelRenewEvent();
674     }
675
676     @Override
677     public void dispose() {
678         cancelAllJobs();
679         manager.removeDevice(config.getTerminalID());
680         super.dispose();
681     }
682 }