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.magentatv.internal.handler;
15 import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
16 import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
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;
28 import java.util.TimeZone;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
32 import javax.measure.Unit;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager;
38 import org.openhab.binding.magentatv.internal.MagentaTVException;
39 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEvent;
40 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEventInstanceCreator;
41 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEvent;
42 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEventInstanceCreator;
43 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatus;
44 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatusInstanceCreator;
45 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfo;
46 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfoInstanceCreator;
47 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse;
48 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponse;
49 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentials;
50 import org.openhab.binding.magentatv.internal.config.MagentaTVDynamicConfig;
51 import org.openhab.binding.magentatv.internal.config.MagentaTVThingConfiguration;
52 import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork;
53 import org.openhab.core.config.core.Configuration;
54 import org.openhab.core.library.types.DateTimeType;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.NextPreviousType;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.library.types.PlayPauseType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.library.types.RewindFastforwardType;
61 import org.openhab.core.library.types.StringType;
62 import org.openhab.core.library.unit.Units;
63 import org.openhab.core.thing.ChannelUID;
64 import org.openhab.core.thing.Thing;
65 import org.openhab.core.thing.ThingStatus;
66 import org.openhab.core.thing.ThingStatusDetail;
67 import org.openhab.core.thing.binding.BaseThingHandler;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.UnDefType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
75 import com.google.gson.Gson;
76 import com.google.gson.GsonBuilder;
79 * The {@link MagentaTVHandler} is responsible for handling commands, which are
80 * sent to one of the channels.
82 * @author Markus Michels - Initial contribution
85 public class MagentaTVHandler extends BaseThingHandler implements MagentaTVListener {
86 private final Logger logger = LoggerFactory.getLogger(MagentaTVHandler.class);
88 protected MagentaTVDynamicConfig config = new MagentaTVDynamicConfig();
89 private final Gson gson;
90 protected final MagentaTVNetwork network;
91 protected final MagentaTVDeviceManager manager;
92 private final HttpClient httpClient;
93 protected MagentaTVControl control = new MagentaTVControl();
95 private String thingId = "";
96 private volatile int idRefresh = 0;
97 private @Nullable ScheduledFuture<?> initializeJob;
98 private @Nullable ScheduledFuture<?> pairingWatchdogJob;
99 private @Nullable ScheduledFuture<?> renewEventJob;
102 * Constructor, save bindingConfig (services as default for thingConfig)
105 * @param bindingConfig
107 public MagentaTVHandler(MagentaTVDeviceManager manager, Thing thing, MagentaTVNetwork network,
108 HttpClient httpClient) {
110 this.manager = manager;
111 this.network = network;
112 this.httpClient = httpClient;
113 gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new MRProgramInfoEventInstanceCreator())
114 .registerTypeAdapter(OAuthTokenResponse.class, new MRProgramStatusInstanceCreator())
115 .registerTypeAdapter(OAuthAuthenticateResponse.class, new MRShortProgramInfoInstanceCreator())
116 .registerTypeAdapter(OAuthAuthenticateResponse.class, new MRPayEventInstanceCreator()).create();
120 * Thing initialization:
121 * - initialize thing status from UPnP discovery, thing config, local network settings
122 * - initiate OAuth if userId is not configured and credentials are available
123 * - wait for NotifyServlet to initialize (solves timing issues on fast startup)
126 public void initialize() {
127 // The framework requires you to return from this method quickly. For that the initialization itself is executed
129 String label = getThing().getLabel();
130 thingId = label != null ? label : getThing().getUID().toString();
131 resetEventChannels();
132 updateStatus(ThingStatus.UNKNOWN);
133 config = new MagentaTVDynamicConfig(getConfigAs(MagentaTVThingConfiguration.class));
135 initializeJob = scheduler.schedule(this::initializeThing, 5, TimeUnit.SECONDS);
136 } catch (RuntimeException e) {
137 logger.warn("Unable to schedule thing initialization", e);
141 private void initializeThing() {
142 String errorMessage = "";
144 config.setFriendlyName(getThing().getLabel().toString());
145 if (config.getUDN().isEmpty()) {
146 // get UDN from device name
147 String uid = this.getThing().getUID().getAsString();
148 config.setUDN(substringAfterLast(uid, ":"));
150 if (config.getMacAddress().isEmpty()) {
151 // get MAC address from UDN (last 12 digits)
152 String macAddress = substringAfterLast(config.getUDN(), "_");
153 if (macAddress.isEmpty()) {
154 macAddress = substringAfterLast(config.getUDN(), "-");
156 config.setMacAddress(macAddress);
158 control = new MagentaTVControl(config, network, httpClient);
159 config.updateNetwork(control.getConfig()); // get network parameters from control
161 // Check for emoty credentials (e.g. missing in .things file)
162 String account = config.getAccountName();
163 if (config.getUserId().isEmpty()) {
164 if (account.isEmpty()) {
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
166 "Credentials missing or invalid! Fill credentials into thing configuration or generate UID on the openHAB console - see README");
173 connectReceiver(); // throws MagentaTVException on error
175 // setup background device check
176 renewEventJob = scheduler.scheduleWithFixedDelay(this::renewEventSubscription, 2, 5, TimeUnit.MINUTES);
178 // change to ThingStatus.ONLINE will be done when the pairing result is received
179 // (see onPairingResult())
180 } catch (MagentaTVException e) {
181 errorMessage = e.toString();
182 } catch (RuntimeException e) {
183 logger.warn("{}: Exception on initialization", thingId, e);
185 if (!errorMessage.isEmpty()) {
186 logger.debug("{}: {}", thingId, errorMessage);
187 setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
193 * This routine is called every time the Thing configuration has been changed
196 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
197 logger.debug("{}: Thing config updated, re-initialize", thingId);
199 if (configurationParameters.containsKey(PROPERTY_ACCT_NAME)) {
201 String newAccount = (String) configurationParameters.get(PROPERTY_ACCT_NAME);
202 if ((newAccount != null) && !newAccount.isEmpty()) {
203 // new account info, need to renew userId
204 config.setUserId("");
208 super.handleConfigurationUpdate(configurationParameters);
212 * Handle channel commands
214 * @param channelUID - the channel, which received the command
215 * @param command - the actual command (could be instance of StringType,
216 * DecimalType or OnOffType)
219 public void handleCommand(ChannelUID channelUID, Command command) {
220 if (command == RefreshType.REFRESH) {
221 // currently no channels to be refreshed
226 if (!isOnline() || "PAIR".equalsIgnoreCase(command.toString())) {
227 logger.debug("{}: Receiver {} is offline, try to (re-)connect", thingId, deviceName());
228 connectReceiver(); // reconnect to MR, throws an exception if this fails
231 logger.debug("{}: Channel command for device {}: {} for channel {}", thingId, config.getFriendlyName(),
232 command, channelUID.getId());
233 switch (channelUID.getId()) {
234 case CHANNEL_POWER: // toggle power
235 logger.debug("{}: Toggle power, new state={}", thingId, command);
236 control.sendKey("POWER");
239 logger.debug("{}: Player command: {}", thingId, command);
240 if (command instanceof OnOffType) {
241 control.sendKey("POWER");
242 } else if (command instanceof PlayPauseType) {
243 if (command == PlayPauseType.PLAY) {
244 control.sendKey("PLAY");
245 } else if (command == PlayPauseType.PAUSE) {
246 control.sendKey("PAUSE");
248 } else if (command instanceof NextPreviousType) {
249 if (command == NextPreviousType.NEXT) {
250 control.sendKey("NEXTCH");
251 } else if (command == NextPreviousType.PREVIOUS) {
252 control.sendKey("PREVCH");
254 } else if (command instanceof RewindFastforwardType) {
255 if (command == RewindFastforwardType.FASTFORWARD) {
256 control.sendKey("FORWARD");
257 } else if (command == RewindFastforwardType.REWIND) {
258 control.sendKey("REWIND");
261 logger.debug("{}: Unknown media command: {}", thingId, command);
264 case CHANNEL_CHANNEL:
265 String chan = command.toString();
266 control.selectChannel(chan);
269 if (command == OnOffType.ON) {
270 control.sendKey("MUTE");
272 control.sendKey("VOLUP");
276 if ("PAIR".equalsIgnoreCase(command.toString())) { // special key to re-pair receiver (already done
278 logger.debug("{}: PAIRing key received, reconnect receiver {}", thingId, deviceName());
280 control.sendKey(command.toString());
281 mapKeyToMediateState(command.toString());
285 logger.debug("{}: Command {} for unknown channel {}", thingId, command, channelUID.getAsString());
287 } catch (MagentaTVException e) {
288 String errorMessage = MessageFormat.format("Channel operation failed (command={0}, value={1}): {2}",
289 command, channelUID.getId(), e.getMessage());
290 logger.debug("{}: {}", thingId, errorMessage);
291 setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
295 private void mapKeyToMediateState(String key) {
297 switch (key.toUpperCase()) {
299 state = PlayPauseType.PLAY;
302 state = PlayPauseType.PAUSE;
305 state = RewindFastforwardType.FASTFORWARD;
308 updateState(CHANNEL_PLAYER, RewindFastforwardType.REWIND);
312 logger.debug("{}: Setting Player state to {}", thingId, state);
313 updateState(CHANNEL_PLAYER, state);
318 * Connect to the receiver
320 * @throws MagentaTVException something failed
322 protected void connectReceiver() throws MagentaTVException {
323 if (control.checkDev()) {
324 updateThingProperties();
325 control.setThingId(config.getFriendlyName());
326 manager.registerDevice(config.getUDN(), config.getTerminalID(), config.getIpAddress(), this);
327 control.subscribeEventChannel();
328 control.sendPairingRequest();
330 // check for pairing timeout
331 final int iRefresh = ++idRefresh;
332 pairingWatchdogJob = scheduler.schedule(() -> {
333 if (iRefresh == idRefresh) { // Make a best effort to not run multiple deferred refresh
334 if (config.getVerificationCode().isEmpty()) {
335 setOnlineStatus(ThingStatus.OFFLINE, "Timeout on pairing request!");
338 }, 15, TimeUnit.SECONDS);
343 * If userId is empty and credentials are given the Telekom OAuth service is
344 * used to query the userId
346 * @throws MagentaTVException
348 private void getUserId() throws MagentaTVException {
349 String userId = config.getUserId();
350 if (userId.isEmpty()) {
351 // run OAuth authentication, this finally provides the userId
352 logger.debug("{}: Login with account {}", thingId, config.getAccountName());
353 userId = control.getUserId(config.getAccountName(), config.getAccountPassword());
355 // Update thing configuration (persistent) - remove credentials, add userId
356 Configuration configuration = this.getConfig();
357 configuration.remove(PROPERTY_ACCT_NAME);
358 configuration.remove(PROPERTY_ACCT_PWD);
359 configuration.remove(PROPERTY_USERID);
360 configuration.put(PROPERTY_ACCT_NAME, "");
361 configuration.put(PROPERTY_ACCT_PWD, "");
362 configuration.put(PROPERTY_USERID, userId);
363 this.updateConfiguration(configuration);
364 config.setAccountName("");
365 config.setAccountPassword("");
367 logger.debug("{}: Skip OAuth, use existing userId {}", thingId, config.getUserId());
369 if (!userId.isEmpty()) {
370 config.setUserId(userId);
372 logger.warn("{}: Unable to obtain userId from OAuth", thingId);
377 * Update thing status
379 * @param mode new thing status
380 * @return ON = power on, OFF=power off
382 public void setOnlineStatus(ThingStatus newStatus, String errorMessage) {
383 ThingStatus status = this.getThing().getStatus();
384 if (status != newStatus) {
385 if (newStatus == ThingStatus.ONLINE) {
386 updateStatus(newStatus);
387 updateState(CHANNEL_POWER, OnOffType.ON);
389 if (!errorMessage.isEmpty()) {
390 logger.debug("{}: Communication Error - {}, switch Thing offline", thingId, errorMessage);
391 updateStatus(newStatus, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
393 updateStatus(newStatus);
395 updateState(CHANNEL_POWER, OnOffType.OFF);
401 * A wakeup of the MR was detected (e.g. UPnP received)
403 * @throws MagentaTVException
406 public void onWakeup(Map<String, String> discoveredProperties) throws MagentaTVException {
407 if ((this.getThing().getStatus() == ThingStatus.OFFLINE) || config.getVerificationCode().isEmpty()) {
408 // Device sent a UPnP discovery information, trigger to reconnect
411 logger.debug("{}: Refesh device status for {} (UDN={}", thingId, deviceName(), config.getUDN());
412 setOnlineStatus(ThingStatus.ONLINE, "");
417 * The pairing result has been received. The pairing code will be used to generate the verification code and
418 * complete pairing with the MR. Finally if pairing was completed successful the thing status will change to ONLINE
420 * @param pairingCode pairing code received from MR (NOTIFY event data)
421 * @throws MagentaTVException
424 public void onPairingResult(String pairingCode) throws MagentaTVException {
425 if (control.isInitialized()) {
426 if (control.generateVerificationCode(pairingCode)) {
427 config.setPairingCode(pairingCode);
429 "{}: Pairing code received (UDN {}, terminalID {}, pairingCode={}, verificationCode={}, userId={})",
430 thingId, config.getUDN(), config.getTerminalID(), config.getPairingCode(),
431 config.getVerificationCode(), config.getUserId());
433 // verify pairing completes the pairing process
434 if (control.verifyPairing()) {
435 logger.debug("{}: Pairing completed for device {} ({}), Thing now ONLINE", thingId,
436 config.getFriendlyName(), config.getTerminalID());
437 setOnlineStatus(ThingStatus.ONLINE, "");
438 cancelPairingCheck(); // stop timeout check
441 updateThingProperties(); // persist pairing and verification code
443 logger.debug("{}: control not yet initialized!", thingId);
448 public void onMREvent(String jsonInput) {
449 logger.trace("{}: Process MR event for device {}, json={}", thingId, deviceName(), jsonInput);
450 boolean flUpdatePower = false;
451 String jsonEvent = fixEventJson(jsonInput);
452 if (jsonEvent.contains(MR_EVENT_EIT_CHANGE)) {
453 logger.debug("{}: EVENT_EIT_CHANGE event received.", thingId);
455 MRProgramInfoEvent pinfo = gson.fromJson(jsonEvent, MRProgramInfoEvent.class);
456 if (!pinfo.channelNum.isEmpty()) {
457 logger.debug("{}: EVENT_EIT_CHANGE for channel {}/{}", thingId, pinfo.channelNum, pinfo.channelCode);
458 updateState(CHANNEL_CHANNEL, new DecimalType(pinfo.channelNum));
459 updateState(CHANNEL_CHANNEL_CODE, new DecimalType(pinfo.channelCode));
461 if (pinfo.programInfo != null) {
463 for (MRProgramStatus ps : pinfo.programInfo) {
464 if ((ps.startTime == null) || ps.startTime.isEmpty()) {
465 logger.debug("{}: EVENT_EIT_CHANGE: empty event data = {}", thingId, jsonEvent);
466 continue; // empty program_info
468 updateState(CHANNEL_RUN_STATUS, new StringType(control.getRunStatus(ps.runningStatus)));
470 if (ps.shortEvent != null) {
471 for (MRShortProgramInfo se : ps.shortEvent) {
472 if ((ps.startTime == null) || ps.startTime.isEmpty()) {
473 logger.debug("{}: EVENT_EIT_CHANGE: empty program info", thingId);
476 // Convert UTC to local time
477 // 2018/11/04 21:45:00 -> "2018-11-04T10:15:30.00Z"
478 String tsLocal = ps.startTime.replace('/', '-').replace(" ", "T") + "Z";
479 Instant timestamp = Instant.parse(tsLocal);
480 ZonedDateTime localTime = timestamp.atZone(ZoneId.of("Europe/Berlin"));
481 tsLocal = substringBeforeLast(localTime.toString(), "[");
482 tsLocal = substringBefore(tsLocal.replace('-', '/').replace('T', ' '), "+");
484 logger.debug("{}: Info for channel {} / {} - {} {}.{}, start time={}, duration={}", thingId,
485 pinfo.channelNum, pinfo.channelCode, control.getRunStatus(ps.runningStatus),
486 se.eventName, se.textChar, tsLocal, ps.duration);
487 if (ps.runningStatus != EV_EITCHG_RUNNING_NOT_RUNNING) {
488 updateState(CHANNEL_PROG_TITLE, new StringType(se.eventName));
489 updateState(CHANNEL_PROG_TEXT, new StringType(se.textChar));
490 updateState(CHANNEL_PROG_START, new DateTimeType(localTime));
493 DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
494 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
495 Date date = dateFormat.parse(ps.duration);
496 long minutes = date.getTime() / 1000L / 60l;
497 updateState(CHANNEL_PROG_DURATION, toQuantityType(minutes, Units.MINUTE));
498 } catch (ParseException e) {
499 logger.debug("{}: Unable to parse programDuration: {}", thingId, ps.duration);
503 flUpdatePower = true;
510 } else if (jsonEvent.contains("new_play_mode")) {
511 MRPayEvent event = gson.fromJson(jsonEvent, MRPayEvent.class);
512 if (event.duration == null) {
515 if (event.playPostion == null) {
516 event.playPostion = -1;
518 logger.debug("{}: STB event playContent: playMode={}, duration={}, playPosition={}", thingId,
519 control.getPlayStatus(event.newPlayMode), event.duration, event.playPostion);
521 // If we get a playConfig event there MR must be online. However it also sends a
522 // plyMode stop before powering off the device, so we filter this.
523 if ((event.newPlayMode != EV_PLAYCHG_STOP) && this.isInitialized()) {
524 flUpdatePower = true;
526 if (event.newPlayMode != -1) {
527 String playMode = control.getPlayStatus(event.newPlayMode);
528 updateState(CHANNEL_PLAY_MODE, new StringType(playMode));
529 mapPlayModeToMediaControl(playMode);
531 if (event.duration > 0) {
532 updateState(CHANNEL_PROG_DURATION, new StringType(event.duration.toString()));
534 if (event.playPostion != -1) {
535 updateState(CHANNEL_PROG_POS, toQuantityType(event.playPostion / 6, Units.MINUTE));
538 logger.debug("{}: Unknown MR event, JSON={}", thingId, jsonEvent);
541 // We received a non-stopped event -> MR must be on
542 updateState(CHANNEL_POWER, OnOffType.ON);
546 private void mapPlayModeToMediaControl(String playMode) {
552 logger.debug("{}: Setting Player state to PLAY", thingId);
553 updateState(CHANNEL_PLAYER, PlayPauseType.PLAY);
557 logger.debug("{}: Setting Player state to PAUSE", thingId);
558 updateState(CHANNEL_PLAYER, PlayPauseType.PAUSE);
564 * When the MR powers off it send a UPnP message, which is catched by the binding.
567 public void onPowerOff() throws MagentaTVException {
568 logger.debug("{}: Power-Off received for device {}", thingId, deviceName());
569 // MR was powered off -> update power status, reset items
570 resetEventChannels();
573 private void resetEventChannels() {
574 updateState(CHANNEL_POWER, OnOffType.OFF);
575 updateState(CHANNEL_PROG_TITLE, StringType.EMPTY);
576 updateState(CHANNEL_PROG_TEXT, StringType.EMPTY);
577 updateState(CHANNEL_PROG_START, StringType.EMPTY);
578 updateState(CHANNEL_PROG_DURATION, DecimalType.ZERO);
579 updateState(CHANNEL_PROG_POS, DecimalType.ZERO);
580 updateState(CHANNEL_CHANNEL, DecimalType.ZERO);
581 updateState(CHANNEL_CHANNEL_CODE, DecimalType.ZERO);
584 private String fixEventJson(String jsonEvent) {
585 // MR401: channel_num is a string -> ok
586 // MR201: channel_num is an int -> fix JSON formatting to String
587 if (jsonEvent.contains(MR_EVENT_CHAN_TAG) && !jsonEvent.contains(MR_EVENT_CHAN_TAG + "\"")) {
588 // hack: reformat the JSON string to make it compatible with the GSON parsing
589 logger.trace("{}: malformed JSON->fix channel_num", thingId);
590 String start = substringBefore(jsonEvent, MR_EVENT_CHAN_TAG); // up to "channel_num":
591 String end = substringAfter(jsonEvent, MR_EVENT_CHAN_TAG); // behind "channel_num":
592 String chan = substringBetween(jsonEvent, MR_EVENT_CHAN_TAG, ",").trim();
593 return start + "\"channel_num\":" + "\"" + chan + "\"" + end;
598 private boolean isOnline() {
599 return this.getThing().getStatus() == ThingStatus.ONLINE;
603 * Renew the event subscription. The periodic refresh is required, otherwise the receive will stop sending events.
604 * Reconnect if nessesary.
606 private void renewEventSubscription() {
607 if (!control.isInitialized()) {
610 logger.debug("{}: Check receiver status, current state {}/{}", thingId,
611 this.getThing().getStatusInfo().getStatus(), this.getThing().getStatusInfo().getStatusDetail());
614 // when pairing is completed re-new event channel subscription
615 if ((this.getThing().getStatus() != ThingStatus.OFFLINE) && !config.getVerificationCode().isEmpty()) {
616 logger.debug("{}: Renew MR event subscription for device {}", thingId, deviceName());
617 control.subscribeEventChannel();
619 } catch (MagentaTVException e) {
620 logger.warn("{}: Re-new event subscription failed: {}", deviceName(), e.toString());
623 // another try: if the above SUBSCRIBE fails, try a re-connect immediatly
625 if ((this.getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR)
626 && !config.getUserId().isEmpty()) {
627 // if we have no userId the OAuth is not completed or pairing process got stuck
628 logger.debug("{}: Reconnect media receiver", deviceName());
629 connectReceiver(); // throws MagentaTVException on error
631 } catch (MagentaTVException | RuntimeException e) {
632 logger.debug("{}: Re-connect to receiver failed: {}", deviceName(), e.toString());
636 public void updateThingProperties() {
637 Map<String, String> properties = new HashMap<String, String>();
638 properties.put(PROPERTY_FRIENDLYNAME, config.getFriendlyName());
639 properties.put(PROPERTY_MODEL_NUMBER, config.getModel());
640 properties.put(PROPERTY_DESC_URL, config.getDescriptionUrl());
641 properties.put(PROPERTY_PAIRINGCODE, config.getPairingCode());
642 properties.put(PROPERTY_VERIFICATIONCODE, config.getVerificationCode());
643 properties.put(PROPERTY_LOCAL_IP, config.getLocalIP());
644 properties.put(PROPERTY_TERMINALID, config.getLocalIP());
645 properties.put(PROPERTY_LOCAL_MAC, config.getLocalMAC());
646 properties.put(PROPERTY_WAKEONLAN, config.getWakeOnLAN());
647 updateProperties(properties);
650 public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
651 return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
654 private String deviceName() {
655 return config.getFriendlyName() + "(" + config.getTerminalID() + ")";
658 private void cancelJob(@Nullable ScheduledFuture<?> job) {
659 if ((job != null) && !job.isCancelled()) {
664 protected void cancelInitialize() {
665 cancelJob(initializeJob);
668 protected void cancelPairingCheck() {
669 cancelJob(pairingWatchdogJob);
672 protected void cancelRenewEvent() {
673 cancelJob(renewEventJob);
676 private void cancelAllJobs() {
678 cancelPairingCheck();
683 public void dispose() {
685 manager.removeDevice(config.getTerminalID());