]> git.basschouten.com Git - openhab-addons.git/commitdiff
[wundergroundupdatereceiver] Bugfixes: Regenerate trigger channel with proper type...
authorDaniel Demus <daniel-github@demus.dk>
Thu, 1 Dec 2022 21:47:11 +0000 (22:47 +0100)
committerGitHub <noreply@github.com>
Thu, 1 Dec 2022 21:47:11 +0000 (22:47 +0100)
* [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 <daniel-github@demus.dk>
bundles/org.openhab.binding.wundergroundupdatereceiver/README.md
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverBindingConstants.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryService.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverHandler.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverParameterMapping.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverServlet.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverUnknownChannelTypeProvider.java
bundles/org.openhab.binding.wundergroundupdatereceiver/src/main/resources/OH-INF/i18n/wundergroundupdatereceiver.properties
bundles/org.openhab.binding.wundergroundupdatereceiver/src/test/java/org/openhab/binding/wundergroundupdatereceiver/internal/WundergroundUpdateReceiverDiscoveryServiceTest.java

index a8c5ebe04cbf2f3ed93fd2364ca7f0218ccd661b..891b65977e79090d762fcc7546145f2576012f1f 100644 (file)
@@ -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)://<your-openHAB-server>:<openHAB-port>/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:
index 2dd0e685b1827c6e47ee2afa54a5764d1c85f13c..b0ef872986e84dd8008290fbd4df8e2468a283dd 100644 (file)
@@ -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";
 
index 8300bb71b5f9bc13dabe1928c5cabf29aadda130..c93cfae1a40bdd925f74e7ac336f783053334997 100644 (file)
@@ -38,7 +38,7 @@ public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscover
     WundergroundUpdateReceiverServletControls servletControls;
 
     private static final int TIMEOUT_SEC = 1;
-    private final HashMap<String, Map<String, String[]>> thinglessStationIds = new HashMap<>();
+    private final HashMap<String, Map<String, String>> 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<String, String[]> request) {
+    public void addUnhandledStationId(@Nullable String stationId, Map<String, String> request) {
         if (stationId == null || stationId.isEmpty()) {
             return;
         }
@@ -73,7 +73,7 @@ public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscover
         return isBackgroundDiscoveryEnabled() || isScanning();
     }
 
-    public @Nullable Map<String, String[]> getUnhandledStationRequest(@Nullable String stationId) {
+    public @Nullable Map<String, String> getUnhandledStationRequest(@Nullable String stationId) {
         return this.thinglessStationIds.get(stationId);
     }
 
index 09fe7e0baa78811c0bc838b188898d47a654b449..e0254638a4c65c5c2af33ba47c89284b67095b16 100644 (file)
@@ -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<String, String[]> requestParameters = discoveryService.getUnhandledStationRequest(config.stationId);
+        Map<String, String> 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());
     }
 
index 09f43608bdc49771801a8f9f059d5f9ceaaaf07e..d55c3fcf3e3154a9cc2c8adcadf9a7d7c1c34d5b 100644 (file)
@@ -54,7 +54,7 @@ public class WundergroundUpdateReceiverParameterMapping {
     }
 
     private static final List<String> 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<WundergroundUpdateReceiverParameterMapping> 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));
     }
 
index 005b2809674745bbf27ea6ff4b52e8b16e4bbf41..cdba261011bcab452f2af3f2a099d9c79154cfd7 100644 (file)
@@ -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<String, WundergroundUpdateReceiverHandler> handlers = new HashMap<>();
@@ -173,6 +175,11 @@ public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet
         }
     }
 
+    protected Map<String, String> normalizeParameterMap(Map<String, String[]> 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<String, String> states = normalizeParameterMap(req.getParameterMap());
         Optional.ofNullable(this.handlers.get(stationId)).ifPresentOrElse(handler -> {
-            Map<String, String> 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<String, WundergroundUpdateReceiverHandler> getHandlers() {
         return Collections.unmodifiableMap(this.handlers);
     }
+
+    private String makeUidSafeString(String key) {
+        return CLEANER.matcher(key).replaceAll("-");
+    }
 }
index 6c24f8961149ed7b42aa5a19cdb91b1becc4eb7b..47f4c8343a29079e6d38a843753734603b8ae67a 100644 (file)
@@ -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<String> BOOLEAN_STRINGS = List.of("1", "0", "true", "false");
+    private static final List<String> BOOLEAN_STRINGS = List.of("1", "0", "true", "false", "yes", "no", "on", "off");
     private final Map<ChannelTypeUID, ChannelType> 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;
     }
index 2fae057492cadd475044e1ac019f6d5e27b24958..33a6ab416bad966508fe181076ca7902b3c55226 100644 (file)
@@ -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.
index e504fe5df52254cfbddd4a0387e6eff93207dd30..b90d4b203b2e05d94e7322c5248c9fc986b7c7a6 100644 (file)
@@ -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<ChannelTypeUID> 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<Object> 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());
+        }
+    }
 }