[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 <daniel-github@demus.dk>
This commit is contained in:
Daniel Demus 2022-12-01 22:47:11 +01:00 committed by GitHub
parent 9cc3cd0cf9
commit 43d01ad49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 241 additions and 37 deletions

View 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:

View 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";

View 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);
}

View 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());
}

View 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));
}

View 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("-");
}
}

View 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;
}

View 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.

View 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());
}
}
}