Skip to content

Commit

Permalink
Add UnknownDevice as the default deserialization strategy for unspeci…
Browse files Browse the repository at this point in the history
…fied device subtypes.
  • Loading branch information
dvdgeisler committed Nov 28, 2022
1 parent bfac19c commit 2a9c1ed
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
import de.dvdgeisler.iot.dirigera.client.api.model.device.repeater.RepeaterDevice;
import de.dvdgeisler.iot.dirigera.client.api.model.device.shortcutcontroller.ShortcutControllerDevice;
import de.dvdgeisler.iot.dirigera.client.api.model.device.soundcontroller.SoundControllerDevice;
import de.dvdgeisler.iot.dirigera.client.api.model.device.unknown.UnknownDevice;

import java.time.LocalDateTime;
import java.util.List;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "deviceType", visible = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "deviceType",
visible = true,
defaultImpl = UnknownDevice.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = GatewayDevice.class, name = "gateway"),
@JsonSubTypes.Type(value = LightDevice.class, name = "light"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.dvdgeisler.iot.dirigera.client.api.model.device.unknown;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.dvdgeisler.iot.dirigera.client.api.model.device.*;

import java.time.LocalDateTime;
import java.util.List;

/**
* The {@link UnknownDevice} represents a minimal subtype of a device. An {@link UnknownDevice} is provided as the
* default deserialization strategy in case Jackson reads a json blob whose subtype is not derivable by its
* {@link Device#deviceType} entry. A corresponding record is created via the {@link UnknownDeviceDeserializer} in the
* {@link UnknownDeviceCollector}.
*/
@JsonDeserialize(using = UnknownDeviceDeserializer.class)
public class UnknownDevice extends Device<
DeviceAttributes<DeviceStateAttributes>,
DeviceConfigurationAttributes> {

public UnknownDevice(
final String id,
final DeviceCategory type,
final DeviceType deviceType,
final LocalDateTime createdAt,
final Boolean isReachable,
final LocalDateTime lastSeen,
final DeviceAttributes<DeviceStateAttributes> attributes,
final DeviceCapabilities capabilities,
final List<String> remoteLinks,
final DeviceConfigurationAttributes deviceConfigurationAttributes) {
super(id, type, deviceType, createdAt, isReachable, lastSeen, attributes, capabilities, remoteLinks, deviceConfigurationAttributes);
}

public UnknownDevice() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package de.dvdgeisler.iot.dirigera.client.api.model.device.unknown;

import com.fasterxml.jackson.databind.JsonNode;
import de.dvdgeisler.iot.dirigera.client.api.model.device.DeviceCategory;
import de.dvdgeisler.iot.dirigera.client.api.model.device.DeviceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* Collects records of deserialized devices for which no specialized deserialization strategy is defined. If a device
* class is recorded as unknown for the first time, a corresponding warning is logged, including additional information
* about it's JSON format.
*/
public class UnknownDeviceCollector {
private final static Logger log = LoggerFactory.getLogger(UnknownDeviceCollector.class);

public static final UnknownDeviceCollector instance = new UnknownDeviceCollector();

public static class UnknownDeviceEntry {
public final UnknownDevice device;
public final JsonNode jsonNode;

public UnknownDeviceEntry(final UnknownDevice device, final JsonNode jsonNode) {
this.device = device;
this.jsonNode = jsonNode;
}

@Override
public boolean equals(final Object o) {
final UnknownDeviceEntry that;

if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
that = (UnknownDeviceEntry) o;
return Objects.equals(getDeviceCategory(this.device), getDeviceCategory(that.device)) &&
Objects.equals(getDeviceType(this.device), getDeviceType(that.device)) &&
Objects.equals(getDeviceManufacturer(this.device), getDeviceManufacturer(that.device)) &&
Objects.equals(getDeviceModel(this.device), getDeviceModel(that.device)) &&
Objects.equals(getDeviceCustomName(this.device), getDeviceCustomName(that.device));
}

@Override
public int hashCode() {
return Objects.hash(
getDeviceCategory(this.device),
getDeviceType(this.device),
getDeviceManufacturer(this.device),
getDeviceModel(this.device),
getDeviceCustomName(this.device));
}
}

private final Set<UnknownDeviceEntry> entries;

private UnknownDeviceCollector() {
this.entries = new HashSet<>();
}

private boolean add(final UnknownDeviceEntry unknownDeviceEntry) {
if (this.entries.add(unknownDeviceEntry)) {
log.warn("""
Unknown device found:
type={}, category={},
manufacturer={}, model={},
customName={}, id={}
Help us to support additional devices and create an issue on
{} with the following content:
{}
""",
getDeviceCategory(unknownDeviceEntry.device),
getDeviceType(unknownDeviceEntry.device),
getDeviceManufacturer(unknownDeviceEntry.device),
getDeviceModel(unknownDeviceEntry.device),
getDeviceCustomName(unknownDeviceEntry.device),
getDeviceId(unknownDeviceEntry.device),
"https://github.com/dvdgeisler/DirigeraClient",
unknownDeviceEntry.jsonNode);
return true;
}
return false;
}

public boolean add(final UnknownDevice unknownDevice, final JsonNode jsonNode) {
return this.add(new UnknownDeviceEntry(unknownDevice, jsonNode));
}

private static DeviceCategory getDeviceCategory(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.type).orElse(null);
}

private static DeviceType getDeviceType(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.deviceType).orElse(null);
}

private static String getDeviceManufacturer(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.attributes).map(a -> a.manufacturer).orElse(null);
}

private static String getDeviceModel(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.attributes).map(a -> a.model).orElse(null);
}

private static String getDeviceCustomName(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.attributes).map(a -> a.state).map(s -> s.customName).orElse(null);
}

private static String getDeviceId(final UnknownDevice device) {
return Optional.ofNullable(device).map(d -> d.id).orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.dvdgeisler.iot.dirigera.client.api.model.device.unknown;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;

/**
* Deserializes an unknown device into an {@link UnknownDevice} instance. Furthermore, a corresponding record is created
* in the {@link UnknownDeviceCollector} for analytical purposes.
*/
public class UnknownDeviceDeserializer extends StdDeserializer<UnknownDevice> {

/**
* Extends an {@link UnknownDevice} to overwrite its Jackson deserialization strategy
*/
@JsonDeserialize(using = JsonDeserializer.None.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private static class UnknownDeviceWrapper extends UnknownDevice {
}

protected UnknownDeviceDeserializer() {
super(UnknownDevice.class);
}

@Override
public UnknownDevice deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException {
final JsonNode jsonNode;
final UnknownDeviceWrapper defaultDevice;

jsonNode = ctxt.readTree(p);
defaultDevice = ctxt.readTreeAsValue(jsonNode, UnknownDeviceWrapper.class);
UnknownDeviceCollector.instance.add(defaultDevice, jsonNode);

return defaultDevice;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.dvdgeisler.iot.dirigera.client.api.model.device.unknown;

import de.dvdgeisler.iot.dirigera.client.api.model.device.Device;
import de.dvdgeisler.iot.dirigera.client.api.model.device.DeviceTest;

import static org.junit.jupiter.api.Assertions.assertTrue;

class UnknownDeviceTest extends DeviceTest {
final static String JSON = "{\"id\" : \"123\"}";

public UnknownDeviceTest() {
super(JSON);
}

@Override
public void validateDeserialize(final Device<?,?> device) {
assertTrue(device instanceof UnknownDevice);
}
}

0 comments on commit 2a9c1ed

Please sign in to comment.