From: Daniel Demus Date: Thu, 1 Dec 2022 21:47:11 +0000 (+0100) Subject: [wundergroundupdatereceiver] Bugfixes: Regenerate trigger channel with proper type... X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=43d01ad49c483db015705f606e63b3d3e8876d9e;p=openhab-addons.git [wundergroundupdatereceiver] Bugfixes: Regenerate trigger channel with proper type and more metadata. Normalize channel names. It might be easiest to delete and allow recreation of channels. (#13327) * [wundergroundupdatereceiver] LAST_QUERY parameter should not be mapped automatically * [wundergroundupdatereceiver] All channeltype props need to be applied Especially the channel kind * [wundergroundupdatereceiver] Remove illegal characters from channel name Additionally expand the channel naming test to assert the generated channelUID and test that _ in names isn't inadvertently replaced * [wundergroundupdatereceiver] Don't default AutoUpdatePolicy on creation * [wundergroundupdatereceiver] Migrate changed channel to trigger type Signed-off-by: Daniel Demus --- diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/README.md b/bundles/org.openhab.binding.wundergroundupdatereceiver/README.md index a8c5ebe04c..891b65977e 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/README.md +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/README.md @@ -6,7 +6,7 @@ This binding enables acting as a receiver of updates from devices that post meas If the hostname is configurable - as on weather stations based on the Fine Offset Electronics WH2600-IP - this is simple, otherwise you have to set up dns such that it resolves the above hostname to your server, without preventing the server from resolving the proper ip if you want to forward the request. The server thus listens at http(s)://:/weatherstation/updateweatherstation.php and the device needs to be pointed at this address. -If you can't configure the device itself to submit to an alternate hostname you would need to set up a dns server that resolves rtupdate.wunderground.com to the IP-address of your server and provide as dns to the device does DHCP. +If you can't configure the device itself to submit to an alternate hostname you would need to set up a dns server that resolves rtupdate.wunderground.com to the IP-address of your server and provide it as the DHCP dns-server to the device. Make sure not to use this dns server instance for any other DHCP clients. The request is in itself simple to parse, so by redirecting it to your openHAB server you can intercept the values and use them to control items in your home. @@ -19,6 +19,7 @@ It can also be used to submit the same measurements to multiple weather services ## Supported Things Any device that sends weather measurement updates to the wunderground.com update URLs is supported. +Multiple devices submitting to the same wunderground account ID can be aggregated. It is easiest to use with devices that have a configurable target address, but can be made to work with any internet-connected device, that gets its dns server via DHCP or where the DNS server can be set. ## Discovery @@ -42,7 +43,9 @@ If you don't plan on submitting measurements to wunderground.com, it can be any Each measurement type the wunderground.com update service accepts has a channel. The channels must be named exactly as the request parameter they receive. I.e. the wind speed channel must be named `windspeedmph` as that is the request parameter name defined by Wunderground in their API. -The channel name set up in the binding should be considered an id with no semantic content other than pointing to the wounderground API. +Illegal channel id characters are converted to -. +For example, AqPM2.5 has a channel named `AqPM2-5`. +The channel name set up in the binding should be considered an id with no semantic content other than pointing to the wunderground API. Additionally there is a receipt timestamp and a trigger channel. ### Request parameters are mapped to one of the following channel-types: diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverBindingConstants.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverBindingConstants.java index 2dd0e685b1..b0ef872986 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverBindingConstants.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverBindingConstants.java @@ -49,6 +49,8 @@ public class WundergroundUpdateReceiverBindingConstants { public static final String NOW = "now"; + public static final String UNCATEGORIZED = "Uncategorized"; + // Excluded technical paramter names public static final String REALTIME_MARKER = "realtime"; public static final String PASSWORD = "PASSWORD"; @@ -56,8 +58,8 @@ public class WundergroundUpdateReceiverBindingConstants { // List of default synthetic channeltypes added to a new thing public static final String DATEUTC_DATETIME = "dateutc-datetime"; - public static final String LAST_RECEIVED_DATETIME = "last-received-datetime"; public static final String LAST_RECEIVED = "last-received"; + public static final String LAST_RECEIVED_DATETIME = LAST_RECEIVED + "-datetime"; public static final String LAST_QUERY = "last-query"; public static final String LAST_QUERY_STATE = LAST_QUERY + "-state"; public static final String LAST_QUERY_TRIGGER = LAST_QUERY + "-trigger"; @@ -72,7 +74,7 @@ public class WundergroundUpdateReceiverBindingConstants { public static final String PRESSURE_GROUP = "pressure"; public static final String POLLUTION_GROUP = "pollution"; - // Known or observed request paramters received from devices submitting to wunderground.com + // Known or observed request parameters received from devices submitting to wunderground.com public static final String DATEUTC = "dateutc"; public static final String SOFTWARE_TYPE = "softwaretype"; public static final String LOW_BATTERY = "lowbatt"; @@ -125,7 +127,7 @@ public class WundergroundUpdateReceiverBindingConstants { public static final String AQ_OC = "AqOC"; public static final String AQ_BC = "AqBC"; public static final String AQ_UV_AETH = "AqUV-AETH"; - public static final String AQ_PM2_5 = "AqPM2.5"; + public static final String AQ_PM2_5 = "AqPM2-5"; public static final String AQ_PM10 = "AqPM10"; public static final String AQ_OZONE = "AqOZONE"; diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryService.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryService.java index 8300bb71b5..c93cfae1a4 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryService.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryService.java @@ -38,7 +38,7 @@ public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscover WundergroundUpdateReceiverServletControls servletControls; private static final int TIMEOUT_SEC = 1; - private final HashMap> thinglessStationIds = new HashMap<>(); + private final HashMap> thinglessStationIds = new HashMap<>(); private boolean servletWasInactive = false; private boolean scanning = false; @@ -57,7 +57,7 @@ public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscover thinglessStationIds.remove(stationId); } - public void addUnhandledStationId(@Nullable String stationId, Map request) { + public void addUnhandledStationId(@Nullable String stationId, Map request) { if (stationId == null || stationId.isEmpty()) { return; } @@ -73,7 +73,7 @@ public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscover return isBackgroundDiscoveryEnabled() || isScanning(); } - public @Nullable Map getUnhandledStationRequest(@Nullable String stationId) { + public @Nullable Map getUnhandledStationRequest(@Nullable String stationId) { return this.thinglessStationIds.get(stationId); } diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverHandler.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverHandler.java index 09fe7e0baa..e0254638a4 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverHandler.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverHandler.java @@ -40,6 +40,7 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.types.Command; @@ -110,16 +111,15 @@ public class WundergroundUpdateReceiverHandler extends BaseThingHandler { this.config = getConfigAs(WundergroundUpdateReceiverConfiguration.class); wundergroundUpdateReceiverServlet.addHandler(this); @Nullable - Map requestParameters = discoveryService.getUnhandledStationRequest(config.stationId); + Map requestParameters = discoveryService.getUnhandledStationRequest(config.stationId); if (requestParameters != null && thing.getChannels().isEmpty()) { - final String[] noValues = new String[0]; ThingBuilder thingBuilder = editThing(); List.of(LAST_RECEIVED, LAST_QUERY_TRIGGER, DATEUTC_DATETIME, LAST_QUERY_STATE) - .forEach((String channelId) -> buildChannel(thingBuilder, channelId, noValues)); - requestParameters - .forEach((String parameter, String[] query) -> buildChannel(thingBuilder, parameter, query)); + .forEach((String channelId) -> buildChannel(thingBuilder, channelId, "")); + requestParameters.forEach((String parameter, String query) -> buildChannel(thingBuilder, parameter, query)); updateThing(thingBuilder.build()); } + migrateChannels(); discoveryService.removeUnhandledStationId(config.stationId); if (wundergroundUpdateReceiverServlet.isActive()) { updateStatus(ThingStatus.ONLINE); @@ -130,6 +130,17 @@ public class WundergroundUpdateReceiverHandler extends BaseThingHandler { wundergroundUpdateReceiverServlet.getErrorDetail()); } + private void migrateChannels() { + Optional.ofNullable(getThing().getChannel(queryTriggerChannel)).ifPresent(c -> { + if (c.getKind() != ChannelKind.TRIGGER) { + ThingBuilder builder = editThing(); + builder.withoutChannel(c.getUID()); + buildChannel(builder, LAST_QUERY_TRIGGER, ""); + updateThing(builder.build()); + } + }); + } + @Override public void dispose() { wundergroundUpdateReceiverServlet.removeHandler(this.getStationId()); @@ -149,10 +160,10 @@ public class WundergroundUpdateReceiverHandler extends BaseThingHandler { triggerChannel(queryTriggerChannel, lastQuery); } - private void buildChannel(ThingBuilder thingBuilder, String parameter, String... query) { + private void buildChannel(ThingBuilder thingBuilder, String parameter, String value) { @Nullable WundergroundUpdateReceiverParameterMapping channelTypeMapping = WundergroundUpdateReceiverParameterMapping - .getOrCreateMapping(parameter, String.join("", query), channelTypeProvider); + .getOrCreateMapping(parameter, value, channelTypeProvider); if (channelTypeMapping == null) { return; } @@ -162,7 +173,10 @@ public class WundergroundUpdateReceiverHandler extends BaseThingHandler { } ChannelBuilder channelBuilder = ChannelBuilder .create(new ChannelUID(thing.getUID(), channelTypeMapping.channelGroup, parameter)) - .withType(channelTypeMapping.channelTypeId).withAcceptedItemType(channelType.getItemType()); + .withKind(channelType.getKind()).withAutoUpdatePolicy(channelType.getAutoUpdatePolicy()) + .withDefaultTags(channelType.getTags()).withType(channelTypeMapping.channelTypeId) + .withAcceptedItemType(channelType.getItemType()).withLabel(channelType.getLabel()); + Optional.ofNullable(channelType.getDescription()).ifPresent(channelBuilder::withDescription); thingBuilder.withChannel(channelBuilder.build()); } diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverParameterMapping.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverParameterMapping.java index 09f43608bd..d55c3fcf3e 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverParameterMapping.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverParameterMapping.java @@ -54,7 +54,7 @@ public class WundergroundUpdateReceiverParameterMapping { } private static final List UNMAPPED_PARAMETERS = List.of(STATION_ID_PARAMETER, PASSWORD, ACTION, - REALTIME_MARKER); + REALTIME_MARKER, LAST_QUERY); private static final WundergroundUpdateReceiverParameterMapping[] KNOWN_MAPPINGS = { new WundergroundUpdateReceiverParameterMapping(LAST_RECEIVED, LAST_RECEIVED_DATETIME_CHANNELTYPEUID, @@ -173,7 +173,7 @@ public class WundergroundUpdateReceiverParameterMapping { } Optional knownMapping = lookupMapping(parameterName); return knownMapping.orElseGet(() -> new WundergroundUpdateReceiverParameterMapping(parameterName, - channelTypeProvider.getOrCreateChannelType(parameterName, value).getUID(), "Uncategorized", null, false, + channelTypeProvider.getOrCreateChannelType(parameterName, value).getUID(), UNCATEGORIZED, null, false, null)); } diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverServlet.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverServlet.java index 005b280967..cdba261011 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverServlet.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverServlet.java @@ -27,6 +27,7 @@ import java.util.Hashtable; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -53,6 +54,7 @@ public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet public static final String SERVLET_URL = "/weatherstation/updateweatherstation.php"; private static final long serialVersionUID = -5296703727081438023L; + private static final Pattern CLEANER = Pattern.compile("[^\\w-]"); private final Logger logger = LoggerFactory.getLogger(WundergroundUpdateReceiverServlet.class); private final Map handlers = new HashMap<>(); @@ -173,6 +175,11 @@ public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet } } + protected Map normalizeParameterMap(Map parameterMap) { + return parameterMap.entrySet().stream() + .collect(toMap(e -> makeUidSafeString(e.getKey()), e -> String.join("", e.getValue()))); + } + @Override protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException { if (!active) { @@ -190,16 +197,15 @@ public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet logger.trace("doGet {}", req.getQueryString()); String stationId = req.getParameter(STATION_ID_PARAMETER); + Map states = normalizeParameterMap(req.getParameterMap()); Optional.ofNullable(this.handlers.get(stationId)).ifPresentOrElse(handler -> { - Map states = req.getParameterMap().entrySet().stream() - .collect(toMap(Map.Entry::getKey, e -> String.join("", e.getValue()))); String queryString = req.getQueryString(); if (queryString != null && queryString.length() > 0) { states.put(LAST_QUERY, queryString); } handler.updateChannelStates(states); }, () -> { - this.discoveryService.addUnhandledStationId(stationId, req.getParameterMap()); + this.discoveryService.addUnhandledStationId(stationId, states); }); resp.setStatus(HttpServletResponse.SC_OK); @@ -216,4 +222,8 @@ public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet protected Map getHandlers() { return Collections.unmodifiableMap(this.handlers); } + + private String makeUidSafeString(String key) { + return CLEANER.matcher(key).replaceAll("-"); + } } diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverUnknownChannelTypeProvider.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverUnknownChannelTypeProvider.java index 6c24f89611..47f4c8343a 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverUnknownChannelTypeProvider.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverUnknownChannelTypeProvider.java @@ -13,6 +13,7 @@ package org.openhab.binding.wundergroundupdatereceiver.internal; import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER; +import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.UNCATEGORIZED; import java.util.Collection; import java.util.List; @@ -22,6 +23,8 @@ import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeBuilder; import org.openhab.core.thing.type.ChannelTypeProvider; @@ -37,7 +40,7 @@ import org.slf4j.LoggerFactory; @NonNullByDefault public class WundergroundUpdateReceiverUnknownChannelTypeProvider implements ChannelTypeProvider { - private static final List BOOLEAN_STRINGS = List.of("1", "0", "true", "false"); + private static final List BOOLEAN_STRINGS = List.of("1", "0", "true", "false", "yes", "no", "on", "off"); private final Map channelTypes = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(WundergroundUpdateReceiverUnknownChannelTypeProvider.class); @@ -57,7 +60,8 @@ public class WundergroundUpdateReceiverUnknownChannelTypeProvider implements Cha ChannelType type = getChannelType(typeUid, null); if (type == null) { String itemType = guessItemType(value); - type = ChannelTypeBuilder.state(typeUid, parameterName + " channel type", itemType).build(); + type = ChannelTypeBuilder.state(typeUid, parameterName + " channel type", itemType).isAdvanced(true) + .withCategory(UNCATEGORIZED).withAutoUpdatePolicy(AutoUpdatePolicy.DEFAULT).build(); return addChannelType(typeUid, type); } return type; @@ -65,18 +69,18 @@ public class WundergroundUpdateReceiverUnknownChannelTypeProvider implements Cha private static String guessItemType(String value) { if (BOOLEAN_STRINGS.contains(value.toLowerCase())) { - return "Switch"; + return CoreItemFactory.SWITCH; } try { Float.valueOf(value); - return "Number"; + return CoreItemFactory.NUMBER; } catch (NumberFormatException ignored) { } - return "String"; + return CoreItemFactory.STRING; } private ChannelType addChannelType(ChannelTypeUID channelTypeUID, ChannelType channelType) { - logger.warn("Adding channelType {} for unknown parameter", channelTypeUID.getAsString()); + logger.warn("Adding new synthetic channelType {} for unrecognised parameter", channelTypeUID.getAsString()); this.channelTypes.put(channelTypeUID, channelType); return channelType; } diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/resources/OH-INF/i18n/wundergroundupdatereceiver.properties b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/resources/OH-INF/i18n/wundergroundupdatereceiver.properties index 2fae057492..33a6ab416b 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/resources/OH-INF/i18n/wundergroundupdatereceiver.properties +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/resources/OH-INF/i18n/wundergroundupdatereceiver.properties @@ -59,9 +59,9 @@ channel-type.wundergroundupdatereceiver.indoor-humidity.label = Indoor Humidity channel-type.wundergroundupdatereceiver.indoor-humidity.description = Indoor humidity in %. channel-type.wundergroundupdatereceiver.indoor-temperature.label = Indoor Temperature channel-type.wundergroundupdatereceiver.indoor-temperature.description = Indoor temperature. -channel-type.wundergroundupdatereceiver.last-query-state.label = The last query +channel-type.wundergroundupdatereceiver.last-query-state.label = Last query channel-type.wundergroundupdatereceiver.last-query-state.description = The query part of the last request from the device -channel-type.wundergroundupdatereceiver.last-query-trigger.label = The last query +channel-type.wundergroundupdatereceiver.last-query-trigger.label = Last query channel-type.wundergroundupdatereceiver.last-query-trigger.description = The query part of the last request from the device channel-type.wundergroundupdatereceiver.last-received-datetime.label = Last Received channel-type.wundergroundupdatereceiver.last-received-datetime.description = The date and time of the last update. diff --git a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/test/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryServiceTest.java b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/test/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryServiceTest.java index e504fe5df5..b90d4b203b 100644 --- a/bundles/org.openhab.binding.wundergroundupdatereceiver/src/test/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.wundergroundupdatereceiver/src/test/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryServiceTest.java @@ -35,11 +35,13 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; +import org.hamcrest.Matcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.DefaultSystemChannelTypeProvider; import org.openhab.core.thing.ManagedThingProvider; import org.openhab.core.thing.Thing; @@ -48,6 +50,7 @@ import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.internal.type.StateChannelTypeBuilderImpl; import org.openhab.core.thing.internal.type.TriggerChannelTypeBuilderImpl; +import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.thing.type.ChannelTypeProvider; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeUID; @@ -69,6 +72,60 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { openMocks(this); } + @Test + void programmaticChannelsAreAddedCorrectlyOnce() { + // Given + final String queryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "humidity=74&" + "AqPM2.5=30&" + + "windspdmph_avg2m=10&" + "dateutc=2021-02-07%2014:04:03&" + "softwaretype=WH2600%20V2.2.8&" + + "action=updateraw&" + "realtime=1&" + "rtfreq=5"; + MetaData.Request request = new MetaData.Request("GET", + new HttpURI("http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + queryString), + HttpVersion.HTTP_1_1, new HttpFields()); + HttpChannel httpChannel = mock(HttpChannel.class); + Request req = new Request(httpChannel, null); + req.setMetaData(request); + + TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry(); + WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService( + true); + HttpService httpService = mock(HttpService.class); + WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService); + discoveryService.addUnhandledStationId(REQ_STATION_ID, sut.normalizeParameterMap(req.getParameterMap())); + Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID) + .withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID))) + .withLabel("test thing").withLocation("location").build(); + ManagedThingProvider managedThingProvider = mock(ManagedThingProvider.class); + when(managedThingProvider.get(any())).thenReturn(thing); + WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(thing, sut, discoveryService, + new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry, managedThingProvider); + handler.setCallback(mock(ThingHandlerCallback.class)); + + // When + handler.initialize(); + var actual = handler.getThing().getChannels(); + + // Then + assertThat(actual.size(), is(9)); + + assertChannel(actual.get(0), METADATA_GROUP, LAST_RECEIVED, LAST_RECEIVED_DATETIME_CHANNELTYPEUID, + ChannelKind.STATE, is("DateTime")); + assertChannel(actual.get(1), METADATA_GROUP, LAST_QUERY_TRIGGER, LAST_QUERY_TRIGGER_CHANNELTYPEUID, + ChannelKind.TRIGGER, nullValue()); + assertChannel(actual.get(2), METADATA_GROUP, LAST_QUERY_STATE, LAST_QUERY_STATE_CHANNELTYPEUID, + ChannelKind.STATE, is("String")); + assertChannel(actual.get(3), METADATA_GROUP, DATEUTC, DATEUTC_CHANNELTYPEUID, ChannelKind.STATE, is("String")); + assertChannel(actual.get(4), METADATA_GROUP, REALTIME_FREQUENCY, REALTIME_FREQUENCY_CHANNELTYPEUID, + ChannelKind.STATE, is("Number")); + assertChannel(actual.get(5), METADATA_GROUP, SOFTWARE_TYPE, SOFTWARETYPE_CHANNELTYPEUID, ChannelKind.STATE, + is("String")); + assertChannel(actual.get(6), HUMIDITY_GROUP, HUMIDITY, HUMIDITY_CHANNELTYPEUID, ChannelKind.STATE, + is("Number:Dimensionless")); + assertChannel(actual.get(7), WIND_GROUP, WIND_SPEED_AVG_2MIN, WIND_SPEED_AVG_2MIN_CHANNELTYPEUID, + ChannelKind.STATE, is("Number:Speed")); + assertChannel(actual.get(8), POLLUTION_GROUP, AQ_PM2_5, PM2_5_MASS_CHANNELTYPEUID, ChannelKind.STATE, + is("Number:Density")); + } + @Test void aRequestWithAnUnregisteredStationidIsAddedToTheQueueOnce() throws ServletException, NamespaceException, IOException { @@ -124,9 +181,9 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry(); WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService( false); - discoveryService.addUnhandledStationId(REQ_STATION_ID, req.getParameterMap()); HttpService httpService = mock(HttpService.class); WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService); + discoveryService.addUnhandledStationId(REQ_STATION_ID, sut.normalizeParameterMap(req.getParameterMap())); Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID) .withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID))) .withLabel("test thing").withLocation("location").build(); @@ -170,9 +227,9 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry(); WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService( true); - discoveryService.addUnhandledStationId(REQ_STATION_ID, req1.getParameterMap()); HttpService httpService = mock(HttpService.class); WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService); + discoveryService.addUnhandledStationId(REQ_STATION_ID, sut.normalizeParameterMap(req1.getParameterMap())); Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID) .withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID))) .withLabel("test thing").withLocation("location").build(); @@ -237,9 +294,9 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry(); WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService( true); - discoveryService.addUnhandledStationId(REQ_STATION_ID, req1.getParameterMap()); HttpService httpService = mock(HttpService.class); WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService); + discoveryService.addUnhandledStationId(REQ_STATION_ID, sut.normalizeParameterMap(req1.getParameterMap())); Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID) .withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID))) .withLabel("test thing").withLocation("location").build(); @@ -285,11 +342,99 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { assertThat(actual, equalTo(before)); } - class TestChannelTypeRegistry extends ChannelTypeRegistry { + @Test + void lastQueryTriggerIsMigratedSuccessfully() throws IOException { + // Given + final String firstDeviceQueryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "tempf=26.1&" + "humidity=74&" + + "dateutc=2021-02-07%2014:04:03&" + "softwaretype=WH2600%20V2.2.8&" + "action=updateraw&" + + "realtime=1&" + "rtfreq=5"; + MetaData.Request request1 = new MetaData.Request("GET", new HttpURI( + "http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + firstDeviceQueryString), + HttpVersion.HTTP_1_1, new HttpFields()); + HttpChannel httpChannel = mock(HttpChannel.class); + Request req1 = new Request(httpChannel, null); + req1.setMetaData(request1); - TestChannelTypeRegistry() { + UpdatingChannelTypeRegistry channelTypeRegistry = new UpdatingChannelTypeRegistry(); + WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService( + true); + HttpService httpService = mock(HttpService.class); + WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService); + discoveryService.addUnhandledStationId(REQ_STATION_ID, sut.normalizeParameterMap(req1.getParameterMap())); + Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID) + .withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID))) + .withLabel("test thing").withLocation("location").build(); + ManagedThingProvider managedThingProvider = mock(ManagedThingProvider.class); + when(managedThingProvider.get(any())).thenReturn(null); + WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(thing, sut, discoveryService, + new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry, managedThingProvider); + handler.setCallback(mock(ThingHandlerCallback.class)); + + // When + handler.initialize(); + sut.addHandler(handler); + + // Then + ChannelTypeUID[] expectedBefore = new ChannelTypeUID[] { TEMPERATURE_CHANNELTYPEUID, HUMIDITY_CHANNELTYPEUID, + DATEUTC_CHANNELTYPEUID, SOFTWARETYPE_CHANNELTYPEUID, REALTIME_FREQUENCY_CHANNELTYPEUID, + LAST_QUERY_STATE_CHANNELTYPEUID, LAST_RECEIVED_DATETIME_CHANNELTYPEUID, + LAST_QUERY_TRIGGER_CHANNELTYPEUID }; + List before = handler.getThing().getChannels().stream().map(Channel::getChannelTypeUID) + .collect(Collectors.toList()); + assertThat(before, hasItems(expectedBefore)); + + // When + var actual = handler.getThing().getChannels(); + + // Then + assertThat(actual.size(), is(8)); + assertChannel(actual.get(7), METADATA_GROUP, LAST_QUERY_TRIGGER, LAST_QUERY_TRIGGER_CHANNELTYPEUID, + ChannelKind.STATE, is("DateTime")); + + // When + handler.dispose(); + handler.initialize(); + + final String secondDeviceQueryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "lowbatt=1&" + "soilmoisture1=78&" + + "soilmoisture2=73&" + "solarradiation=42.24&" + "dateutc=2021-02-07%2014:04:03&" + + "softwaretype=WH2600%20V2.2.8&" + "action=updateraw&" + "realtime=1&" + "rtfreq=5"; + MetaData.Request request = new MetaData.Request("GET", new HttpURI( + "http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + secondDeviceQueryString), + HttpVersion.HTTP_1_1, new HttpFields()); + Request req2 = new Request(httpChannel, null); + req2.setMetaData(request); + sut.activate(); + + // Then + assertThat(sut.isActive(), is(true)); + + // When + sut.doGet(req2, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS)); + actual = handler.getThing().getChannels(); + + // Then + assertThat(actual.size(), is(8)); + assertChannel(actual.get(7), METADATA_GROUP, LAST_QUERY_TRIGGER, LAST_QUERY_TRIGGER_CHANNELTYPEUID, + ChannelKind.TRIGGER, nullValue()); + } + + private void assertChannel(Channel actual, String expectedGroup, String expectedName, ChannelTypeUID expectedUid, + ChannelKind expectedKind, Matcher expectedItemType) { + assertThat(actual, is(notNullValue())); + assertThat(actual.getLabel() + " UID", actual.getUID(), + is(new ChannelUID(TEST_THING_UID, expectedGroup, expectedName))); + assertThat(actual.getLabel() + " ChannelTypeUID", actual.getChannelTypeUID(), is(expectedUid)); + assertThat(actual.getLabel() + " Kind", actual.getKind(), is(expectedKind)); + assertThat(actual.getLabel() + " AcceptedItemType", actual.getAcceptedItemType(), expectedItemType); + } + + abstract class AbstractTestChannelTypeRegistry extends ChannelTypeRegistry { + + protected final ChannelTypeProvider provider; + + AbstractTestChannelTypeRegistry(ChannelTypeProvider mock) { super(); - ChannelTypeProvider provider = mock(ChannelTypeProvider.class); + this.provider = mock; when(provider.getChannelType(eq(SOFTWARETYPE_CHANNELTYPEUID), any())).thenReturn( new StateChannelTypeBuilderImpl(SOFTWARETYPE_CHANNELTYPEUID, "Software type", "String").build()); when(provider.getChannelType(eq(TEMPERATURE_CHANNELTYPEUID), any())) @@ -303,6 +448,11 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { when(provider.getChannelType(eq(HUMIDITY_CHANNELTYPEUID), any())).thenReturn( new StateChannelTypeBuilderImpl(HUMIDITY_CHANNELTYPEUID, "Humidity", "Number:Dimensionless") .build()); + when(provider.getChannelType(eq(WIND_SPEED_AVG_2MIN_CHANNELTYPEUID), any())) + .thenReturn(new StateChannelTypeBuilderImpl(WIND_SPEED_AVG_2MIN_CHANNELTYPEUID, + "Wind Speed 2min Average", "Number:Speed").build()); + when(provider.getChannelType(eq(PM2_5_MASS_CHANNELTYPEUID), any())).thenReturn( + new StateChannelTypeBuilderImpl(PM2_5_MASS_CHANNELTYPEUID, "PM2.5 Mass", "Number:Density").build()); when(provider.getChannelType(eq(DATEUTC_CHANNELTYPEUID), any())).thenReturn( new StateChannelTypeBuilderImpl(DATEUTC_CHANNELTYPEUID, "Last Updated", "String").build()); when(provider.getChannelType(eq(LOW_BATTERY_CHANNELTYPEUID), any())) @@ -316,10 +466,31 @@ class WundergroundUpdateReceiverDiscoveryServiceTest { when(provider.getChannelType(eq(LAST_RECEIVED_DATETIME_CHANNELTYPEUID), any())).thenReturn( new StateChannelTypeBuilderImpl(LAST_RECEIVED_DATETIME_CHANNELTYPEUID, "Last Received", "DateTime") .build()); - when(provider.getChannelType(eq(LAST_QUERY_TRIGGER_CHANNELTYPEUID), any())).thenReturn( - new TriggerChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query").build()); this.addChannelTypeProvider(provider); this.addChannelTypeProvider(new WundergroundUpdateReceiverUnknownChannelTypeProvider()); } } + + class TestChannelTypeRegistry extends AbstractTestChannelTypeRegistry { + + TestChannelTypeRegistry() { + super(mock(ChannelTypeProvider.class)); + when(provider.getChannelType(eq(LAST_QUERY_TRIGGER_CHANNELTYPEUID), any())).thenReturn( + new TriggerChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query").build()); + } + } + + class UpdatingChannelTypeRegistry extends AbstractTestChannelTypeRegistry { + + UpdatingChannelTypeRegistry() { + super(mock(ChannelTypeProvider.class)); + when(provider.getChannelType(eq(LAST_QUERY_TRIGGER_CHANNELTYPEUID), any())) + .thenReturn(new StateChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query", + "DateTime").build()) + .thenReturn(new StateChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query", + "DateTime").build()) + .thenReturn(new TriggerChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query") + .build()); + } + } }