From: Christoph Weitkamp Date: Tue, 8 Dec 2020 04:53:29 +0000 (+0100) Subject: [dwdunwetter] Rework channel creation (#9229) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=af4371844dda47f8775fe88b44d3be81454dde51;p=openhab-addons.git [dwdunwetter] Rework channel creation (#9229) Signed-off-by: Christoph Weitkamp --- diff --git a/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/config/DwdUnwetterConfiguration.java b/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/config/DwdUnwetterConfiguration.java index 83535537c5..a7bbde1564 100644 --- a/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/config/DwdUnwetterConfiguration.java +++ b/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/config/DwdUnwetterConfiguration.java @@ -22,6 +22,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public class DwdUnwetterConfiguration { public int refresh; - public int warningCount; + public int warningCount = 1; public String cellId = ""; } diff --git a/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandler.java b/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandler.java index 7ae12b2f78..d271fc8c38 100644 --- a/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandler.java +++ b/bundles/org.openhab.binding.dwdunwetter/src/main/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandler.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -30,9 +31,10 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.thing.binding.builder.ChannelBuilder; -import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.util.ThingHandlerHelper; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -54,7 +56,6 @@ public class DwdUnwetterHandler extends BaseThingHandler { private @Nullable DwdWarningsData data; private boolean inRefresh; - private boolean initializing; public DwdUnwetterHandler(Thing thing) { super(thing); @@ -79,14 +80,8 @@ public class DwdUnwetterHandler extends BaseThingHandler { return; } - if (initializing) { - logger.trace("Still initializing. Ignoring refresh request."); - return; - } - - ThingStatus status = getThing().getStatus(); - if (status != ThingStatus.ONLINE && status != ThingStatus.UNKNOWN) { - logger.debug("Unable to refresh. Thing status is {}", status); + if (!ThingHandlerHelper.isHandlerInitialized(getThing())) { + logger.debug("Unable to refresh. Thing status is '{}'", getThing().getStatus()); return; } @@ -105,9 +100,7 @@ public class DwdUnwetterHandler extends BaseThingHandler { return; } - if (status == ThingStatus.UNKNOWN) { - updateStatus(ThingStatus.ONLINE); - } + updateStatus(ThingStatus.ONLINE); updateState(getChannelUuid(CHANNEL_LAST_UPDATED), new DateTimeType()); @@ -142,18 +135,36 @@ public class DwdUnwetterHandler extends BaseThingHandler { @Override public void initialize() { logger.debug("Start initializing!"); - initializing = true; updateStatus(ThingStatus.UNKNOWN); DwdUnwetterConfiguration config = getConfigAs(DwdUnwetterConfiguration.class); - warningCount = config.warningCount; + int newWarningCount = config.warningCount; + + if (warningCount != newWarningCount) { + List toBeAddedChannels = new ArrayList<>(); + List toBeRemovedChannels = new ArrayList<>(); + if (warningCount > newWarningCount) { + for (int i = newWarningCount + 1; i <= warningCount; ++i) { + toBeRemovedChannels.addAll(removeChannels(i)); + } + } else { + for (int i = warningCount + 1; i <= newWarningCount; ++i) { + toBeAddedChannels.addAll(createChannels(i)); + } + } + warningCount = newWarningCount; - data = new DwdWarningsData(config.cellId); + ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels); + for (Channel channel : toBeAddedChannels) { + builder.withChannel(channel); + } + updateThing(builder.build()); + } - updateThing(editThing().withChannels(createChannels()).build()); + data = new DwdWarningsData(config.cellId); refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.MINUTES); - initializing = false; + logger.debug("Finished initializing!"); } @@ -166,69 +177,68 @@ public class DwdUnwetterHandler extends BaseThingHandler { } /** - * Creates a trigger Channel. + * Creates a normal, state based, channel associated with a warning. */ - private Channel createTriggerChannel(String typeId, String label, int warningNumber) { + private void createChannelIfNotExist(ThingHandlerCallback cb, List channels, String typeId, String label, + int warningNumber) { ChannelUID channelUID = getChannelUuid(typeId, warningNumber); - return ChannelBuilder.create(channelUID, "String") // - .withType(new ChannelTypeUID(BINDING_ID, typeId)) // - .withLabel(label + " (" + (warningNumber + 1) + ")")// - .withKind(ChannelKind.TRIGGER) // - .build(); + Channel existingChannel = getThing().getChannel(channelUID); + if (existingChannel != null) { + logger.trace("Thing '{}' already has an existing channel '{}'. Omit adding new channel '{}'.", + getThing().getUID(), existingChannel.getUID(), channelUID); + } else { + channels.add(cb.createChannelBuilder(channelUID, new ChannelTypeUID(BINDING_ID, typeId)) + .withLabel(label + " " + getChannelLabelSuffix(warningNumber)).build()); + } } - /** - * Creates a normal, state based, channel associated with a warning. - */ - private Channel createChannel(String typeId, String itemType, String label, int warningNumber) { - ChannelUID channelUID = getChannelUuid(typeId, warningNumber); - return ChannelBuilder.create(channelUID, itemType) // - .withType(new ChannelTypeUID(BINDING_ID, typeId)) // - .withLabel(label + " (" + (warningNumber + 1) + ")")// - .build(); + private String getChannelLabelSuffix(int warningNumber) { + return "(" + (warningNumber + 1) + ")"; } /** - * Creates a normal, state based, channel not associated with a warning. + * Creates the Channels for each warning. + * + * @return The List of Channels to be added */ - private Channel createChannel(String typeId, String itemType, String label) { - ChannelUID channelUID = getChannelUuid(typeId); - return ChannelBuilder.create(channelUID, itemType) // - .withType(new ChannelTypeUID(BINDING_ID, typeId)) // - .withLabel(label)// - .build(); + private List createChannels(int warningNumber) { + logger.debug("Building channels for thing '{}'.", getThing().getUID()); + List channels = new ArrayList<>(); + ThingHandlerCallback callback = getCallback(); + if (callback != null) { + createChannelIfNotExist(callback, channels, CHANNEL_UPDATED, "Updated", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_WARNING, "Warning", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_SEVERITY, "Severity", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_DESCRIPTION, "Description", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_EFFECTIVE, "Issued", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_ONSET, "Valid From", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_EXPIRES, "Valid To", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_EVENT, "Type", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_HEADLINE, "Headline", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_ALTITUDE, "Height (from)", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_CEILING, "Height (to)", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_INSTRUCTION, "Instruction", warningNumber); + createChannelIfNotExist(callback, channels, CHANNEL_URGENCY, "Urgency", warningNumber); + } + return channels; } /** - * Creates the ChannelsT for each warning. + * Filters the Channels for each warning * - * @return The List of Channels + * @return The List of Channels to be removed */ - private List createChannels() { - List channels = new ArrayList<>(warningCount * 11 + 1); - channels.add(createChannel(CHANNEL_LAST_UPDATED, "DateTime", "Last Updated")); - for (int i = 0; i < warningCount; i++) { - channels.add(createChannel(CHANNEL_WARNING, "Switch", "Warning", i)); - channels.add(createTriggerChannel(CHANNEL_UPDATED, "Updated", i)); - channels.add(createChannel(CHANNEL_SEVERITY, "String", "Severity", i)); - channels.add(createChannel(CHANNEL_DESCRIPTION, "String", "Description", i)); - channels.add(createChannel(CHANNEL_EFFECTIVE, "DateTime", "Issued", i)); - channels.add(createChannel(CHANNEL_ONSET, "DateTime", "Valid From", i)); - channels.add(createChannel(CHANNEL_EXPIRES, "DateTime", "Valid To", i)); - channels.add(createChannel(CHANNEL_EVENT, "String", "Type", i)); - channels.add(createChannel(CHANNEL_HEADLINE, "String", "Headline", i)); - channels.add(createChannel(CHANNEL_ALTITUDE, "Number:Length", "Height (from)", i)); - channels.add(createChannel(CHANNEL_CEILING, "Number:Length", "Height (to)", i)); - channels.add(createChannel(CHANNEL_INSTRUCTION, "String", "Instruction", i)); - channels.add(createChannel(CHANNEL_URGENCY, "String", "Urgency", i)); - } - return channels; + @SuppressWarnings("null") + private List removeChannels(int warningNumber) { + return getThing().getChannels().stream() + .filter(channel -> channel.getLabel() != null + && channel.getLabel().endsWith(getChannelLabelSuffix(warningNumber))) + .collect(Collectors.toList()); } @Override public void dispose() { final ScheduledFuture job = refreshJob; - if (job != null) { job.cancel(true); } diff --git a/bundles/org.openhab.binding.dwdunwetter/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.dwdunwetter/src/main/resources/OH-INF/thing/thing-types.xml index 714e92133f..5a25f8beef 100644 --- a/bundles/org.openhab.binding.dwdunwetter/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.dwdunwetter/src/main/resources/OH-INF/thing/thing-types.xml @@ -7,6 +7,9 @@ Weather Warnings for an area + + + diff --git a/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/DwdUnwetterHandlerTest.java b/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/DwdUnwetterHandlerTest.java deleted file mode 100644 index 7673057cc5..0000000000 --- a/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/DwdUnwetterHandlerTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.dwdunwetter; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -import java.io.InputStream; -import java.util.List; -import java.util.Objects; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -import org.hamcrest.CoreMatchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.openhab.binding.dwdunwetter.internal.DwdUnwetterBindingConstants; -import org.openhab.binding.dwdunwetter.internal.handler.DwdUnwetterHandler; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.test.java.JavaTest; -import org.openhab.core.thing.Channel; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusInfo; -import org.openhab.core.thing.ThingUID; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.binding.ThingHandlerCallback; -import org.openhab.core.thing.type.ChannelTypeUID; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -/** - * Test cases for {@link DwdUnwetterHandler}. The tests provide mocks for supporting entities using Mockito. - * - * @author Martin Koehler - Initial contribution - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.WARN) -public class DwdUnwetterHandlerTest extends JavaTest { - - private ThingHandler handler; - - private @Mock ThingHandlerCallback callback; - private @Mock Thing thing; - - @BeforeEach - public void setUp() { - handler = new DwdUnwetterHandler(thing); - handler.setCallback(callback); - // mock getConfiguration to prevent NPEs - when(thing.getUID()).thenReturn(new ThingUID(DwdUnwetterBindingConstants.BINDING_ID, "test")); - Configuration configuration = new Configuration(); - configuration.put("refresh", Integer.valueOf("1")); - configuration.put("warningCount", Integer.valueOf("1")); - when(thing.getConfiguration()).thenReturn(configuration); - } - - @Test - public void testInitializeShouldCallTheCallback() { - // we expect the handler#initialize method to call the callback during execution and - // pass it the thing and a ThingStatusInfo object containing the ThingStatus of the thing. - handler.initialize(); - - // the argument captor will capture the argument of type ThingStatusInfo given to the - // callback#statusUpdated method. - ArgumentCaptor statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); - - // verify the interaction with the callback and capture the ThingStatusInfo argument: - waitForAssert(() -> { - verify(callback, times(1)).statusUpdated(eq(thing), statusInfoCaptor.capture()); - }); - - // assert that the (temporary) UNKNOWN status was to the mocked thing first: - assertThat(statusInfoCaptor.getAllValues().get(0).getStatus(), is(ThingStatus.UNKNOWN)); - } - - /** - * Tests that the labels of the channels are equal to the ChannelType Definition - */ - @Test - public void testLabels() throws Exception { - handler.initialize(); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - InputStream stream = getClass().getResourceAsStream("/OH-INF/thing/thing-types.xml"); - Document document = builder.parse(stream); - NodeList nodeList = document.getElementsByTagName("channel-type"); - - thing = handler.getThing(); - List channels = thing.getChannels(); - for (Channel channel : channels) { - String label = getLabel(nodeList, channel.getChannelTypeUID()); - assertThat(channel.getLabel(), CoreMatchers.startsWith(label)); - } - } - - private String getLabel(NodeList nodeList, ChannelTypeUID uuid) { - for (int i = 0; i < nodeList.getLength(); i++) { - Node node = nodeList.item(i); - Node nodeId = node.getAttributes().getNamedItem("id"); - if (nodeId == null) { - continue; - } - if (Objects.equals(nodeId.getTextContent(), uuid.getId())) { - return getLabel(node.getChildNodes()); - } - } - return null; - } - - private String getLabel(NodeList nodeList) { - for (int i = 0; i < nodeList.getLength(); i++) { - Node node = nodeList.item(i); - if (node.getNodeName().equals("label")) { - return node.getTextContent(); - } - } - return null; - } -} diff --git a/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandlerTest.java b/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandlerTest.java new file mode 100644 index 0000000000..76afca3073 --- /dev/null +++ b/bundles/org.openhab.binding.dwdunwetter/src/test/java/org/openhab/binding/dwdunwetter/internal/handler/DwdUnwetterHandlerTest.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dwdunwetter.internal.handler; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.dwdunwetter.internal.DwdUnwetterBindingConstants; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.test.java.JavaTest; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Test cases for {@link DwdUnwetterHandler}. The tests provide mocks for supporting entities using Mockito. + * + * @author Martin Koehler - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +public class DwdUnwetterHandlerTest extends JavaTest { + + private ThingHandler handler; + + private @Mock ThingHandlerCallback callback; + private @Mock Thing thing; + + @BeforeEach + public void setUp() { + when(callback.createChannelBuilder(any(ChannelUID.class), any(ChannelTypeUID.class))) + .thenAnswer(invocation -> ChannelBuilder.create(invocation.getArgument(0, ChannelUID.class)) + .withType(invocation.getArgument(1, ChannelTypeUID.class))); + + handler = new DwdUnwetterHandler(thing); + handler.setCallback(callback); + // mock getConfiguration to prevent NPEs + when(thing.getUID()).thenReturn(new ThingUID(DwdUnwetterBindingConstants.BINDING_ID, "test")); + Configuration configuration = new Configuration(); + configuration.put("refresh", Integer.valueOf("1")); + configuration.put("warningCount", Integer.valueOf("1")); + when(thing.getConfiguration()).thenReturn(configuration); + } + + @Test + public void testInitializeShouldCallTheCallback() { + // we expect the handler#initialize method to call the callback during execution and + // pass it the thing and a ThingStatusInfo object containing the ThingStatus of the thing. + handler.initialize(); + + // the argument captor will capture the argument of type ThingStatusInfo given to the + // callback#statusUpdated method. + ArgumentCaptor statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); + + // verify the interaction with the callback and capture the ThingStatusInfo argument: + waitForAssert(() -> { + verify(callback, times(1)).statusUpdated(eq(thing), statusInfoCaptor.capture()); + }); + + // assert that the (temporary) UNKNOWN status was to the mocked thing first: + assertThat(statusInfoCaptor.getAllValues().get(0).getStatus(), is(ThingStatus.UNKNOWN)); + } + + /** + * Tests that the labels of the channels are equal to the ChannelType Definition + */ + @Test + public void testLabels() throws Exception { + handler.initialize(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + InputStream stream = getClass().getResourceAsStream("/OH-INF/thing/thing-types.xml"); + Document document = builder.parse(stream); + NodeList nodeList = document.getElementsByTagName("channel-type"); + + thing = handler.getThing(); + List channels = thing.getChannels(); + for (Channel channel : channels) { + String label = getLabel(nodeList, channel.getChannelTypeUID()); + assertThat(channel.getLabel(), CoreMatchers.startsWith(label)); + } + } + + private String getLabel(NodeList nodeList, ChannelTypeUID uuid) { + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + Node nodeId = node.getAttributes().getNamedItem("id"); + if (nodeId == null) { + continue; + } + if (Objects.equals(nodeId.getTextContent(), uuid.getId())) { + return getLabel(node.getChildNodes()); + } + } + return null; + } + + private String getLabel(NodeList nodeList) { + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.getNodeName().equals("label")) { + return node.getTextContent(); + } + } + return null; + } +}