Improve discovery (#16692)

Resolves #16690

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-04-30 00:39:30 +02:00 committed by Ciprian Pascu
parent 2479529eab
commit 5645dfb73b
4 changed files with 166 additions and 55 deletions

View File

@ -4,6 +4,7 @@
<feature name="openhab-binding-denonmarantz" description="Denon / Marantz Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.denonmarantz/${project.version}</bundle>
</feature>
</features>

View File

@ -30,6 +30,9 @@ public class DenonMarantzBindingConstants {
public static final String BINDING_ID = "denonmarantz";
public static final String VENDOR_DENON = "Denon";
public static final String VENDOR_MARANTZ = "Marantz";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AVR = new ThingTypeUID(BINDING_ID, "avr");

View File

@ -22,6 +22,8 @@ import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
@ -33,13 +35,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Jan-Willem Veldhuis - Initial contribution
* The {@link DenonMarantzMDNSDiscoveryParticipant} is responsible for discovering Denon/Marantz AV Receivers.
* It uses the central {@link org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant}.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
@Component
public class DenonMarantzDiscoveryParticipant implements MDNSDiscoveryParticipant {
@Component(configurationPid = "discovery.denonmarantz")
@NonNullByDefault
public class DenonMarantzMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private Logger logger = LoggerFactory.getLogger(DenonMarantzDiscoveryParticipant.class);
private Logger logger = LoggerFactory.getLogger(DenonMarantzMDNSDiscoveryParticipant.class);
// Service type for 'Airplay enabled' receivers
private static final String RAOP_SERVICE_TYPE = "_raop._tcp.local.";
@ -47,21 +52,15 @@ public class DenonMarantzDiscoveryParticipant implements MDNSDiscoveryParticipan
/**
* Match the serial number, vendor and model of the discovered AVR.
* Input is like "0006781D58B1@Marantz SR5008._raop._tcp.local."
* A Denon AVR serial (MAC address) starts with 0005CD
* A Marantz AVR serial (MAC address) starts with 000678
* Older Denon AVR's serial (MAC address) starts with 0005CD
* Older Marantz AVR's serial (MAC address) starts with 000678
* Newer Denon AVR's can start with 000678 as well.
*/
private static final Pattern DENON_MARANTZ_PATTERN = Pattern
.compile("^((?:0005CD|000678)[A-Z0-9]+)@(.+)\\._raop\\._tcp\\.local\\.$");
/**
* Denon AVRs have a MAC address / serial number starting with 0005CD
*/
private static final String DENON_MAC_PREFIX = "0005CD";
/**
* Marantz AVRs have a MAC address / serial number starting with 000678
*/
private static final String MARANTZ_MAC_PREFIX = "000678";
private static final String DENON_MODEL_PREFIX = "denon";
private static final String MARANTZ_MODEL_PREFIX = "marantz";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
@ -74,55 +73,58 @@ public class DenonMarantzDiscoveryParticipant implements MDNSDiscoveryParticipan
}
@Override
@Nullable
public DiscoveryResult createResult(ServiceInfo serviceInfo) {
String qualifiedName = serviceInfo.getQualifiedName();
logger.debug("AVR found: {}", qualifiedName);
ThingUID thingUID = getThingUID(serviceInfo);
if (thingUID != null) {
Matcher matcher = DENON_MARANTZ_PATTERN.matcher(qualifiedName);
matcher.matches(); // we already know it matches, it was matched in getThingUID
String serial = matcher.group(1).toLowerCase();
/**
* The Vendor is not available from the mDNS result.
* We assign the Vendor based on our assumptions of the MAC address prefix.
*/
String vendor = "";
if (serial.startsWith(MARANTZ_MAC_PREFIX)) {
vendor = "Marantz";
} else if (serial.startsWith(DENON_MAC_PREFIX)) {
vendor = "Denon";
}
// 'am=...' property describes the model name
String model = serviceInfo.getPropertyString("am");
String friendlyName = matcher.group(2).trim();
Map<String, Object> properties = new HashMap<>(2);
if (serviceInfo.getHostAddresses().length == 0) {
logger.debug("Could not determine IP address for the Denon/Marantz AVR");
return null;
}
String host = serviceInfo.getHostAddresses()[0];
logger.debug("IP Address: {}", host);
properties.put(PARAMETER_HOST, host);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serial);
properties.put(Thing.PROPERTY_VENDOR, vendor);
properties.put(Thing.PROPERTY_MODEL_ID, model);
String label = friendlyName + " (" + vendor + ' ' + model + ")";
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(label)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
} else {
if (thingUID == null) {
return null;
}
Matcher matcher = DENON_MARANTZ_PATTERN.matcher(qualifiedName);
matcher.matches(); // we already know it matches, it was matched in getThingUID
String serial = matcher.group(1).toLowerCase();
// 'am=...' property describes the model name
String model = serviceInfo.getPropertyString("am");
String friendlyName = matcher.group(2).trim();
String vendor;
String modelLowerCase = model == null ? "" : model.toLowerCase();
if (modelLowerCase.startsWith(DENON_MODEL_PREFIX)) {
vendor = VENDOR_DENON;
} else if (modelLowerCase.startsWith(MARANTZ_MODEL_PREFIX)) {
vendor = VENDOR_MARANTZ;
} else {
vendor = null;
}
Map<String, Object> properties = new HashMap<>(4);
if (serviceInfo.getHostAddresses().length == 0) {
logger.debug("Could not determine IP address for the Denon/Marantz AVR");
return null;
}
String host = serviceInfo.getHostAddresses()[0];
logger.debug("IP Address: {}", host);
properties.put(PARAMETER_HOST, host);
if (vendor != null) {
properties.put(Thing.PROPERTY_VENDOR, vendor);
}
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serial);
if (model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, model);
}
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(friendlyName)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
}
@Override
@Nullable
public ThingUID getThingUID(ServiceInfo service) {
Matcher matcher = DENON_MARANTZ_PATTERN.matcher(service.getQualifiedName());
if (matcher.matches()) {

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.discovery;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.*;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DenonMarantzUpnpDiscoveryParticipant} is responsible for discovering Denon AV Receivers.
* It uses the central {@link org.openhab.core.config.discovery.upnp.internal.UpnpDiscoveryService}.
*
* @author Jacob Laursen - Initial contribution
*/
@Component(configurationPid = "discovery.denonmarantz")
@NonNullByDefault
public class DenonMarantzUpnpDiscoveryParticipant implements UpnpDiscoveryParticipant {
private Logger logger = LoggerFactory.getLogger(DenonMarantzUpnpDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Set.of(THING_TYPE_AVR);
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
DeviceDetails details = device.getDetails();
if (!VENDOR_DENON.equalsIgnoreCase(details.getManufacturerDetails().getManufacturer())) {
return null;
}
URL baseUrl = details.getBaseURL();
if (baseUrl == null) {
logger.debug("Discovered {}, but base URL is missing", device.getDisplayString());
return null;
}
String serialNumber = details.getSerialNumber();
if (serialNumber == null) {
logger.debug("Discovered {}, but serial number is missing", device.getDisplayString());
return null;
}
ThingUID thingUID = getThingUID(device);
if (thingUID == null) {
return null;
}
String host = baseUrl.getHost();
String model = details.getModelDetails().getModelName();
logger.debug("Discovered {}", device.getDisplayString());
Map<String, Object> properties = new HashMap<>(4);
properties.put(PARAMETER_HOST, host);
properties.put(Thing.PROPERTY_VENDOR, VENDOR_DENON);
properties.put(Thing.PROPERTY_MODEL_ID, model);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber.toLowerCase());
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(VENDOR_DENON + " " + model)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
DeviceDetails details = device.getDetails();
if (!VENDOR_DENON.equalsIgnoreCase(details.getManufacturerDetails().getManufacturer())) {
return null;
}
String serialNumber = details.getSerialNumber();
if (serialNumber == null) {
return null;
}
return new ThingUID(THING_TYPE_AVR, serialNumber.toLowerCase());
}
}