From d30c941f0e6e2acbc7cbc8350ddf23783f47a660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Fri, 5 Jun 2020 10:25:32 +0200 Subject: [PATCH 01/83] [vigiecrues] Vigicrues binding : track river level (#7503) Signed-off-by: clinique --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.vigicrues/.classpath | 32 ++++ .../org.openhab.binding.vigicrues/.project | 23 +++ bundles/org.openhab.binding.vigicrues/NOTICE | 13 ++ .../org.openhab.binding.vigicrues/README.md | 81 ++++++++++ bundles/org.openhab.binding.vigicrues/pom.xml | 15 ++ .../src/main/feature/feature.xml | 9 ++ .../internal/VigiCruesBindingConstants.java | 44 ++++++ .../internal/VigiCruesConfiguration.java | 27 ++++ .../internal/VigiCruesHandlerFactory.java | 74 +++++++++ .../internal/handler/VigiCruesHandler.java | 141 ++++++++++++++++++ .../internal/json/OpenDatasoftResponse.java | 50 +++++++ .../vigicrues/internal/json/Parameters.java | 64 ++++++++ .../vigicrues/internal/json/Record.java | 57 +++++++ .../vigicrues/internal/json/Refine.java | 33 ++++ .../internal/json/VigiCruesFields.java | 61 ++++++++ .../resources/ESH-INF/binding/binding.xml | 11 ++ .../resources/ESH-INF/thing/thing-types.xml | 50 +++++++ bundles/pom.xml | 1 + 20 files changed, 792 insertions(+) create mode 100644 bundles/org.openhab.binding.vigicrues/.classpath create mode 100644 bundles/org.openhab.binding.vigicrues/.project create mode 100644 bundles/org.openhab.binding.vigicrues/NOTICE create mode 100644 bundles/org.openhab.binding.vigicrues/README.md create mode 100644 bundles/org.openhab.binding.vigicrues/pom.xml create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/VigiCruesBindingConstants.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/VigiCruesConfiguration.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/VigiCruesHandlerFactory.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/handler/VigiCruesHandler.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/json/OpenDatasoftResponse.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/json/Parameters.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/json/Record.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/json/Refine.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/json/VigiCruesFields.java create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.vigicrues/src/main/resources/ESH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 5e299a75cc4a4..9b9bf59650c17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -208,6 +208,7 @@ /bundles/org.openhab.binding.vektiva/ @octa22 /bundles/org.openhab.binding.velbus/ @cedricboon /bundles/org.openhab.binding.velux/ @gs4711 +/bundles/org.openhab.binding.vigicrues/ @clinique /bundles/org.openhab.binding.vitotronic/ @steand /bundles/org.openhab.binding.volvooncall/ @clinique /bundles/org.openhab.binding.weathercompany/ @mhilbush diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index df3a28ecb3107..15e92b1609b7c 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1036,6 +1036,11 @@ org.openhab.binding.velux ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.vigicrues + ${project.version} + org.openhab.addons.bundles org.openhab.binding.vitotronic diff --git a/bundles/org.openhab.binding.vigicrues/.classpath b/bundles/org.openhab.binding.vigicrues/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.vigicrues/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.vigicrues/.project b/bundles/org.openhab.binding.vigicrues/.project new file mode 100644 index 0000000000000..8bdaeaeb2f825 --- /dev/null +++ b/bundles/org.openhab.binding.vigicrues/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.vigicrues + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.vigicrues/NOTICE b/bundles/org.openhab.binding.vigicrues/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.vigicrues/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.vigicrues/README.md b/bundles/org.openhab.binding.vigicrues/README.md new file mode 100644 index 0000000000000..744569a98129d --- /dev/null +++ b/bundles/org.openhab.binding.vigicrues/README.md @@ -0,0 +1,81 @@ +# VigiCrues Binding + +This binding allows you to get data regarding water flow and water height on major French rivers. +These data are made public through OpenDataSoft website. + +## Supported Things + +There is exactly one supported thing type, which represents a river level measurement station. +It is identified by the `id`. + +To get your station id : + +1. open https://www.vigicrues.gouv.fr/ + +2. Select your region on the France map + +3. Select the station nearest to your location + +4. In the 'Info Station' tab you'll get the id just near the station name (e.g. X9999999299) + +Of course, you can add multiple Things, e.g. for getting measures for different locations. + + +## Discovery + +This binding does not handle auto-discovery. + +## Binding Configuration + +The binding has no configuration options, all configuration is done at Thing level. + +## Thing Configuration + +The thing has a few configuration parameters: + +| Parameter | Description | +|-----------|-------------------------------------------------------------------------| +| id | Id of the station. | +| refresh | Refresh interval in minutes. Optional, the default value is 30 minutes. | + + +## Channels + +The VigiCrues information that retrieved are made available with these channels: + +| Channel ID | Item Type | Description | +|------------------|---------------------------|-------------------------------| +| observation-time | DateTime | Date and time of measurement | +| flow | Number:VolumetricFlowRate | Volume of water per time unit | +| height | Number:Length | Water height of the river | + + +## Full Example + +vigicrues.things: + +``` +Thing vigicrues:station:poissy "Station Poissy" @ "VigiCrues" [id="H300000201", refresh=30] +Thing vigicrues:station:vernon "Station Vernon" @ "VigiCrues" [id="H320000104", refresh=30] +``` + +vigicrues.items: + +``` +Group gVigiCrues "VigiCrues" + Number:Length VC_hauteur "Hauteur Eau Poissy [%.2f %unit%]" (gVigiCrues) {channel="vigicrues:station:poissy:height"} + Number:VolumetricFlowRate VC_debit "Débit Eau Poissy [%.2f %unit%]" (gVigiCrues) {channel="vigicrues:station:poissy:flow"} + DateTime VC_ObservationPTS "Timestamp [%1$tH:%1$tM]" + + org.openhab.addons.bundles + org.openhab.binding.novafinedust + ${project.version} + org.openhab.addons.bundles org.openhab.binding.ntp diff --git a/bundles/org.openhab.binding.novafinedust/.classpath b/bundles/org.openhab.binding.novafinedust/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.novafinedust/.project b/bundles/org.openhab.binding.novafinedust/.project new file mode 100644 index 0000000000000..ce6cbf0e75027 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.novafinedust + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.novafinedust/NOTICE b/bundles/org.openhab.binding.novafinedust/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.novafinedust/README.md b/bundles/org.openhab.binding.novafinedust/README.md new file mode 100644 index 0000000000000..b5b1e8ea0b593 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/README.md @@ -0,0 +1,84 @@ +# NovaFineDust Binding + +This binding is for the fine dust sensor (PM Sensor) from Nova Fitness. +Currently only one model is supported, the SDS011. + +It basically implements the protocol specified in [this document](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf). +One can measure the PM 2.5 and PM 10 values with this device. +It comes very handy for detecting air pollution like neighbors firing their oven with wet wood etc. so one can deactivate the ventilation system. + +## Supported Things + +There is only one Thing type for this binding, which is `SDS011`. + +## Discovery + +There is no automatic discovery. + +## Thing Configuration + +There are 2 different working modes for the `SDS011` thing: Reporting and Polling. + +### Reporting + +This is the preferred mode and thus also configured as a default. +In this mode the sensor wakes up every `reportingInterval` minutes, performs a measurement for 30 seconds and sleeps for `reportingInterval` minus 30 seconds. +Remember: According to the [datasheet](https://www-sd-nf.oss-cn-beijing.aliyuncs.com/%E5%AE%98%E7%BD%91%E4%B8%8B%E8%BD%BD/SDS011%20laser%20PM2.5%20sensor%20specification-V1.4.pdf) the sensor has a lifetime of 8000 hours. Using a 0 as `reportingInterval` will make the sensor report its data as fast as possible. + +### Polling + +If one needs data in different intervals, i.e. not as fast as possible and not in intervals that are a multiple of full minutes, polling can be configured. +The `pollingInterval` parameter specifies the time in seconds when data will be polled from the sensor. + +In addition to the mode one has to provide the port to which the device is connected. + +A full overview about the parameters of the `SDS011` thing is given in the following table: + +| parameter name | mandatory | description | +|-------------------|-----------|---------------------------------------------------------------------------------------| +| port | yes | the port the sensor is connected to, i.e. /dev/ttyUSB0. | +| reporting | no | whether the reporting mode (value=true) or polling mode should be used. | +| reportingInterval | no | the time in minutes between reportings from the sensor (default=1, min=0, max=30). | +| pollingInterval | no | the time in seconds between data polls from the device. (default=10, min=3, max=3600) | + +## Channels + +Since the supported device is a sensor, both channels are read-only channels. + +| channel | type | description | +|----------|----------------|-------------------------------| +| pm25 | Number:Density | This provides the PM2.5 value | +| pm10 | Number:Density | This provides the PM10 value | + +## Full Example + +demo.things: + +``` +Thing novafinedust:SDS011:mySDS011Report "My SDS011 Fine Dust Sensor with reporting" [ port="/dev/ttyUSB0", reporting=true, reportingInterval=1 ] +Thing novafinedust:SDS011:mySDS011Poll "My SDS011 Fine Dust Sensor with polling" [ port="/dev/ttyUSB0", reporting=false, pollingInterval=10 ] +``` + +demo.items: + +``` +Number:Density PM25 "My PM 2.5 value" { channel="novafinedust:SDS011:mySDS011Report:pm25" } +Number:Density PM10 "My PM 10 value" { channel="novafinedust:SDS011:mySDS011Report:pm10" } +``` + +demo.sitemap: + +``` +sitemap demo label="Main Menu" +{ + Frame { + Text item=PM25 label="My PM 2.5 value" + Text item=PM10 label="My PM 10 value" + } +} +``` + +## Limitations + +In theory one can have multiple sensors connected and distinguish them via their device ID. However, this is currently not implemented and the binding always configures any device and accepts data reportings from any device too. +However, it is implemented that one can attach one sensor to one serial port, like `/dev/ttyUSB0` and a second sensor on a different serial port, like `/dev/ttyUSB1`. diff --git a/bundles/org.openhab.binding.novafinedust/pom.xml b/bundles/org.openhab.binding.novafinedust/pom.xml new file mode 100644 index 0000000000000..06f7b28af079a --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.6-SNAPSHOT + + + org.openhab.binding.novafinedust + + openHAB Add-ons :: Bundles :: NovaFineDust Binding + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml b/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml new file mode 100644 index 0000000000000..0c485318fc7c4 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.novafinedust/${project.version} + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java new file mode 100644 index 0000000000000..e73629672c43d --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java @@ -0,0 +1,35 @@ +/** + * 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.novafinedust.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link NovaFineDustBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class NovaFineDustBindingConstants { + + private static final String BINDING_ID = "novafinedust"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SDS011 = new ThingTypeUID(BINDING_ID, "SDS011"); + + // List of all Channel ids + public static final String CHANNEL_PM25 = "pm25"; + public static final String CHANNEL_PM10 = "pm10"; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java new file mode 100644 index 0000000000000..08c52a09b32b8 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java @@ -0,0 +1,32 @@ +/** + * 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.novafinedust.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link NovaFineDustConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class NovaFineDustConfiguration { + + /** + * USB port of the device + */ + public String port = ""; + public boolean reporting = true; + public int reportingInterval = 1; + public int pollingInterval = 10; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java new file mode 100644 index 0000000000000..860c4617acb99 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * 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.novafinedust.internal; + +import static org.openhab.binding.novafinedust.internal.NovaFineDustBindingConstants.THING_TYPE_SDS011; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link NovaFineDustHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.novafinedust", service = ThingHandlerFactory.class) +public class NovaFineDustHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_SDS011); + + private final SerialPortManager serialPortManager; + + @Activate + public NovaFineDustHandlerFactory(@Reference SerialPortManager serialPortManager) { + this.serialPortManager = serialPortManager; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_SDS011.equals(thingTypeUID)) { + return new SDS011Handler(thing, serialPortManager); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java new file mode 100644 index 0000000000000..bf82a39267c17 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java @@ -0,0 +1,268 @@ +/** + * 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.novafinedust.internal; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.TooManyListenersException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.dimension.Density; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +import org.openhab.binding.novafinedust.internal.sds011protocol.SDS011Communicator; +import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDS011Handler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class SDS011Handler extends BaseThingHandler { + private static final Duration CONNECTION_MONITOR_START_DELAY_OFFSET = Duration.ofSeconds(10); + + private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class); + private final SerialPortManager serialPortManager; + + private NovaFineDustConfiguration config = new NovaFineDustConfiguration(); + private @Nullable SDS011Communicator communicator; + + private @Nullable ScheduledFuture pollingJob; + private @Nullable ScheduledFuture connectionMonitor; + + private ZonedDateTime lastCommunication = ZonedDateTime.now(); + + // initialize timeBetweenDataShouldArrive with a large number + private Duration timeBetweenDataShouldArrive = Duration.ofDays(1); + private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5); + + // cached values for refresh command + private State statePM10 = UnDefType.UNDEF; + private State statePM25 = UnDefType.UNDEF; + + public SDS011Handler(Thing thing, SerialPortManager serialPortManager) { + super(thing); + this.serialPortManager = serialPortManager; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // refresh channels with last received values from cache + if (RefreshType.REFRESH.equals(command)) { + if (NovaFineDustBindingConstants.CHANNEL_PM25.equals(channelUID.getId()) && statePM25 != UnDefType.UNDEF) { + updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25); + } + if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) { + updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10); + } + } + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + + config = getConfigAs(NovaFineDustConfiguration.class); + + if (!validateConfiguration()) { + return; + } + + // parse ports and if the port is found, initialize the reader + SerialPortIdentifier portId = serialPortManager.getIdentifier(config.port); + if (portId == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known!"); + return; + } + + this.communicator = new SDS011Communicator(this, portId); + + if (config.reporting) { + timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval); + scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); + } else { + timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval); + scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); + } + + Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET); + connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, + connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS); + } + + private void initializeCommunicator(WorkMode mode, Duration interval) { + SDS011Communicator localCommunicator = communicator; + if (localCommunicator == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Could not create communicator instance"); + return; + } + + boolean initSuccessful = false; + try { + initSuccessful = localCommunicator.initialize(mode, interval); + } catch (final IOException ex) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!"); + return; + } catch (PortInUseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!"); + return; + } catch (TooManyListenersException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot attach listener to port!"); + return; + } catch (UnsupportedCommOperationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot set serial port parameters"); + return; + } + + if (initSuccessful) { + lastCommunication = ZonedDateTime.now(); + updateStatus(ThingStatus.ONLINE); + + if (mode == WorkMode.POLLING) { + pollingJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.requestSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device"); + } + }, 2, config.pollingInterval, TimeUnit.SECONDS); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Commands and replies from the device don't seem to match"); + logger.debug("Could not configure sensor -> setting Thing to OFFLINE and disposing the handler"); + dispose(); + } + } + + private boolean validateConfiguration() { + if (config.port.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!"); + return false; + } + if (config.reporting) { + if (config.reportingInterval < 0 || config.reportingInterval > 30) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Reporting interval has to be between 0 and 30 minutes"); + return false; + } + } else { + if (config.pollingInterval < 3 || config.pollingInterval > 3600) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Polling interval has to be between 3 and 3600 seconds"); + return false; + } + } + return true; + } + + @Override + public void dispose() { + ScheduledFuture localPollingJob = this.pollingJob; + if (localPollingJob != null) { + localPollingJob.cancel(true); + this.pollingJob = null; + } + + ScheduledFuture localConnectionMonitor = this.connectionMonitor; + if (localConnectionMonitor != null) { + localConnectionMonitor.cancel(true); + this.connectionMonitor = null; + } + + SDS011Communicator localCommunicator = this.communicator; + if (localCommunicator != null) { + localCommunicator.dispose(); + } + + this.statePM10 = UnDefType.UNDEF; + this.statePM25 = UnDefType.UNDEF; + } + + /** + * Pass the data from the device to the Thing channels + * + * @param sensorData the parsed data from the sensor + */ + public void updateChannels(SensorMeasuredDataReply sensorData) { + if (sensorData.isValidData()) { + logger.debug("Updating channels with data: {}", sensorData); + + QuantityType statePM10 = new QuantityType<>(sensorData.getPm10(), + SmartHomeUnits.MICROGRAM_PER_CUBICMETRE); + updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10); + this.statePM10 = statePM10; + + QuantityType statePM25 = new QuantityType<>(sensorData.getPm25(), + SmartHomeUnits.MICROGRAM_PER_CUBICMETRE); + updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25); + this.statePM25 = statePM25; + + updateStatus(ThingStatus.ONLINE); + } + // there was a communication, even if the data was not valid, thus resetting the value here + lastCommunication = ZonedDateTime.now(); + } + + private void verifyIfStillConnected() { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTolerance); + if (now.isAfter(lastData)) { + logger.debug("Check Alive timer: Timeout: lastCommunication={}, interval={}, tollerance={}", + lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Check connection cable and afterwards disable and enable this thing to make it work again"); + // in case someone has pulled the plug, we dispose ourselves and the user has to deactivate/activate the + // thing once the cable is plugged in again + dispose(); + } else { + logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}", + lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance); + } + } + + /** + * Set the firmware property on the Thing + * + * @param firmwareVersion the firmware version as a String + */ + public void setFirmware(String firmwareVersion) { + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java new file mode 100644 index 0000000000000..c86af3d705bc3 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java @@ -0,0 +1,35 @@ +/** + * 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.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class holding the command constants to be send to the sensor in the first data byte + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class Command { + + private Command() { + } + + public static final byte MODE = 2; + public static final byte REQUEST_DATA = 4; + public static final byte HARDWARE_ID = 5; + public static final byte SLEEP = 6; + public static final byte FIRMWARE = 7; + public static final byte WORKING_PERIOD = 8; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java new file mode 100644 index 0000000000000..44132bea4411c --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java @@ -0,0 +1,71 @@ +/** + * 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.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; + +/** + * Factory for creating the specific reply instances for data received from the sensor + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class ReplyFactory { + + private static final byte COMMAND_REPLY = (byte) 0xC5; + private static final byte DATA_REPLY = (byte) 0xC0; + + private ReplyFactory() { + } + + /** + * Creates the specific reply message according to the commandID and first data byte + * + * @param bytes the received message + * @return a specific instance of a sensor reply message + */ + public static @Nullable SensorReply create(byte[] bytes) { + if (bytes.length != 10) { + return null; + } + + byte commandID = bytes[1]; + byte firstDataByte = bytes[2]; + + if (commandID == COMMAND_REPLY) { + switch (firstDataByte) { + case Command.FIRMWARE: + return new SensorFirmwareReply(bytes); + case Command.WORKING_PERIOD: + return new WorkingPeriodReply(bytes); + case Command.MODE: + return new ModeReply(bytes); + case Command.SLEEP: + return new SleepReply(bytes); + default: + return new SensorReply(bytes); + } + } else if (commandID == DATA_REPLY) { + return new SensorMeasuredDataReply(bytes); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java new file mode 100644 index 0000000000000..9e3fd9b960cb5 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java @@ -0,0 +1,315 @@ +/** + * 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.novafinedust.internal.sds011protocol; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Duration; +import java.util.Arrays; +import java.util.TooManyListenersException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.util.HexUtils; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPort; +import org.eclipse.smarthome.io.transport.serial.SerialPortEvent; +import org.eclipse.smarthome.io.transport.serial.SerialPortEventListener; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +import org.openhab.binding.novafinedust.internal.SDS011Handler; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Central instance to communicate with the device, i.e. receive data from it and send commands to it + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SDS011Communicator implements SerialPortEventListener { + + private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class); + + private SerialPortIdentifier portId; + private SDS011Handler thingHandler; + private @Nullable SerialPort serialPort; + + private @Nullable OutputStream outputStream; + private @Nullable InputStream inputStream; + + public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId) { + this.thingHandler = thingHandler; + this.portId = portId; + } + + /** + * Initialize the communication with the device, i.e. open the serial port etc. + * + * @param mode the {@link WorkMode} if we want to use polling or reporting + * @param interval the time between polling or reportings + * @return {@code true} if we can communicate with the device + * @throws PortInUseException + * @throws TooManyListenersException + * @throws IOException + * @throws UnsupportedCommOperationException + */ + public boolean initialize(WorkMode mode, Duration interval) + throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException { + boolean initSuccessful = true; + + SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000); + localSerialPort.setSerialPortParams(9600, 8, 1, 0); + + outputStream = localSerialPort.getOutputStream(); + inputStream = localSerialPort.getInputStream(); + + if (inputStream == null || outputStream == null) { + throw new IOException("Could not create input or outputstream for the port"); + } + + // wake up the device + initSuccessful &= sendSleep(false); + initSuccessful &= getFirmware(); + + if (mode == WorkMode.POLLING) { + initSuccessful &= setMode(WorkMode.POLLING); + initSuccessful &= setWorkingPeriod((byte) 0); + } else { + // reporting + initSuccessful &= setWorkingPeriod((byte) interval.toMinutes()); + initSuccessful &= setMode(WorkMode.REPORTING); + } + + // enable listeners only after we have configured the sensor above because for configuring we send and read data + // sequentially + localSerialPort.notifyOnDataAvailable(true); + localSerialPort.addEventListener(this); + this.serialPort = localSerialPort; + + return initSuccessful; + } + + private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException { + byte[] commandData = message.getBytes(); + if (logger.isDebugEnabled()) { + logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData)); + } + + write(commandData); + + try { + // Give the sensor some time to handle the command + Thread.sleep(500); + } catch (InterruptedException e) { + logger.warn("Problem while waiting for reading a reply to our command."); + Thread.currentThread().interrupt(); + } + SensorReply reply = readReply(); + // in case there is still another reporting active, we want to discard the sensor data and read the reply to our + // command again + if (reply instanceof SensorMeasuredDataReply) { + reply = readReply(); + } + return reply; + } + + private void write(byte[] commandData) throws IOException { + OutputStream localOutputStream = outputStream; + if (localOutputStream != null) { + localOutputStream.write(commandData); + localOutputStream.flush(); + } + } + + private boolean setWorkingPeriod(byte period) throws IOException { + CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period }); + logger.debug("Sending work period: {}", period); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to setWorkingPeriod command: {}", reply); + if (reply instanceof WorkingPeriodReply) { + WorkingPeriodReply wpReply = (WorkingPeriodReply) reply; + if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) { + return true; + } + } + return false; + } + + private boolean setMode(WorkMode workMode) throws IOException { + byte haveToRequestData = 0; + if (workMode == WorkMode.POLLING) { + haveToRequestData = 1; + } + + CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData }); + logger.debug("Sending mode: {}", workMode); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to setMode command: {}", reply); + if (reply instanceof ModeReply) { + ModeReply mr = (ModeReply) reply; + if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) { + return true; + } + } + return false; + } + + private boolean sendSleep(boolean doSleep) throws IOException { + byte payload = (byte) 1; + if (doSleep) { + payload = (byte) 0; + } + + CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload }); + logger.debug("Sending doSleep: {}", doSleep); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to sendSleep command: {}", reply); + + if (!doSleep) { + // sometimes the sensor does not wakeup on the first attempt, thus we try again + for (int i = 0; reply == null && i < 3; i++) { + reply = sendCommand(m); + logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply); + } + } + + if (reply instanceof SleepReply) { + SleepReply sr = (SleepReply) reply; + if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) { + return true; + } + } + return false; + } + + private boolean getFirmware() throws IOException { + CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {}); + logger.debug("Sending get firmware request"); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to getFirmware command: {}", reply); + + if (reply instanceof SensorFirmwareReply) { + SensorFirmwareReply fwReply = (SensorFirmwareReply) reply; + thingHandler.setFirmware(fwReply.getFirmware()); + return true; + } + return false; + } + + /** + * Request data from the device, they will be returned via the serialEvent callback + * + * @throws IOException + */ + public void requestSensorData() throws IOException { + CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {}); + byte[] data = m.getBytes(); + if (logger.isDebugEnabled()) { + logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data)); + } + write(data); + } + + private @Nullable SensorReply readReply() throws IOException { + byte[] readBuffer = new byte[Constants.REPLY_LENGTH]; + + InputStream localInpuStream = inputStream; + + int b = -1; + if (localInpuStream != null && localInpuStream.available() > 0) { + while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) { + logger.debug("Trying to find first reply byte now..."); + } + readBuffer[0] = (byte) b; + int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1); + if (logger.isDebugEnabled()) { + logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead, + HexUtils.bytesToHex(readBuffer)); + } + return ReplyFactory.create(readBuffer); + } + return null; + } + + /** + * Data from the device is arriving and will be parsed accordingly + */ + @Override + public void serialEvent(SerialPortEvent event) { + if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) { + // we get here if data has been received + SensorReply reply = null; + try { + reply = readReply(); + logger.debug("Got data from sensor: {}", reply); + } catch (IOException e) { + logger.warn("Could not read available data from the serial port: {}", e.getMessage()); + } + if (reply instanceof SensorMeasuredDataReply) { + SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply; + if (sensorData.isValidData()) { + thingHandler.updateChannels(sensorData); + } + } + } + } + + /** + * Shutdown the communication, i.e. send the device to sleep and close the serial port + */ + public void dispose() { + SerialPort localSerialPort = serialPort; + if (localSerialPort != null) { + try { + // send the device to sleep to preserve power and extend the lifetime of the sensor + sendSleep(true); + } catch (IOException e) { + // ignore because we are shutting down anyway + logger.debug("Exception while disposing communicator (will ignore it)", e); + } finally { + localSerialPort.removeEventListener(); + localSerialPort.close(); + serialPort = null; + } + } + + try { + InputStream localInputStream = inputStream; + if (localInputStream != null) { + localInputStream.close(); + } + } catch (IOException e) { + logger.debug("Error while closing the input stream: {}", e.getMessage()); + } + + try { + OutputStream localOutputStream = outputStream; + if (localOutputStream != null) { + localOutputStream.close(); + } + } catch (IOException e) { + logger.debug("Error while closing the output stream: {}", e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java new file mode 100644 index 0000000000000..2b4eaa30a318a --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java @@ -0,0 +1,27 @@ +/** + * 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.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for the different sensor modes + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public enum WorkMode { + REPORTING, + POLLING +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java new file mode 100644 index 0000000000000..d0e0caff34506 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java @@ -0,0 +1,97 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import java.io.ByteArrayOutputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Message to be send to the device + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class CommandMessage { + private static final byte HEAD = -86; // AA + private static final byte COMMAND_ID = -76; // B4 + private static final byte TAIL = -85; // AB + + private static final int DATA_BYTES_AFTER_FIRST_DATA_BYTE = 12; + + private final byte firstDataByte; + private byte[] payLoad = new byte[DATA_BYTES_AFTER_FIRST_DATA_BYTE]; + private byte[] targetDevice = new byte[] { -1, -1 }; // FF FF = all devices + + public CommandMessage(byte command, byte[] payLoad) { + this.firstDataByte = command; + this.payLoad = payLoad; + } + + public CommandMessage(byte command, byte[] payLoad, byte[] targetDevice) { + this.firstDataByte = command; + this.payLoad = payLoad; + this.targetDevice = targetDevice; + } + + /** + * Get the raw bytes to be send out to the device + * + * @return ByteArray containing the bytes for a message to the device + */ + public byte[] getBytes() { + ByteArrayOutputStream message = new ByteArrayOutputStream(19); + + message.write(HEAD); + message.write(COMMAND_ID); + message.write(firstDataByte); + + for (byte b : payLoad) { + message.write(b); + } + int padding = DATA_BYTES_AFTER_FIRST_DATA_BYTE - payLoad.length; + for (int i = 0; i < padding; i++) { + message.write(0x00); + } + + for (byte b : targetDevice) { + message.write(b); + } + message.write(calculateCheckSum(message.toByteArray())); + message.write(TAIL); + + return message.toByteArray(); + } + + private byte calculateCheckSum(byte[] data) { + int checksum = 0; + for (int i = 2; i <= 14; i++) { + checksum += data[i]; + } + checksum = (checksum - 2) % 256; + + return (byte) checksum; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Message: "); + sb.append("Command=" + firstDataByte); + sb.append(" Target Device=" + HexUtils.bytesToHex(targetDevice)); + sb.append(" Payload=" + HexUtils.bytesToHex(payLoad)); + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java new file mode 100644 index 0000000000000..8ed17a7ec04ed --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java @@ -0,0 +1,37 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants for sensor messages + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class Constants { + + private Constants() { + } + + public static final byte MESSAGE_START = (byte) 0xAA; + public static final int MESSAGE_START_AS_INT = 170; + public static final byte MESSAGE_END = (byte) 0xAB; + + public static final int REPLY_LENGTH = 10; + + public static final byte QUERY_ACTION = (byte) 0x00; + public static final byte SET_ACTION = (byte) 0x01; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java new file mode 100644 index 0000000000000..9c52d14807498 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java @@ -0,0 +1,63 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode; + +/** + * Reply from sensor to a set mode command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class ModeReply extends SensorReply { + + private final byte actionType; + private final WorkMode mode; + + public ModeReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + if (bytes[4] == (byte) 1) { + this.mode = WorkMode.POLLING; + } else { + this.mode = WorkMode.REPORTING; + } + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the set work mode + * + * @return work mode set on the sensor + */ + public WorkMode getMode() { + return mode; + } + + @Override + public String toString() { + return "ModeReply: [mode=" + mode + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java new file mode 100644 index 0000000000000..3e15c541c9030 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java @@ -0,0 +1,51 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Data from the sensor containing information about the installed firmware + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorFirmwareReply extends SensorReply { + + private final byte year; + private final byte month; + private final byte day; + + public SensorFirmwareReply(byte[] receivedData) { + super(receivedData); + this.year = receivedData[3]; + this.month = receivedData[4]; + this.day = receivedData[5]; + } + + /** + * Gets the firmware of the sensor as a String + * + * @return firmware of the sensor formatted as YY-MM-DD + */ + public String getFirmware() { + String firmware = year + "-" + month + "-" + day; + return firmware; + } + + @Override + public String toString() { + return "FirmwareReply: [firmware=" + getFirmware() + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java new file mode 100644 index 0000000000000..05ee374e4c4eb --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java @@ -0,0 +1,79 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Class containing the actual measured values from the sensor + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorMeasuredDataReply extends SensorReply { + private final byte pm25lowByte; + private final byte pm25highByte; + private final byte pm10lowByte; + private final byte pm10highByte; + + /** + * Create a new instance by parsing the given 10 bytes. + * + */ + public SensorMeasuredDataReply(byte[] bytes) { + super(bytes); + pm25lowByte = bytes[2]; + pm25highByte = bytes[3]; + pm10lowByte = bytes[4]; + pm10highByte = bytes[5]; + } + + /** + * Check if data is valid by checking header, commanderNo, messageTail and checksum. + */ + public boolean isValidData() { + return header == Constants.MESSAGE_START && commandID == (byte) 0xC0 && messageTail == Constants.MESSAGE_END + && checksum == calculateChecksum(); + } + + /** + * Get the measured PM2.5 value + * + * @return the measured PM2.5 value + */ + public float getPm25() { + int shiftedValue = (pm25highByte << 8 & 0xFF) | pm25lowByte & 0xFF; + return ((float) shiftedValue) / 10; + } + + /** + * Get the measured PM10 value + * + * @return the measured PM10 value + */ + public float getPm10() { + int shiftedValue = (pm10highByte << 8 & 0xFF) | pm10lowByte & 0xFF; + return ((float) shiftedValue) / 10; + } + + @Override + public String toString() { + return String.format( + "SensorMeasuredDataReply: [valid=%s, PM 2.5=%.1f, PM 10=%.1f, sourceDevice=%s, pm25lowHigh=(%s) pm10lowHigh=(%s)]", + isValidData(), getPm25(), getPm10(), HexUtils.bytesToHex(new byte[] { deviceID[0], deviceID[1] }), + HexUtils.bytesToHex(new byte[] { pm25lowByte, pm25highByte }), + HexUtils.bytesToHex(new byte[] { pm10lowByte, pm10highByte })); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java new file mode 100644 index 0000000000000..f2f0d23e916e8 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java @@ -0,0 +1,89 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Base class holding information sent by the sensor to us + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorReply { + + protected final byte header; + protected final byte commandID; + protected final byte[] payLoad; + protected final byte[] deviceID; + protected final byte checksum; + protected final byte messageTail; + + /** + * Creates the container for data received from the sensor + * + * @param bytes the data received from the sensor + * @throws IllegalArgumentException Is thrown if less than 10 bytes are provided. + */ + public SensorReply(byte[] bytes) { + if (bytes.length != 10) { + throw new IllegalArgumentException("was expecting 10 bytes, but received " + bytes.length); + } + this.header = bytes[0]; + this.commandID = bytes[1]; + this.payLoad = Arrays.copyOfRange(bytes, 2, 6); + this.deviceID = Arrays.copyOfRange(bytes, 6, 8); + this.checksum = bytes[8]; + this.messageTail = bytes[9]; + } + + /** + * Gets the commandID byte. However there is the first data byte which holds a kind of "sub command" that has to be + * evaluated too + * + * @return byte representing the commandID + */ + public byte getCommandID() { + return this.commandID; + } + + /** + * Gets the first byte from the data bytes (usually holds the {@link Command}) as a form of some sub command + * + * @return first byte from the data section of a reply + */ + public byte getFirstDataByte() { + return this.payLoad[0]; + } + + protected byte calculateChecksum() { + byte sum = 0; + for (byte b : payLoad) { + sum += b; + } + for (byte b : deviceID) { + sum += b; + } + return sum; + } + + @Override + public String toString() { + return String.format("GeneralReply: [head=%x, commandID=%x, payload=%s, deviceID=%s, checksum=%s, tail=%x", + header, commandID, HexUtils.bytesToHex(payLoad), HexUtils.bytesToHex(deviceID), checksum, messageTail); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java new file mode 100644 index 0000000000000..19b8d250c7ba7 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java @@ -0,0 +1,58 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Reply from sensor to a set sleep command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SleepReply extends SensorReply { + + private final byte actionType; + private final byte sleep; + + public SleepReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + this.sleep = bytes[4]; + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the info whether this is a sleep or wakeup reply + * + * @return 0 = sleep 1 = work + */ + public byte getSleep() { + return sleep; + } + + @Override + public String toString() { + return "SleepReply: [sleep=" + sleep + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java new file mode 100644 index 0000000000000..b882243edfbff --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java @@ -0,0 +1,58 @@ +/** + * 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.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Reply from sensor to a set working period command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class WorkingPeriodReply extends SensorReply { + + private final byte actionType; + private final byte period; + + public WorkingPeriodReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + this.period = bytes[4]; + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the set working period + * + * @return working period set on the sensor + */ + public byte getPeriod() { + return period; + } + + @Override + public String toString() { + return "WorkingPeriodReply: [Period=" + this.period + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..2761c8a57e4e1 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + NovaFineDust Binding + This is the binding for Nova Fitness Fine Dust SDS011 sensor. + Stefan Triller + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..b44f880cf0483 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,61 @@ + + + + + + Nova SDS011 Fine Dust Sensor connected via USB + + + + + + + + + serial-port + + USB port the device is connected to i.e. /dev/ttyUSB0 + + + true + + + + + + Reporting is strongly recommended to increase sensor lifetime + + + 1 + true + + Device will report every x minutes and sleep for x*60 - 30 seconds afterwards, 0 = as fast as possible without sleep + + + 10 + true + + Device will be polled every x seconds (polling is not recommended) + + + + + + + Number:Density + + The PM 2.5 value + + + + + Number:Density + + The PM 10 value + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index e8a0c60f11ed8..a1c41ec3f12ea 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -172,6 +172,7 @@ org.openhab.binding.nibeuplink org.openhab.binding.nikobus org.openhab.binding.nikohomecontrol + org.openhab.binding.novafinedust org.openhab.binding.ntp org.openhab.binding.nuki org.openhab.binding.oceanic From 24e083850ac41cb7e809491f63f79a32ce4308ad Mon Sep 17 00:00:00 2001 From: Sami Salonen Date: Sun, 14 Jun 2020 21:27:53 +0300 Subject: [PATCH 41/83] [fmiweather] FMI Weather Binding initial contribution (#6329) Signed-off-by: Sami Salonen --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.fmiweather/.classpath | 32 + .../org.openhab.binding.fmiweather/.project | 23 + bundles/org.openhab.binding.fmiweather/NOTICE | 15 + .../org.openhab.binding.fmiweather/README.md | 1470 +++++++++++++++++ .../doc/images/fmi-example-things.png | Bin 0 -> 115997 bytes .../org.openhab.binding.fmiweather/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/AbstractWeatherHandler.java | 274 +++ .../fmiweather/internal/BindingConstants.java | 59 + .../internal/ForecastWeatherHandler.java | 210 +++ .../fmiweather/internal/HandlerFactory.java | 60 + .../internal/ObservationWeatherHandler.java | 209 +++ .../fmiweather/internal/client/Client.java | 445 +++++ .../fmiweather/internal/client/Data.java | 59 + .../internal/client/FMIResponse.java | 115 ++ .../fmiweather/internal/client/FMISID.java | 41 + .../internal/client/ForecastRequest.java | 45 + .../fmiweather/internal/client/LatLon.java | 42 + .../fmiweather/internal/client/Location.java | 73 + .../internal/client/ObservationRequest.java | 48 + .../internal/client/QueryParameter.java | 29 + .../fmiweather/internal/client/Request.java | 81 + .../FMIExceptionReportException.java | 38 + .../client/exception/FMIIOException.java | 35 + .../exception/FMIResponseException.java | 35 + .../FMIUnexpectedResponseException.java | 39 + .../internal/discovery/CitiesOfFinland.java | 263 +++ .../discovery/FMIWeatherDiscoveryService.java | 224 +++ .../resources/ESH-INF/binding/binding.xml | 10 + .../resources/ESH-INF/thing/thing-types.xml | 531 ++++++ .../AbstractFMIResponseParsingTest.java | 152 ++ .../binding/fmiweather/FMIRequestTest.java | 76 + .../FMIResponseParsingEmptyTest.java | 51 + ...FMIResponseParsingExceptionReportTest.java | 48 + ...onseParsingInvalidOrUnexpectedXmlTest.java | 42 + .../FMIResponseParsingMultiplePlacesTest.java | 148 ++ .../FMIResponseParsingSinglePlaceTest.java | 100 ++ .../fmiweather/ParsingStationsTest.java | 51 + .../fmiweather/ResponseLocationMatcher.java | 72 + .../binding/fmiweather/TimestampMatcher.java | 68 + .../binding/fmiweather/ValuesMatcher.java | 56 + .../org/openhab/binding/fmiweather/error1.xml | 17 + .../fmiweather/forecast_multiple_places.xml | 185 +++ .../binding/fmiweather/observations_empty.xml | 22 + .../observations_multiple_places.xml | 225 +++ .../fmiweather/observations_single_place.xml | 146 ++ .../openhab/binding/fmiweather/stations.xml | 140 ++ bundles/pom.xml | 1 + 50 files changed, 6137 insertions(+) create mode 100644 bundles/org.openhab.binding.fmiweather/.classpath create mode 100644 bundles/org.openhab.binding.fmiweather/.project create mode 100644 bundles/org.openhab.binding.fmiweather/NOTICE create mode 100644 bundles/org.openhab.binding.fmiweather/README.md create mode 100644 bundles/org.openhab.binding.fmiweather/doc/images/fmi-example-things.png create mode 100644 bundles/org.openhab.binding.fmiweather/pom.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/AbstractWeatherHandler.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/BindingConstants.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/ForecastWeatherHandler.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/HandlerFactory.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/ObservationWeatherHandler.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Client.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Data.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/FMIResponse.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/FMISID.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/ForecastRequest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/LatLon.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Location.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/ObservationRequest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/QueryParameter.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Request.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/exception/FMIExceptionReportException.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/exception/FMIIOException.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/exception/FMIResponseException.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/exception/FMIUnexpectedResponseException.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/discovery/CitiesOfFinland.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/discovery/FMIWeatherDiscoveryService.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/main/resources/ESH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/AbstractFMIResponseParsingTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIRequestTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingEmptyTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingExceptionReportTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingInvalidOrUnexpectedXmlTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingMultiplePlacesTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingSinglePlaceTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ParsingStationsTest.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ResponseLocationMatcher.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/TimestampMatcher.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ValuesMatcher.java create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/error1.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/forecast_multiple_places.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_empty.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_multiple_places.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_single_place.xml create mode 100644 bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/stations.xml diff --git a/CODEOWNERS b/CODEOWNERS index 0e7e051600518..fecb34a485af2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -58,6 +58,7 @@ /bundles/org.openhab.binding.exec/ @kgoderis /bundles/org.openhab.binding.feed/ @svilenvul /bundles/org.openhab.binding.feican/ @Hilbrand +/bundles/org.openhab.binding.fmiweather/ @ssalonen /bundles/org.openhab.binding.folding/ @fa2k /bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand /bundles/org.openhab.binding.freebox/ @lolodomo diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index c4dbcbcf3d9ed..8655b64fbe81e 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -281,6 +281,11 @@ org.openhab.binding.feican ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.fmiweather + ${project.version} + org.openhab.addons.bundles org.openhab.binding.folding diff --git a/bundles/org.openhab.binding.fmiweather/.classpath b/bundles/org.openhab.binding.fmiweather/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.fmiweather/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.fmiweather/.project b/bundles/org.openhab.binding.fmiweather/.project new file mode 100644 index 0000000000000..f8a3dddd324ab --- /dev/null +++ b/bundles/org.openhab.binding.fmiweather/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.fmiweather + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.fmiweather/NOTICE b/bundles/org.openhab.binding.fmiweather/NOTICE new file mode 100644 index 0000000000000..9436446135a35 --- /dev/null +++ b/bundles/org.openhab.binding.fmiweather/NOTICE @@ -0,0 +1,15 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab2-addons + + diff --git a/bundles/org.openhab.binding.fmiweather/README.md b/bundles/org.openhab.binding.fmiweather/README.md new file mode 100644 index 0000000000000..54aa0e14295a7 --- /dev/null +++ b/bundles/org.openhab.binding.fmiweather/README.md @@ -0,0 +1,1470 @@ +# FMI Weather Binding + +This binding integrates to [the Finnish Meteorological Institute (FMI) Open Data API](https://en.ilmatieteenlaitos.fi/open-data). + +Binding provides access to weather observations from FMI weather stations and [HIRLAM weather forecast model](https://en.ilmatieteenlaitos.fi/weather-forecast-models) forecasts. +Forecast covers all of Europe, see previous link for more information. + +![example of things](doc/images/fmi-example-things.png) + +## License + +Finnish Meteorological Institute's open data service uses the Creative Commons Attribution 4.0 International license (CC BY 4.0). +By using the binding, you agree to license terms as explained in [FMI website](https://en.ilmatieteenlaitos.fi/open-data-licence). + +## Supported Things + +There are two supported things: + +- `observation` thing shows current weather observation for a given station. Data is updated automatically every 10 minutes. +- `forecast` thing shows forecasted weather conditions for a location. Data is updated automatically every 20 minutes. + +## Discovery + +The binding automatically discovers weather stations and forecasts for nearby places: + +- `observation` things for nearby weather stations +- `forecast` things for nearby Finnish cities and for the current location + +## Thing Configuration + +Typically there is no need to manually configure things unless you prefer to use textual configuration, or if you want to have observation or forecast for a specific location. + +In case you are using textual configuration, you need to use quotes around text parameters. In PaperUI this is not necessary. + +### `observation` thing configuration + +| Parameter | Type | Required | Description | Example | +| --------- | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `fmisid` | text | ✓ | FMI Station ID. You can FMISID of see all weathers stations at [FMI web site](https://en.ilmatieteenlaitos.fi/observation-stations?p_p_id=stationlistingportlet_WAR_fmiwwwweatherportlets&p_p_lifecycle=0&p_p_state=normal&p_p_mode=view&p_p_col_id=column-4&p_p_col_count=1&_stationlistingportlet_WAR_fmiwwwweatherportlets_stationGroup=WEATHER#station-listing) | `"852678"` for Espoo Nuuksio station | + + +### `forecast` thing configuration + +| Parameter | Type | Required | Description | Example | +| ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | --------------------------------- | +| `location` | text | ✓ | Latitude longitude location for the forecast. The parameter is given in format `LATITUDE,LONGITUDE`. | `"48.864716, 2.349014"` for Paris | + +## Channels + +Observation and forecast things provide slightly different details on weather. + +### `observation` thing channels + +Observation channels are grouped in single group, `current`. + +| Channel ID | Item Type | Description | +| ----------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `time` | `DateTime` | Observation time | +| `temperature` | `Number:Temperature` | Air temperature | +| `humidity` | `Number:Dimensionless` | Relative Humidity | +| `wind-direction` | `Number:Angle` | Wind Direction | +| `wind-speed` | `Number:Speed` | Wind Speed | +| `wind-gust` | `Number:Speed` | Wind Gust Speed | +| `pressure` | `Number:Pressure` | Air pressure | +| `precipitation` | `Number:Length` | Precipitation in one hour | +| `snow-depth` | `Number:Length` | Snow depth | +| `visibility` | `Number:Length` | Visibility | +| `clouds` | `Number:Dimensionless` | Cloudiness. Given as percentage, 0 % being clear skies, and 100 % being overcast. `UNDEF` when cloud coverage could not be determined. | +| `present-weather` | `Number` | Prevailing weather as WMO code 4680. For details, see e.g. [description at Centre for Environmental Data Analysis](https://artefacts.ceda.ac.uk/badc_datadocs/surface/code.html). | + +You can check the exact observation time by using the `time` channel. + +To refer to certain channel, use the normal convention `THING_ID:GROUP_ID#CHANNEL_ID`, e.g. `fmiweather:observation:station_874863_Espoo_Tapiola:current#temperature`. + +### `forecast` thing channels + +Forecast has multiple channel groups, one for each forecasted time. The groups are named as follows: + +- `forecastNow`: Forecasted weather for the current time +- `forecastHours01`: Forecasted weather for 1 hours from now +- `forecastHours02`: Forecasted weather for 2 hours from now +- etc. +- `forecastHours50`: Forecasted weather for 50 hours from now + +You can check the exact forecast time by using the `time` channel. + +Since forecasts are updated at certain times of the day, the last forecast values might be unavailable (`UNDEF`). Typically forecasts between now and 44 hours should be available at all times. + +| Channel ID | Item Type | Description | +| ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `time` | `DateTime` | Date of data forecasted | +| `temperature` | `Number:Temperature` | Forecasted air temperature | +| `humidity` | `Number:Dimensionless` | Forecasted relative Humidity | +| `wind-direction` | `Number:Angle` | Forecasted wind Direction | +| `wind-speed` | `Number:Speed` | Forecasted wind Speed | +| `wind-gust` | `Number:Speed` | Forecasted wind Gust Speed | +| `pressure` | `Number:Pressure` | Forecasted air pressure | +| `precipitation-intensity` | `Number:Speed` | Forecasted precipitation intensity at the forecast time in mm/h | +| `total-cloud-cover` | `Number:Dimensionless` | Forecasted total cloud cover as percentage | +| `weather-id` | `Number` | Number indicating forecasted weather condition. Corresponds to `WeatherSymbol3` parameter. For descriptions in Finnish, see [FMI web site](https://ilmatieteenlaitos.fi/latauspalvelun-pikaohje). | + +To refer to certain channel, use the normal convention `THING_ID:GROUP_ID#CHANNEL_ID`, e.g. `fmiweather:forecast:ParisForecast:forecastHours06#wind-speed`. + +## Unit Conversion + +Please use the [Units Of Measurement](https://www.openhab.org/docs/concepts/units-of-measurement.html) concept of openHAB for unit conversion which is fully supported by this binding. + +## Full Example + +### Things + +`fmi.things`: + +``` +Thing fmiweather:observation:station_Helsinki_Kumpula "Helsinki Kumpula Observation" [fmisid="101004"] +Thing fmiweather:forecast:forecast_Paris "Paris Forecast" [location="48.864716, 2.349014"] +``` + +### Items + +`observation.items`: + + + +``` +DateTime HelsinkiObservationTime "Observation Time [%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS]" + + org.openhab.addons.bundles + org.openhab.binding.smhi + ${project.version} + org.openhab.addons.bundles org.openhab.binding.snmp diff --git a/bundles/org.openhab.binding.smhi/.classpath b/bundles/org.openhab.binding.smhi/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.smhi/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.smhi/.project b/bundles/org.openhab.binding.smhi/.project new file mode 100644 index 0000000000000..8f332b762a0bb --- /dev/null +++ b/bundles/org.openhab.binding.smhi/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.smhi + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.smhi/NOTICE b/bundles/org.openhab.binding.smhi/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.smhi/README.md b/bundles/org.openhab.binding.smhi/README.md new file mode 100644 index 0000000000000..5eb57073fb8ce --- /dev/null +++ b/bundles/org.openhab.binding.smhi/README.md @@ -0,0 +1,144 @@ +# Smhi Binding + +This binding gets hourly and daily forecast from SMHI - the Swedish Meteorological and Hydrological Institute. +It can get forecasts for the nordic countries (Sweden, Norway, Denmark and Finland). + +## Supported Things + +The binding support only one thing-type: forecast. +The thing can be configured to get hourly forecasts for up to 24 hours, and daily forecasts for up to 10 days. + + +## Discovery + +This binding does not support automatic discovery. + +## Thing Configuration + +The forecast thing needs to be configured with the latitude and longitude for the location of the forecast. +You can also choose for which hours and which days you would like to get forecasts. + +| Parameter | Description | Required | +|-----------|-------------|----------| +| Latitude | Latitude of the forecast | Yes | +| Longitute | Longitude of the forecast | Yes | +| Hourly forecasts | The hourly forecasts to display | No | +| Daily forecasts | The daily forecasts to display | No | + +## Channels + +The channels are the same for all forecasts: + +#### Basic channels + +| channel | type | description | +|----------|--------|------------------------------| +| Temperature | Number:Temperature | Temperature in Celsius | +| Wind direction | Number:Angle | Wind direction in degrees | +| Wind Speed | Number:Speed | Wind speed in m/s | +| Wind gust speed | Number:Speed | Wind gust speed in m/s | +| Minimum precipitation | Number:Speed | Minimum precipitation intensity in mm/h | +| Maximum precipitation | Number:Speed | Maximum precipitation intensity in mm/h | +| Precipitation category* | Number | Type of precipitation | +| Air pressure | Number:Pressure | Air pressure in hPa | +| Relative humidity | Number:Dimensionless | Relative humidity in percent | +| Total cloud cover | Number:Dimensionless | Mean value of total cloud cover in percent | +| Weather condition** | Number | Short description of the weather conditions | + +#### Advanced channels + +| channel | type | description | +|----------|--------|------------------------------| +| Visibility | Number:Length | Horizontal visibility in km | +| Thunder probability | Number:Dimensionless | Probability of thunder in percent | +| Frozen precipitation | Number:Dimensionless | Percent of precipitation in frozen form (will be set to UNDEF if there's no precipitation) | +| Low level cloud cover | Number:Dimensionless | Mean value of low level cloud cover (0-2500 m) in percent | +| Medium level cloud cover | Number:Dimensionless | Mean value of medium level cloud cover (2500-6000 m) in percent | +| High level cloud cover | Number:Dimensionless | Mean value of high level cloud cover (> 6000 m) in percent | +| Mean precipitation | Number:Speed | Mean precipitation intensity in mm/h | +| Median precipitation | Number:Speed | Median precipitation intensity in mm/h | + +\* The precipitation category can have a value from 0-6, representing different types of precipitaion: + +| Value | Meaning | +|-------|---------| +| 0 | No precipitation| +| 1 | Snow | +| 2 | Snow and rain | +| 3 | Rain | +| 4 | Drizzle | +| 5 | Freezing rain | +| 6 | Freezing drizzle | + +\** The weather condition channel can take values from 1-27, each corresponding to a different weather condition: + +| Value | Condition | +|-------|-----------| +| 1 | Clear sky | +| 2 | Nearly clear sky | +| 3 | Variable cloudiness | +| 4 | Halfclear sky | +| 5 | Cloudy sky | +| 6 | Overcast | +| 7 | Fog | +| 8 | Light rain showers | +| 9 | Moderate rain showers | +| 10 | Heavy rain showers | +| 11 | Thunderstorm | +| 12 | Light sleet showers | +| 13 | Moderate sleet showers | +| 14 | Heavy sleet showers | +| 15 | Light snow showers | +| 16 | Moderate snow showers | +| 17 | Heavy snow showers | +| 18 | Light rain | +| 19 | Moderate rain | +| 20 | Heavy rain | +| 21 | Thunder | +| 22 | Light sleet | +| 23 | Moderate sleet | +| 24 | Heavy sleet | +| 25 | Light snowfall | +| 26 | Moderate snowfall | +| 27 | Heavy snowfall | + + +## Full Example + +demo.things + +``` +Thing smhi:forecast:demoforecast "Demo forecast" [ latitude=57.997072, longitude=15.990068, hourlyForecasts=0,1,2, dailyForecasts=0,1 ] +``` + +demo.items + +``` +Number:Temperature Smhi_Temperature_Now "Current temperature [%.1f °C]" {channel="smhi:forecast:demoforecast:hour_0#t"} +Number:Speed Smhi_Min_Precipitation_Now "Current precipitation (min) [%.1f mm/h]" {channel="smhi:forecast:demoforecast:hour_0#pmin"} + +Number:Temperature Smhi_Temperature_1hour "Temperature next hour [%.1f °C]" {channel="smhi:forecast:demoforecast:hour_1#t"} +Number:Speed Smhi_Min_Precipitation_1hour "Precipitaion next hour (min) [%.1f mm/h]" {channel="smhi:forecast:demoforecast:hour_1#pmin"} + +Number:Temperature Smhi_Temperature_Tomorrow "Temperature tomorrow [%.1f °C]" {channel="smhi:forecast:demoforecast:day_1#t"} +Number:Speed Smhi_Min_Precipitation_Tomorrow "Precipitaion tomorrow (min) [%.1f mm/h]" {channel="smhi:forecast:demoforecast:hour_1#pmin"} +``` + +demo.sitemap + +``` +sitemap demo label="Smhi" { + Frame label="Current weather" { + Text item=Smhi_Temperature_Now + Text item=Smhi_Min_Precipitation_Now + } + Frame label="Weather next hour" { + Text item=Smhi_Temperature_1hour + Text item=Smhi_Min_Precipitation_1hour + } + Frame label="Weather tomorrow" { + Text item=Smhi_Temperature_Tomorrow + Text item=Smhi_Min_Precipitation_Tomorrow + } +} +``` diff --git a/bundles/org.openhab.binding.smhi/pom.xml b/bundles/org.openhab.binding.smhi/pom.xml new file mode 100644 index 0000000000000..d813dce2f265b --- /dev/null +++ b/bundles/org.openhab.binding.smhi/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.6-SNAPSHOT + + + org.openhab.binding.smhi + + openHAB Add-ons :: Bundles :: Smhi Binding + + diff --git a/bundles/org.openhab.binding.smhi/src/main/feature/feature.xml b/bundles/org.openhab.binding.smhi/src/main/feature/feature.xml new file mode 100644 index 0000000000000..ce69cfc5799d8 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.smhi/${project.version} + + diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Forecast.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Forecast.java new file mode 100644 index 0000000000000..0661e853af889 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Forecast.java @@ -0,0 +1,54 @@ +/** + * 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.smhi.internal; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A class containing a forecast for a specific point in time. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class Forecast implements Comparable { + private final ZonedDateTime validTime; + private final Map parameters; + + public Forecast(ZonedDateTime validTime, Map parameters) { + this.validTime = validTime; + this.parameters = parameters; + } + + public ZonedDateTime getValidTime() { + return validTime; + } + + public Map getParameters() { + return parameters; + } + + public @Nullable BigDecimal getParameter(String parameter) { + return parameters.get(parameter); + } + + @Override + public int compareTo(Forecast o) { + return validTime.compareTo(o.validTime); + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Parser.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Parser.java new file mode 100644 index 0000000000000..c8baa5b61466d --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/Parser.java @@ -0,0 +1,97 @@ +/** + * 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.smhi.internal; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Class with static methods for parsing json strings returned from Smhi + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class Parser { + + private static JsonParser parser = new JsonParser(); + + /** + * Parse a json string received from Smhi containing forecasts. + * + * @param json A json string + * @return A {@link TimeSeries} object + */ + public static TimeSeries parseTimeSeries(String json) { + ZonedDateTime referenceTime; + JsonObject object = parser.parse(json).getAsJsonObject(); + + referenceTime = parseApprovedTime(json); + JsonArray timeSeries = object.get("timeSeries").getAsJsonArray(); + + List forecasts = StreamSupport.stream(timeSeries.spliterator(), false) + .map(element -> parseForecast(element.getAsJsonObject())) + .sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + + return new TimeSeries(referenceTime, forecasts); + } + + /** + * Parse a json string containing the approved time and reference time of the latest forecast + * + * @param json A json string + * @return {@link ZonedDateTime} of the reference time + */ + public static ZonedDateTime parseApprovedTime(String json) { + JsonObject timeObj = parser.parse(json).getAsJsonObject(); + + return ZonedDateTime.parse(timeObj.get("referenceTime").getAsString()); + } + + /** + * Parse a single forecast, i.e. a forecast for a specific time. + * + * @param object + * @return + */ + private static Forecast parseForecast(JsonObject object) { + ZonedDateTime validTime = ZonedDateTime.parse(object.get("validTime").getAsString()); + Map parameters = new HashMap<>(); + + JsonArray parameterArray = object.get("parameters").getAsJsonArray(); + + parameterArray.forEach(element -> { + JsonObject parameterObj = element.getAsJsonObject(); + String name = parameterObj.get("name").getAsString().toLowerCase(Locale.ROOT); + BigDecimal value = parameterObj.get("values").getAsJsonArray().get(0).getAsBigDecimal(); + + parameters.put(name, value); + }); + + return new Forecast(validTime, parameters); + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/PointOutOfBoundsException.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/PointOutOfBoundsException.java new file mode 100644 index 0000000000000..3a1a85018896b --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/PointOutOfBoundsException.java @@ -0,0 +1,26 @@ +/** + * 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.smhi.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class PointOutOfBoundsException extends Exception { + + private static final long serialVersionUID = 546566512L; + +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiBindingConstants.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiBindingConstants.java new file mode 100644 index 0000000000000..4da68906d874d --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiBindingConstants.java @@ -0,0 +1,73 @@ +/** + * 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.smhi.internal; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link SmhiBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class SmhiBindingConstants { + + public static final String BINDING_ID = "smhi"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_FORECAST = new ThingTypeUID(BINDING_ID, "forecast"); + + // Smhi's ids for parameters, also used as channel ids + public static final String PRESSURE = "msl"; + public static final String TEMPERATURE = "t"; + public static final String VISIBILITY = "vis"; + public static final String WIND_DIRECTION = "wd"; + public static final String WIND_SPEED = "ws"; + public static final String RELATIVE_HUMIDITY = "r"; + public static final String THUNDER_PROBABILITY = "tstm"; + public static final String TOTAL_CLOUD_COVER = "tcc_mean"; + public static final String LOW_CLOUD_COVER = "lcc_mean"; + public static final String MEDIUM_CLOUD_COVER = "mcc_mean"; + public static final String HIGH_CLOUD_COVER = "hcc_mean"; + public static final String GUST = "gust"; + public static final String PRECIPITATION_MIN = "pmin"; + public static final String PRECIPITATION_MAX = "pmax"; + public static final String PRECIPITATION_MEAN = "pmean"; + public static final String PRECIPITATION_MEDIAN = "pmedian"; + public static final String PERCENT_FROZEN = "spp"; + public static final String PRECIPITATION_CATEGORY = "pcat"; + public static final String WEATHER_SYMBOL = "wsymb2"; + + public static final List CHANNEL_IDS = Collections + .unmodifiableList(Stream + .of(PRESSURE, TEMPERATURE, VISIBILITY, WIND_DIRECTION, WIND_SPEED, RELATIVE_HUMIDITY, + THUNDER_PROBABILITY, TOTAL_CLOUD_COVER, LOW_CLOUD_COVER, MEDIUM_CLOUD_COVER, + HIGH_CLOUD_COVER, GUST, PRECIPITATION_MIN, PRECIPITATION_MAX, PRECIPITATION_MEAN, + PRECIPITATION_MEDIAN, PERCENT_FROZEN, PRECIPITATION_CATEGORY, WEATHER_SYMBOL) + .collect(Collectors.toList())); + + public static final String BASE_URL = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/"; + public static final String APPROVED_TIME_URL = BASE_URL + "approvedtime.json"; + public static final String POINT_FORECAST_URL = BASE_URL + + "geotype/point/lon/%.6f/lat/%.6f/data.json"; + + public static final BigDecimal OCTAS_TO_PERCENT = BigDecimal.valueOf(12.5); +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConfiguration.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConfiguration.java new file mode 100644 index 0000000000000..7697ff97e5159 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConfiguration.java @@ -0,0 +1,31 @@ +/** + * 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.smhi.internal; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SmhiConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class SmhiConfiguration { + public double latitude; + public double longitude; + public @Nullable List hourlyForecasts; + public @Nullable List dailyForecasts; +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConnector.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConnector.java new file mode 100644 index 0000000000000..5c77f49e2119d --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiConnector.java @@ -0,0 +1,99 @@ +/** + * 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.smhi.internal; + +import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*; + +import java.time.ZonedDateTime; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for handling http requests to Smhi's API and return values. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class SmhiConnector { + + private final Logger logger = LoggerFactory.getLogger(SmhiConnector.class); + + private static final String ACCEPT = "application/json"; + + private final HttpClient httpClient; + + public SmhiConnector(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Get the reference time (the time when the forecast starts) of the latest published forecast + * + * @return A {@link ZonedDateTime} with the time of the latest forecast. + */ + public ZonedDateTime getReferenceTime() throws SmhiException { + logger.debug("Fetching reference time"); + Request req = httpClient.newRequest(APPROVED_TIME_URL); + req.accept(ACCEPT); + ContentResponse resp; + try { + resp = req.send(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new SmhiException(e); + } + logger.debug("Received response with status {} - {}", resp.getStatus(), resp.getReason()); + if (resp.getStatus() == 200) { + return Parser.parseApprovedTime(resp.getContentAsString()); + } else { + throw new SmhiException(resp.getReason()); + } + } + + /** + * Get a forecast for the specified WGS84 coordinates. + * + * @param lat Latitude + * @param lon Longitude + * @return A {@link TimeSeries} object containing the published forecasts. + */ + public TimeSeries getForecast(double lat, double lon) throws SmhiException, PointOutOfBoundsException { + logger.debug("Fetching new forecast"); + String url = String.format(POINT_FORECAST_URL, lon, lat); + Request req = httpClient.newRequest(url); + req.accept(ACCEPT); + ContentResponse resp; + try { + resp = req.send(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new SmhiException(e); + } + logger.debug("Received response with status {} - {}", resp.getStatus(), resp.getReason()); + switch (resp.getStatus()) { + case 200: + return Parser.parseTimeSeries(resp.getContentAsString()); + case 400: + case 404: + throw new PointOutOfBoundsException(); + default: + throw new SmhiException(resp.getReason()); + } + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiException.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiException.java new file mode 100644 index 0000000000000..11373e4c22adc --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiException.java @@ -0,0 +1,33 @@ +/** + * 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.smhi.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class SmhiException extends Exception { + + private static final long serialVersionUID = 516565331L; + + public SmhiException(String message) { + super(message); + } + + public SmhiException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandler.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandler.java new file mode 100644 index 0000000000000..1031913c403ed --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandler.java @@ -0,0 +1,443 @@ +/** + * 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.smhi.internal; + +import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*; + +import java.math.BigDecimal; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.MetricPrefix; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelGroupUID; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SmhiHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class SmhiHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(SmhiHandler.class); + + private SmhiConfiguration config = new SmhiConfiguration(); + + private final HttpClient httpClient; + private @Nullable SmhiConnector connection; + private ZonedDateTime currentHour; + private ZonedDateTime currentDay; + private @Nullable TimeSeries cachedTimeSeries; + private boolean hasLatestForecast = false; + private @Nullable Future forecastUpdater; + private @Nullable Future instantUpdate; + + public SmhiHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.httpClient = httpClient; + this.currentHour = calculateCurrentHour(); + this.currentDay = calculateCurrentDay(); + } + + /** + * Handles commands sent to channels. Since all values are read-only, only REFRESH commands are allowed. + * Sending REFRESH to any item updates all items, since all values are returned in the response from Smhi. + * Therefore there's a wait of 5 seconds before the values are fetched, in which time all other commands are + * blocked, to prevent spamming Smhi's API. + * + * @param channelUID + * @param command + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateNow(); + } + } + + @Override + public void initialize() { + config = getConfigAs(SmhiConfiguration.class); + + connection = new SmhiConnector(httpClient); + + // Check which channel groups are selected in the config. + List channels = new ArrayList<>(); + channels.addAll(createChannels()); + updateThing(editThing().withChannels(channels).build()); + + startPolling(); + updateNow(); + } + + /** + * Start polling for updated weather forecast. + */ + private synchronized void startPolling() { + logger.debug("Start polling"); + forecastUpdater = scheduler.scheduleWithFixedDelay(this::waitForForecast, 1, 1, TimeUnit.MINUTES); + } + + /** + * Cancels all jobs. + */ + private synchronized void cancelPolling() { + logger.debug("Cancelling polling"); + Future localRef = forecastUpdater; + if (localRef != null) { + localRef.cancel(false); + } + localRef = instantUpdate; + if (localRef != null) { + localRef.cancel(false); + } + } + + /** + * Update channels with new forecast data. + * + * @param timeSeries A {@link TimeSeries} object containing forecasts. + */ + private void updateChannels(TimeSeries timeSeries) { + // Loop through hourly forecasts and update those available + for (int i = 0; i < 25; i++) { + List channels = thing.getChannelsOfGroup("hour_" + i); + if (channels.isEmpty()) { + continue; + } + Forecast forecast = timeSeries.getForecast(i); + if (forecast != null) { + channels.forEach(c -> { + String id = c.getUID().getIdWithoutGroup(); + BigDecimal value = forecast.getParameter(id); + updateChannel(c, value); + }); + } + } + // Loop through daily forecasts and updates those available + for (int i = 0; i < 10; i++) { + List channels = thing.getChannelsOfGroup("day_" + i); + if (channels.isEmpty()) { + continue; + } + + int offset = 24 * i + 12; + Forecast forecast = timeSeries.getForecast(currentDay, offset); + + if (forecast == null) { + if (logger.isDebugEnabled()) { + logger.debug("No forecast yet for {}", currentDay.plusHours(offset)); + } + channels.forEach(c -> { + updateState(c.getUID(), UnDefType.NULL); + }); + } else { + channels.forEach(c -> { + String id = c.getUID().getIdWithoutGroup(); + BigDecimal value = forecast.getParameter(id); + updateChannel(c, value); + }); + } + } + } + + private void updateChannel(Channel channel, @Nullable BigDecimal value) { + String id = channel.getUID().getIdWithoutGroup(); + State newState = UnDefType.NULL; + + if (value != null) { + switch (id) { + case PRESSURE: + newState = new QuantityType<>(value, MetricPrefix.HECTO(SIUnits.PASCAL)); + break; + case TEMPERATURE: + newState = new QuantityType<>(value, SIUnits.CELSIUS); + break; + case VISIBILITY: + newState = new QuantityType<>(value, MetricPrefix.KILO(SIUnits.METRE)); + break; + case WIND_DIRECTION: + newState = new QuantityType<>(value, SmartHomeUnits.DEGREE_ANGLE); + break; + case WIND_SPEED: + case GUST: + newState = new QuantityType<>(value, SmartHomeUnits.METRE_PER_SECOND); + break; + case RELATIVE_HUMIDITY: + case THUNDER_PROBABILITY: + newState = new QuantityType<>(value, SmartHomeUnits.PERCENT); + break; + case PERCENT_FROZEN: + // Smhi returns -9 for spp if there's no precipitation, convert to UNDEF + if (value.intValue() == -9) { + newState = UnDefType.UNDEF; + } else { + newState = new QuantityType<>(value, SmartHomeUnits.PERCENT); + } + break; + case HIGH_CLOUD_COVER: + case MEDIUM_CLOUD_COVER: + case LOW_CLOUD_COVER: + case TOTAL_CLOUD_COVER: + newState = new QuantityType<>(value.multiply(OCTAS_TO_PERCENT), SmartHomeUnits.PERCENT); + break; + case PRECIPITATION_MAX: + case PRECIPITATION_MEAN: + case PRECIPITATION_MEDIAN: + case PRECIPITATION_MIN: + newState = new QuantityType<>(value, SmartHomeUnits.MILLIMETRE_PER_HOUR); + break; + default: + newState = new DecimalType(value); + } + } + + updateState(channel.getUID(), newState); + } + + /** + * Dispose the {@link org.eclipse.smarthome.core.thing.binding.ThingHandler}. Cancel scheduled jobs + */ + public void dispose() { + cancelPolling(); + } + + /** + * First check if the time has shifted to a new hour, then start checking if a new forecast have been + * published, in that case, fetch it and update channels. + */ + private void waitForForecast() { + if (isItNewHour()) { + currentHour = calculateCurrentHour(); + currentDay = calculateCurrentDay(); + // Update channels with cached forecasts - just shift an hour forward + TimeSeries forecast = cachedTimeSeries; + if (forecast != null) { + updateChannels(forecast); + } + hasLatestForecast = false; + } + if (!hasLatestForecast && isForecastUpdated()) { + getUpdatedForecast(); + } + } + + /** + * Schedules an imminent update, making it wait 5 seconds to catch any bursts of calls before executing. + */ + private synchronized void updateNow() { + Future localRef = instantUpdate; + if (localRef == null || localRef.isDone()) { + instantUpdate = scheduler.schedule(this::getUpdatedForecast, 5, TimeUnit.SECONDS); + } else { + logger.debug("Already waiting for scheduled refresh"); + } + } + + /** + * Checks if it is a new hour. + * + * @return true if the current time is more than one hour after currentHour, otherwise false. + */ + private boolean isItNewHour() { + return ZonedDateTime.now().minusHours(1).isAfter(currentHour); + } + + /** + * Call Smhi's endpoint to check for the time of the last forecast, to see if a new one is available. + * + * @return true if the time of the latest forecast is equal to or after currentHour, otherwise false + */ + private boolean isForecastUpdated() { + ZonedDateTime referenceTime; + SmhiConnector apiConnection = connection; + if (apiConnection != null) { + try { + referenceTime = apiConnection.getReferenceTime(); + } catch (SmhiException e) { + return false; + } + return referenceTime.isEqual(currentHour) || referenceTime.isAfter(currentHour); + } + return false; + } + + /** + * Fetches latest forecast from Smhi, update channels and check if it was published in the current hour. + * If it is, set flag to indicate we have the latest forecast. + */ + private void getUpdatedForecast() { + TimeSeries forecast; + ZonedDateTime referenceTime; + SmhiConnector apiConnection = connection; + if (apiConnection != null) { + try { + forecast = apiConnection.getForecast(config.latitude, config.longitude); + } catch (SmhiException e) { + String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); + logger.debug("Failed to get new forecast: {}", message); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + return; + } catch (PointOutOfBoundsException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Coordinates outside valid area"); + cancelPolling(); + return; + } + updateStatus(ThingStatus.ONLINE); + referenceTime = forecast.getReferenceTime(); + updateChannels(forecast); + if (referenceTime.isEqual(currentHour) || referenceTime.isAfter(currentHour)) { + hasLatestForecast = true; + } + cachedTimeSeries = forecast; + } + } + + /** + * Get the current time rounded down to hour + * + * @return A {@link ZonedDateTime} corresponding to the last even hour + */ + private ZonedDateTime calculateCurrentHour() { + ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + int y = now.getYear(); + int m = now.getMonth().getValue(); + int d = now.getDayOfMonth(); + int h = now.getHour(); + return ZonedDateTime.of(y, m, d, h, 0, 0, 0, ZoneOffset.UTC); + } + + /** + * Get the current time rounded down to day + * + * @return A {@link ZonedDateTime} corresponding to the last even day. + */ + private ZonedDateTime calculateCurrentDay() { + ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + int y = now.getYear(); + int m = now.getMonth().getValue(); + int d = now.getDayOfMonth(); + return ZonedDateTime.of(y, m, d, 0, 0, 0, 0, ZoneOffset.UTC); + } + + /** + * Creates channels based on selections in thing configuration + * + * @return + */ + private List createChannels() { + List channels = new ArrayList<>(); + + // There's currently a bug in PaperUI that can cause options to be added more than one time + // to the list. Convert to a sorted set to work around this. + // See https://github.com/openhab/openhab-webui/issues/212 + Set hours = new TreeSet<>(); + Set days = new TreeSet<>(); + if (config.hourlyForecasts != null) { + hours.addAll(config.hourlyForecasts); + } + if (config.dailyForecasts != null) { + days.addAll(config.dailyForecasts); + } + + for (int i : hours) { + ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "hour_" + i); + CHANNEL_IDS.forEach(id -> { + channels.add(createChannel(groupUID, id)); + }); + } + + for (int i : days) { + ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "day_" + i); + CHANNEL_IDS.forEach(id -> { + channels.add(createChannel(groupUID, id)); + }); + } + return channels; + } + + /** + * Create a channel with the correct item type based on the channel ID + * + * @param channelGroupUID Channel group the channel belongs to + * @param channelID ID of the channel (without group ID) + * @return The created channel + */ + private Channel createChannel(ChannelGroupUID channelGroupUID, String channelID) { + ChannelUID channelUID = new ChannelUID(channelGroupUID, channelID); + String itemType = "Number"; + switch (channelID) { + case TEMPERATURE: + itemType += ":Temperature"; + break; + case PRESSURE: + itemType += ":Pressure"; + break; + case VISIBILITY: + itemType += ":Length"; + break; + case WIND_DIRECTION: + itemType += ":Angle"; + case WIND_SPEED: + case GUST: + case PRECIPITATION_MAX: + case PRECIPITATION_MEAN: + case PRECIPITATION_MEDIAN: + case PRECIPITATION_MIN: + itemType += ":Speed"; + break; + case RELATIVE_HUMIDITY: + case PERCENT_FROZEN: + case TOTAL_CLOUD_COVER: + case HIGH_CLOUD_COVER: + case MEDIUM_CLOUD_COVER: + case LOW_CLOUD_COVER: + case THUNDER_PROBABILITY: + itemType += ":Dimensionless"; + break; + + } + Channel channel = ChannelBuilder.create(channelUID, itemType) + .withType(new ChannelTypeUID(BINDING_ID, channelID)).build(); + return channel; + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandlerFactory.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandlerFactory.java new file mode 100644 index 0000000000000..b3a4c6e444b42 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/SmhiHandlerFactory.java @@ -0,0 +1,70 @@ +/** + * 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.smhi.internal; + +import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SmhiHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.smhi", service = ThingHandlerFactory.class) +public class SmhiHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_FORECAST); + + private @NonNullByDefault({}) HttpClient httpClient; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_FORECAST.equals(thingTypeUID)) { + return new SmhiHandler(thing, httpClient); + } + + return null; + } + + @Reference + protected void setHttpClient(HttpClientFactory clientFactory) { + this.httpClient = clientFactory.getCommonHttpClient(); + } + + protected void unSetHttpClient(HttpClientFactory clientFactory) { + this.httpClient = null; + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/TimeSeries.java b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/TimeSeries.java new file mode 100644 index 0000000000000..89210cc547fc1 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/java/org/openhab/binding/smhi/internal/TimeSeries.java @@ -0,0 +1,93 @@ +/** + * 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.smhi.internal; + +import java.time.ZonedDateTime; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A collection class with utility methods to retrieve forecasts pertaining to a specified time. + * + * @author Anders Alfredsson - Initial contribution + */ +@NonNullByDefault +public class TimeSeries implements Iterable { + + private final ZonedDateTime referenceTime; + private final List forecasts; + + public TimeSeries(ZonedDateTime referenceTime, List forecasts) { + this.referenceTime = referenceTime; + this.forecasts = forecasts; + } + + public ZonedDateTime getReferenceTime() { + return referenceTime; + } + + /** + * Retrieves the first {@link Forecast} that is equal to or after offset time (from now). + * + * @param hourOffset number of hours after now. + * @return + */ + public @Nullable Forecast getForecast(int hourOffset) { + return getForecast(ZonedDateTime.now(), hourOffset); + } + + /** + * Retrieves the first {@link Forecast} that is equal to or after the offset time (from startTime). + * + * @param hourOffset number of hours after now. + * @return + */ + public @Nullable Forecast getForecast(ZonedDateTime startTime, int hourOffset) { + if (hourOffset < 0) { + throw new IllegalArgumentException("Offset must be at least 0"); + } + + for (Forecast forecast : forecasts) { + if (forecast.getValidTime().compareTo(startTime.plusHours(hourOffset)) >= 0) { + return forecast; + } + } + return null; + } + + @Override + public Iterator iterator() { + return forecasts.iterator(); + } + + @Override + public void forEach(@Nullable Consumer action) { + if (action == null) { + throw new IllegalArgumentException(); + } + for (Forecast f : forecasts) { + action.accept(f); + } + } + + @Override + public Spliterator spliterator() { + return forecasts.spliterator(); + } +} diff --git a/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..c36d2302bbac0 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + SMHI Binding + Binding for getting weather forecasts from the Swedish Meteorological and Hydrological Institute (SMHI) + Anders Alfredsson + + diff --git a/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/config/forecast-config.xml b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/config/forecast-config.xml new file mode 100644 index 0000000000000..b44b88d015ff4 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/config/forecast-config.xml @@ -0,0 +1,68 @@ + + + + + + + Latitude for the forecast + + + + Longitude for the forecast + + + + The hourly forecasts to display + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The daily forecasts to display + true + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/channel-types.xml new file mode 100644 index 0000000000000..214b4c3bb5497 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/channel-types.xml @@ -0,0 +1,214 @@ + + + + + Number:Pressure + + Air pressure in hPa + + + + Number:Temperature + + Temperature + + + + Number:Length + + Horizontal visibility + + + + Number:Angle + + Wind direction + + + + Number:Speed + + Wind speed + + + + Number:Dimensionless + + Relative humidity in percent + + + + Number:Dimensionless + + Probability of thunder in percent + + + + Number:Dimensionless + + Mean value of total cloud cover in percent + + + + Number:Dimensionless + + Mean value of low level cloud cover (0-2500 m) in percent + + + + Number:Dimensionless + + Mean value of medium level cloud cover (2500-6000 m) in percent + + + + Number:Dimensionless + + Mean value of high level cloud cover (> 6000 m) in percent + + + + Number:Speed + + Wind gust speed + + + + Number:Speed + + Minimum precipitation intensity + + + + Number:Speed + + Maximum precipitation intensity + + + + Number:Speed + + Mean precipitation intensity + + + + Number:Speed + + Median precipitation intensity + + + + Number + + Type of precipitation + + + + + + + + + + + + + + Number:Dimensionless + + Percent of precipitation in frozen form + + + + Number + + Short description of the weather conditions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hourly forecast for the specified offset + + + + + + + + + + + + + + + + + + + + + + + + + + Forecast at noon for the specified offset + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..71ddcc65f5f93 --- /dev/null +++ b/bundles/org.openhab.binding.smhi/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,157 @@ + + + + + + Gets weather forecasts from SMHI + + + + + Forecast for the current hour + + + + Forecast for the next hour + + + + Forecast for 2 hours from now + + + + Forecast for 3 hours from now + + + + Forecast for 4 hours from now + + + + Forecast for 5 hours from now + + + + Forecast for 6 hours from now + + + + Forecast for 7 hours from now + + + + Forecast for 8 hours from now + + + + Forecast for 9 hours from now + + + + Forecast for 10 hours from now + + + + Forecast for 11 hours from now + + + + Forecast for 12 hours from now + + + + Forecast for 13 hours from now + + + + Forecast for 14 hours from now + + + + Forecast for 15 hours from now + + + + Forecast for 16 hours from now + + + + Forecast for 17 hours from now + + + + Forecast for 18 hours from now + + + + Forecast for 19 hours from now + + + + Forecast for 20 hours from now + + + + Forecast for 21 hours from now + + + + Forecast for 22 hours from now + + + + Forecast for 23 hours from now + + + + Forecast for 24 hours from now + + + + + Forecast for today + + + + Forecast for tomorrow + + + + Forecast for 2 days from now + + + + Forecast for 3 days from now + + + + Forecast for 4 days from now + + + + Forecast for 5 days from now + + + + Forecast for 6 days from now + + + + Forecast for 7 days from now + + + + Forecast for 8 days from now + + + + Forecast for 9 days from now + + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 4ec92fdb13160..b01a8236568ab 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -220,6 +220,7 @@ org.openhab.binding.sleepiq org.openhab.binding.smaenergymeter org.openhab.binding.smartmeter + org.openhab.binding.smhi org.openhab.binding.snmp org.openhab.binding.solaredge org.openhab.binding.solarlog From e428407d2d6d65bb963ee77a3230b4dc2dc7fda8 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Tue, 16 Jun 2020 20:57:53 +0200 Subject: [PATCH 48/83] [amazonechocontrol] fix memory/thread leak and failing connection (#7919) * fix memory leak on failing websocket * address review comment and fix some smaller issues * more fixes * remove leftovers Signed-off-by: Jan N. Klug --- .../internal/WebSocketConnection.java | 74 +++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java index 5c64e94533afc..f99797e10d609 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java @@ -25,6 +25,7 @@ import java.util.Timer; import java.util.TimerTask; import java.util.UUID; +import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import org.apache.commons.lang.StringUtils; @@ -32,7 +33,11 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand; @@ -54,25 +59,24 @@ public class WebSocketConnection { private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); private final Gson gson = new Gson(); - WebSocketClient webSocketClient; - @Nullable - Session session; - @Nullable - Timer pingTimer; - @Nullable - Timer pongTimeoutTimer; - Listener listener; - boolean closed; - IWebSocketCommandHandler webSocketCommandHandler; + private final WebSocketClient webSocketClient; + private final IWebSocketCommandHandler webSocketCommandHandler; + private final AmazonEchoControlWebSocket amazonEchoControlWebSocket; + + private @Nullable Session session; + private @Nullable Timer pingTimer; + private @Nullable Timer pongTimeoutTimer; + private @Nullable Future sessionFuture; + + private boolean closed; public WebSocketConnection(String amazonSite, List sessionCookies, IWebSocketCommandHandler webSocketCommandHandler) throws IOException { this.webSocketCommandHandler = webSocketCommandHandler; - listener = new Listener(); + amazonEchoControlWebSocket = new AmazonEchoControlWebSocket(); SslContextFactory sslContextFactory = new SslContextFactory(); webSocketClient = new WebSocketClient(sslContextFactory); - try { String host; if (StringUtils.equalsIgnoreCase(amazonSite, "amazon.com")) { @@ -106,16 +110,13 @@ public WebSocketConnection(String amazonSite, List sessionCookies, } ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setHeader("host", host); - request.setHeader("Cache-Control", "no-cache"); - request.setHeader("Pragma", "no-cache"); + request.setHeader("Host", host); request.setHeader("Origin", "alexa." + amazonSite); - request.setCookies(cookiesForWs); initPongTimeoutTimer(); - webSocketClient.connect(listener, uri, request); + sessionFuture = webSocketClient.connect(amazonEchoControlWebSocket, uri, request); } catch (URISyntaxException e) { logger.debug("Initialize web socket failed", e); } @@ -130,7 +131,7 @@ private void setSession(Session session) { @Override public void run() { - listener.sendPing(); + amazonEchoControlWebSocket.sendPing(); } }, 180000, 180000); } @@ -152,11 +153,18 @@ public void close() { try { session.close(); } catch (Exception e) { - logger.debug("Closing sessing failed", e); + logger.debug("Closing session failed", e); } } + logger.trace("Connect future = {}", sessionFuture); + final Future sessionFuture = this.sessionFuture; + if (!sessionFuture.isDone()) { + sessionFuture.cancel(true); + } try { - webSocketClient.stop(); + if (webSocketClient.isStarted()) { + webSocketClient.stop(); + } } catch (InterruptedException e) { // Just ignore } catch (Exception e) { @@ -169,6 +177,7 @@ void clearPongTimeoutTimer() { Timer pongTimeoutTimer = this.pongTimeoutTimer; this.pongTimeoutTimer = null; if (pongTimeoutTimer != null) { + logger.trace("Cancelling pong timeout"); pongTimeoutTimer.cancel(); } } @@ -177,20 +186,24 @@ void initPongTimeoutTimer() { clearPongTimeoutTimer(); Timer pongTimeoutTimer = new Timer(); this.pongTimeoutTimer = pongTimeoutTimer; + logger.trace("Scheduling pong timeout"); pongTimeoutTimer.schedule(new TimerTask() { @Override public void run() { + logger.trace("Pong timeout reached. Closing connection."); close(); } }, 60000); } - class Listener implements WebSocketListener { + @WebSocket(maxTextMessageSize = 64 * 1024, maxBinaryMessageSize = 64 * 1024) + @SuppressWarnings("unused") + public class AmazonEchoControlWebSocket { int msgCounter = -1; int messageId; - Listener() { + AmazonEchoControlWebSocket() { this.messageId = ThreadLocalRandom.current().nextInt(0, Short.MAX_VALUE); } @@ -361,7 +374,7 @@ Message parseIncomingMessage(byte[] data) { return message; } - @Override + @OnWebSocketConnect public void onWebSocketConnect(@Nullable Session session) { if (session != null) { this.msgCounter = -1; @@ -372,7 +385,7 @@ public void onWebSocketConnect(@Nullable Session session) { } } - @Override + @OnWebSocketMessage public void onWebSocketBinary(byte @Nullable [] data, int offset, int len) { if (data == null) { return; @@ -411,20 +424,23 @@ public void onWebSocketBinary(byte @Nullable [] data, int offset, int len) { } } - @Override + @OnWebSocketMessage public void onWebSocketText(@Nullable String message) { + logger.trace("Received text message: '{}'", message); } - @Override + @OnWebSocketClose public void onWebSocketClose(int code, @Nullable String reason) { logger.info("Web Socket close {}. Reason: {}", code, reason); WebSocketConnection.this.close(); } - @Override + @OnWebSocketError public void onWebSocketError(@Nullable Throwable error) { logger.info("Web Socket error", error); - WebSocketConnection.this.close(); + if (!closed) { + WebSocketConnection.this.close(); + } } public void sendPing() { From d43edc4de5086762ba6acfcf86473f3c2f9fe737 Mon Sep 17 00:00:00 2001 From: eugen Date: Wed, 17 Jun 2020 00:31:18 +0200 Subject: [PATCH 49/83] [homekit] add support for heater/cooler (#7887) * add heater/cooler * switch back to custom java hap build * make getKeyFromMapping more generic Signed-off-by: Eugen Freiter --- bundles/org.openhab.io.homekit/README.md | 22 ++ .../internal/HomekitAccessoryType.java | 1 + .../internal/HomekitCharacteristicType.java | 4 + .../homekit/internal/HomekitTaggedItem.java | 34 +++ .../AbstractHomekitAccessoryImpl.java | 70 +++++-- .../accessories/HomekitAccessoryFactory.java | 18 +- .../HomekitCharacteristicFactory.java | 44 +++- .../accessories/HomekitHeaterCoolerImpl.java | 197 ++++++++++++++++++ .../accessories/HomekitThermostatImpl.java | 2 +- 9 files changed, 367 insertions(+), 25 deletions(-) create mode 100644 bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index 222b7ca0a9f79..7d341b47a1f31 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -233,6 +233,20 @@ A full list of supported accessory types can be found in the table *below*. | | TargetTemperature | | Number | target temperature. supported configuration: minValue, maxValue, step | | | CurrentHeatingCoolingMode | | String | Current heating cooling mode (OFF, AUTO, HEAT, COOL). for mapping see homekit settings above. | | | TargetHeatingCoolingMode | | String | Target heating cooling mode (OFF, AUTO, HEAT, COOL). for mapping see homekit settings above. | +| | | Name | String | Name of the thermostat | +| | | CoolingThresholdTemperature | Number | maximum temperature that must be reached before cooling is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | +| | | HeatingThresholdTemperature | Number | minimum temperature that must be reached before heating is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | +| HeaterCooler | | | | Heater or/and cooler device | +| | ActiveStatus | | Switch | accessory current working status. A value of "ON"/"OPEN" indicate that the accessory is active and is functioning without any errors. | +| | CurrentTemperature | | Number | current temperature. supported configuration: minValue, maxValue, step | +| | CurrentHeaterCoolerState | | String | current heater/cooler mode (INACTIVE, IDLE, HEATING, COOLING). Mapping can be redefined at item level, e.g. [HEATING="HEAT", COOLING="COOL"] | +| | TargetHeaterCoolerState | | String | target heater/cooler mode (AUTO, HEAT, COOL). Mapping can be redefined at item level, e.g. [AUTO="AUTOMATIC"] | +| | | Name | String | Name of the heater/cooler | +| | | RotationSpeed | Number | fan rotation speed in % (1-100) | +| | | SwingMode | Number,SwitchItem | swing mode. values: 0/OFF=SWING DISABLED, 1/ON=SWING ENABLED | +| | | LockControl | Number,SwitchItem | status of physical control lock. values: 0/OFF=CONTROL LOCK DISABLED, 1/ON=CONTROL LOCK ENABLED | +| | | CoolingThresholdTemperature | Number | maximum temperature that must be reached before cooling is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | +| | | HeatingThresholdTemperature | Number | minimum temperature that must be reached before heating is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | | Lock | | | | A Lock Mechanism | | | LockCurrentState | | Switch | current states of lock mechanism (OFF=SECURED, ON=UNSECURED) | | | LockTargetState | | Switch | target states of lock mechanism (OFF=SECURED, ON=UNSECURED) | @@ -378,6 +392,14 @@ Switch contactsensor_tampered "Contact Sensor Tampered" Group gSecuritySystem "Security System Group" {homekit="SecuritySystem"} String security_current_state "Security Current State" (gSecuritySystem) {homekit="SecuritySystem.CurrentSecuritySystemState"} String security_target_state "Security Target State" (gSecuritySystem) {homekit="SecuritySystem.TargetSecuritySystemState"} + +Group gCooller "Cooler Group" {homekit="HeaterCooler"} +Switch cooler_active "Cooler Active" (gCooler) {homekit="ActiveStatus"} +Number cooler_current_temp "Cooler Current Temp [%.1f C]" (gCooler) {homekit="CurrentTemperature"} +String cooler_current_mode "Cooler Current Mode" (gCooler) {homekit="CurrentHeaterCoolerState" [HEATING="HEAT", COOLING="COOL"]} +String cooler_target_mode "Cooler Target Mode" (gCooler) {homekit="TargetHeaterCoolerState"} +Number cooler_cool_thrs "Cooler Cool Threshold Temp [%.1f C]" (gCooler) {homekit="CoolingThresholdTemperature" [minValue=10.5, maxValue=50]} +Number cooler_heat_thrs "Cooler Heat Threshold Temp [%.1f C]" (gCooler) {homekit="HeatingThresholdTemperature" [minValue=0.5, maxValue=20]} ``` ## Usage of dimmer modes diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java index 67143a8e661f0..2bff38198737b 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java @@ -43,6 +43,7 @@ public enum HomekitAccessoryType { OUTLET("Outlet"), SPEAKER("Speaker"), GARAGE_DOOR_OPENER("GarageDoorOpener"), + HEATER_COOLER("HeaterCooler"), DUMMY("Dummy"), @Deprecated() BLINDS("Blinds"), diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java index 520e3c0b08381..7fab099184d01 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java @@ -95,6 +95,10 @@ public enum HomekitCharacteristicType { CURRENT_DOOR_STATE("CurrentDoorState"), TARGET_DOOR_STATE("TargetDoorState"), + TARGET_HEATER_COOLER_STATE("TargetHeaterCoolerState"), + CURRENT_HEATER_COOLER_STATE("CurrentHeaterCoolerState"), + COOLING_THRESHOLD_TEMPERATURE("CoolingThresholdTemperature"), + HEATING_THRESHOLD_TEMPERATURE("HeatingThresholdTemperature"), @Deprecated() OLD_BATTERY_LEVEL("homekit:BatteryLevel"), diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java index 52bd734dc39be..00561f8021e1b 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java @@ -14,6 +14,7 @@ import static org.openhab.io.homekit.internal.HomekitAccessoryType.DUMMY; +import java.math.BigDecimal; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -179,6 +180,39 @@ public boolean isMemberOfAccessoryGroup() { return parentGroupItem != null; } + /** + * return object from item configuration for given key or default if not found + * + * @param key configuration key + * @param defaultValue default value + * @param expected class + * @return value + */ + @SuppressWarnings("unchecked") + public T getConfiguration(String key, T defaultValue) { + if (configuration != null) { + final Object value = configuration.get(key); + if (value != null && value.getClass().equals(defaultValue.getClass())) { + return (T) value; + } + } + return defaultValue; + } + + /** + * return configuration as double if exists otherwise return defaultValue + * + * @param key configuration key + * @param defaultValue default value + * @return value + */ + public double getConfigurationAsDouble(String key, double defaultValue) { + return getConfiguration(key, BigDecimal.valueOf(defaultValue)).doubleValue(); + } + + /** + * parse and apply item configuration. + */ private void parseConfiguration() { if (configuration != null) { Object dimmerModeConfig = configuration.get(DIMMER_MODE); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java index 70458100a0144..7722d0bc54ad8 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -28,6 +29,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.library.unit.ImperialUnits; import org.eclipse.smarthome.core.library.unit.SIUnits; import org.eclipse.smarthome.core.types.State; @@ -169,18 +171,6 @@ protected void unsubscribe(HomekitCharacteristicType characteristicType) { return null; } - @SuppressWarnings("unchecked") - private T getItemConfiguration(@NonNull HomekitTaggedItem item, @NonNull String key, @NonNull T defaultValue) { - final @Nullable Map configuration = item.getConfiguration(); - if (configuration != null) { - Object value = configuration.get(key); - if (value != null && value.getClass().equals(defaultValue.getClass())) { - return (T) value; - } - } - return defaultValue; - } - /** * return configuration attached to the root accessory, e.g. groupItem. * Note: result will be casted to the type of the default value. @@ -192,7 +182,7 @@ private T getItemConfiguration(@NonNull HomekitTaggedItem item, @NonNull Str * @return configuration value */ protected T getAccessoryConfiguration(@NonNull String key, @NonNull T defaultValue) { - return getItemConfiguration(accessory, key, defaultValue); + return accessory.getConfiguration(key, defaultValue); } /** @@ -209,8 +199,58 @@ protected T getAccessoryConfiguration(@NonNull String key, @NonNull T defaul protected T getAccessoryConfiguration(@NonNull HomekitCharacteristicType characteristicType, @NonNull String key, @NonNull T defaultValue) { final Optional characteristic = getCharacteristic(characteristicType); - return characteristic.isPresent() ? getItemConfiguration(characteristic.get(), key, defaultValue) - : defaultValue; + return characteristic.isPresent() ? characteristic.get().getConfiguration(key, defaultValue) : defaultValue; + } + + /** + * update mapping with values from item configuration. + * it checks for all keys from the mapping whether there is configuration at item with the same key and if yes, + * replace the value. + * + * @param characteristicType characteristicType to identify item + * @param map mapping to update + */ + protected void updateMapping(HomekitCharacteristicType characteristicType, Map map) { + getCharacteristic(characteristicType).ifPresent(c -> { + final Map configuration = c.getConfiguration(); + if (configuration != null) { + map.replaceAll((k, current_value) -> { + final Object new_value = configuration.get(current_value); + return (new_value instanceof String) ? (String) new_value : current_value; + }); + } + }); + } + + /** + * takes item state as value and retrieves the key for that value from mapping. + * e.g. used to map StringItem value to HomeKit Enum + * + * @param characteristicType characteristicType to identify item + * @param mapping mapping + * @param defaultValue default value if nothing found in mapping + * @param type of the result derived from + * @return key for the value + */ + protected T getKeyFromMapping(final HomekitCharacteristicType characteristicType, Map mapping, + final T defaultValue) { + final Optional c = getCharacteristic(characteristicType); + if (c.isPresent()) { + final State state = c.get().getItem().getState(); + logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(), + state, mapping); + if (state instanceof StringType) { + return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue())) + .findAny().map(Entry::getKey).orElseGet(() -> { + logger.warn( + "Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.", + state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(), + defaultValue); + return defaultValue; + }); + } + } + return defaultValue; } protected void addCharacteristic(HomekitTaggedItem characteristic) { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java index de7effdb6ccaa..cb8a406a099b5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java @@ -91,7 +91,8 @@ public class HomekitAccessoryFactory { put(SPEAKER, new HomekitCharacteristicType[] { MUTE }); put(GARAGE_DOOR_OPENER, new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS }); - + put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE, + TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE }); // LEGACY put(BLINDS, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE }); put(OLD_HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY }); @@ -124,6 +125,7 @@ public class HomekitAccessoryFactory { put(SPEAKER, HomekitSpeakerImpl.class); put(GARAGE_DOOR_OPENER, HomekitGarageDoorOpenerImpl.class); put(BLINDS, HomekitWindowCoveringImpl.class); + put(HEATER_COOLER, HomekitHeaterCoolerImpl.class); put(OLD_HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class); put(OLD_DIMMABLE_LIGHTBULB, HomekitLightbulbImpl.class); put(OLD_COLORFUL_LIGHTBULB, HomekitLightbulbImpl.class); @@ -166,13 +168,13 @@ public class HomekitAccessoryFactory { public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException { final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType(); - logger.trace("Constructing {} of accessoryType {}", taggedItem.getName(), accessoryType); + logger.trace("Constructing {} of accessoryType {}", taggedItem.getName(), accessoryType.getTag()); final List requiredCharacteristics = getMandatoryCharacteristics(taggedItem, metadataRegistry); final HomekitCharacteristicType[] mandatoryCharacteristics = MANDATORY_CHARACTERISTICS.get(accessoryType); if ((mandatoryCharacteristics != null) && (requiredCharacteristics.size() < mandatoryCharacteristics.length)) { - logger.warn("Accessory of type {} must have following characteristics {}. Found only {}", accessoryType, - mandatoryCharacteristics, requiredCharacteristics); + logger.warn("Accessory of type {} must have following characteristics {}. Found only {}", + accessoryType.getTag(), mandatoryCharacteristics, requiredCharacteristics); throw new HomekitException("Missing mandatory characteristics"); } AbstractHomekitAccessoryImpl accessoryImpl; @@ -189,12 +191,12 @@ public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegi addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry); return accessoryImpl; } else { - logger.warn("Unsupported HomeKit type: {}", accessoryType); + logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag()); throw new HomekitException("Unsupported HomeKit type: " + accessoryType); } } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { - logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType, e); + logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e); throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType); } } @@ -361,7 +363,7 @@ private static void addOptionalCharacteristics(final HomekitTaggedItem taggedIte // an accessory can have multiple optional characteristics. iterate over them. characteristics.forEach((type, item) -> { try { - logger.trace("adding optional characteristic: {} for item {}", type, item.getName()); + logger.trace("adding optional characteristic: {} for item {}", type.getTag(), item.getName()); // check whether a proxyItem already exists, if not create one. final HomekitOHItemProxy proxyItem = proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)); @@ -378,7 +380,7 @@ private static void addOptionalCharacteristics(final HomekitTaggedItem taggedIte accessory.addCharacteristic(optionalItem); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) { logger.warn("Not supported optional HomeKit characteristic. Service type {}, characteristic type {}", - service.getType(), type, e); + service.getType(), type.getTag(), e); } }); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java index 9ed2fdca54c7c..476f06757e896 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java @@ -68,6 +68,8 @@ import io.github.hapjava.characteristics.impl.lightbulb.ColorTemperatureCharacteristic; import io.github.hapjava.characteristics.impl.lightbulb.HueCharacteristic; import io.github.hapjava.characteristics.impl.lightbulb.SaturationCharacteristic; +import io.github.hapjava.characteristics.impl.thermostat.CoolingThresholdTemperatureCharacteristic; +import io.github.hapjava.characteristics.impl.thermostat.HeatingThresholdTemperatureCharacteristic; import io.github.hapjava.characteristics.impl.valve.RemainingDurationCharacteristic; import io.github.hapjava.characteristics.impl.valve.SetDurationCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.CurrentHorizontalTiltAngleCharacteristic; @@ -119,7 +121,8 @@ public class HomekitCharacteristicFactory { put(LOCK_CONTROL, HomekitCharacteristicFactory::createLockPhysicalControlsCharacteristic); put(DURATION, HomekitCharacteristicFactory::createDurationCharacteristic); put(VOLUME, HomekitCharacteristicFactory::createVolumeCharacteristic); - + put(COOLING_THRESHOLD_TEMPERATURE, HomekitCharacteristicFactory::createCoolingThresholdCharacteristic); + put(HEATING_THRESHOLD_TEMPERATURE, HomekitCharacteristicFactory::createHeatingThresholdCharacteristic); put(REMAINING_DURATION, HomekitCharacteristicFactory::createRemainingDurationCharacteristic); // LEGACY put(OLD_BATTERY_LOW_STATUS, HomekitCharacteristicFactory::createStatusLowBatteryCharacteristic); @@ -226,6 +229,17 @@ private static Supplier> getDoubleSupplier(final Homek }; } + private static ExceptionalConsumer setDoubleConsumer(final HomekitTaggedItem taggedItem) { + return (value) -> { + if (taggedItem.getItem() instanceof NumberItem) { + ((NumberItem) taggedItem.getItem()).send(new DecimalType(value)); + } else { + logger.warn("Item type {} is not supported for {}. Only Number type is supported.", + taggedItem.getItem().getType(), taggedItem.getName()); + } + }; + } + protected static Consumer getSubscriber(final HomekitTaggedItem taggedItem, final HomekitCharacteristicType key, final HomekitAccessoryUpdater updater) { return (callback) -> updater.subscribe((GenericItem) taggedItem.getItem(), key.getTag(), callback); @@ -522,4 +536,32 @@ private static VolumeCharacteristic createVolumeCharacteristic(final HomekitTagg (volume) -> ((NumberItem) taggedItem.getItem()).send(new DecimalType(volume)), getSubscriber(taggedItem, DURATION, updater), getUnsubscriber(taggedItem, DURATION, updater)); } + + private static CoolingThresholdTemperatureCharacteristic createCoolingThresholdCharacteristic( + final HomekitTaggedItem taggedItem, final HomekitAccessoryUpdater updater) { + return new CoolingThresholdTemperatureCharacteristic( + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MIN_VALUE, + CoolingThresholdTemperatureCharacteristic.DEFAULT_MIN_VALUE), + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MAX_VALUE, + CoolingThresholdTemperatureCharacteristic.DEFAULT_MAX_VALUE), + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.STEP, + CoolingThresholdTemperatureCharacteristic.DEFAULT_STEP), + getDoubleSupplier(taggedItem), setDoubleConsumer(taggedItem), + getSubscriber(taggedItem, COOLING_THRESHOLD_TEMPERATURE, updater), + getUnsubscriber(taggedItem, COOLING_THRESHOLD_TEMPERATURE, updater)); + } + + private static HeatingThresholdTemperatureCharacteristic createHeatingThresholdCharacteristic( + final HomekitTaggedItem taggedItem, final HomekitAccessoryUpdater updater) { + return new HeatingThresholdTemperatureCharacteristic( + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MIN_VALUE, + HeatingThresholdTemperatureCharacteristic.DEFAULT_MIN_VALUE), + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MAX_VALUE, + HeatingThresholdTemperatureCharacteristic.DEFAULT_MAX_VALUE), + taggedItem.getConfigurationAsDouble(HomekitTaggedItem.STEP, + HeatingThresholdTemperatureCharacteristic.DEFAULT_STEP), + getDoubleSupplier(taggedItem), setDoubleConsumer(taggedItem), + getSubscriber(taggedItem, HEATING_THRESHOLD_TEMPERATURE, updater), + getUnsubscriber(taggedItem, HEATING_THRESHOLD_TEMPERATURE, updater)); + } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java new file mode 100644 index 0000000000000..8d67664548ecd --- /dev/null +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java @@ -0,0 +1,197 @@ +/** + * 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.io.homekit.internal.accessories; + +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.items.GenericItem; +import org.eclipse.smarthome.core.library.items.StringItem; +import org.eclipse.smarthome.core.library.items.SwitchItem; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; +import org.openhab.io.homekit.internal.HomekitCharacteristicType; +import org.openhab.io.homekit.internal.HomekitSettings; +import org.openhab.io.homekit.internal.HomekitTaggedItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.hapjava.accessories.HeaterCoolerAccessory; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateEnum; +import io.github.hapjava.characteristics.impl.heatercooler.TargetHeaterCoolerStateEnum; +import io.github.hapjava.characteristics.impl.thermostat.TemperatureDisplayUnitCharacteristic; +import io.github.hapjava.characteristics.impl.thermostat.TemperatureDisplayUnitEnum; +import io.github.hapjava.services.impl.HeaterCoolerService; + +/** + * Implements Heater Cooler + * + * @author Eugen Freiter - Initial contribution + */ + +public class HomekitHeaterCoolerImpl extends AbstractHomekitAccessoryImpl implements HeaterCoolerAccessory { + private final Logger logger = LoggerFactory.getLogger(HomekitHeaterCoolerImpl.class); + private final BooleanItemReader activeReader; + private final Map currentStateMapping = new EnumMap( + CurrentHeaterCoolerStateEnum.class) { + { + put(CurrentHeaterCoolerStateEnum.INACTIVE, "INACTIVE"); + put(CurrentHeaterCoolerStateEnum.IDLE, "IDLE"); + put(CurrentHeaterCoolerStateEnum.HEATING, "HEATING"); + put(CurrentHeaterCoolerStateEnum.COOLING, "COOLING"); + + } + }; + private final Map targetStateMapping = new EnumMap( + TargetHeaterCoolerStateEnum.class) { + { + put(TargetHeaterCoolerStateEnum.AUTO, "AUTO"); + put(TargetHeaterCoolerStateEnum.HEAT, "HEAT"); + put(TargetHeaterCoolerStateEnum.COOL, "COOL"); + } + }; + + public HomekitHeaterCoolerImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, + HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + super(taggedItem, mandatoryCharacteristics, updater, settings); + activeReader = new BooleanItemReader(getItem(HomekitCharacteristicType.ACTIVE_STATUS, GenericItem.class), + OnOffType.ON, OpenClosedType.OPEN); + updateMapping(CURRENT_HEATER_COOLER_STATE, currentStateMapping); + updateMapping(TARGET_HEATER_COOLER_STATE, targetStateMapping); + final HeaterCoolerService service = new HeaterCoolerService(this); + service.addOptionalCharacteristic(new TemperatureDisplayUnitCharacteristic(() -> getTemperatureDisplayUnit(), + (value) -> setTemperatureDisplayUnit(value), (callback) -> subscribeTemperatureDisplayUnit(callback), + () -> unsubscribeTemperatureDisplayUnit())); + getServices().add(service); + } + + @Override + public CompletableFuture getCurrentTemperature() { + @Nullable + final DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, DecimalType.class); + return CompletableFuture.completedFuture(state != null ? convertToCelsius(state.doubleValue()) : 0.0); + } + + @Override + public CompletableFuture isActive() { + final @Nullable State state = getStateAs(HomekitCharacteristicType.ACTIVE_STATUS, OnOffType.class); + return CompletableFuture.completedFuture(state == OnOffType.ON); + } + + @Override + public CompletableFuture setActive(final boolean state) { + final @Nullable SwitchItem item = getItem(HomekitCharacteristicType.ACTIVE_STATUS, SwitchItem.class); + if (item != null) { + item.send(OnOffType.from(state)); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture getCurrentHeaterCoolerState() { + return CompletableFuture.completedFuture(getKeyFromMapping(CURRENT_HEATER_COOLER_STATE, currentStateMapping, + CurrentHeaterCoolerStateEnum.INACTIVE)); + } + + @Override + public CompletableFuture getTargetHeaterCoolerState() { + return CompletableFuture.completedFuture( + getKeyFromMapping(TARGET_HEATER_COOLER_STATE, targetStateMapping, TargetHeaterCoolerStateEnum.AUTO)); + } + + @Override + public CompletableFuture setTargetHeaterCoolerState(final TargetHeaterCoolerStateEnum state) { + final Optional characteristic = getCharacteristic( + HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE); + if (characteristic.isPresent()) { + ((StringItem) characteristic.get().getItem()).send(new StringType(targetStateMapping.get(state))); + } else { + logger.warn("Missing mandatory characteristic {}", + HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE.getTag()); + } + return CompletableFuture.completedFuture(null); + } + + public CompletableFuture getTemperatureDisplayUnit() { + return CompletableFuture + .completedFuture(getSettings().useFahrenheitTemperature ? TemperatureDisplayUnitEnum.FAHRENHEIT + : TemperatureDisplayUnitEnum.CELSIUS); + } + + public void setTemperatureDisplayUnit(final TemperatureDisplayUnitEnum value) throws Exception { + // temperature unit set globally via binding setting and cannot be changed at item level. + // this method is intentionally empty. + } + + @Override + public void subscribeCurrentHeaterCoolerState(final HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE, callback); + } + + @Override + public void unsubscribeCurrentHeaterCoolerState() { + unsubscribe(HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE); + } + + @Override + public void subscribeTargetHeaterCoolerState(final HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE, callback); + } + + @Override + public void unsubscribeTargetHeaterCoolerState() { + unsubscribe(HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE); + } + + @Override + public void subscribeActive(final HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.ACTIVE_STATUS, callback); + } + + @Override + public void unsubscribeActive() { + unsubscribe(HomekitCharacteristicType.ACTIVE_STATUS); + } + + @Override + public void subscribeCurrentTemperature(final HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.CURRENT_TEMPERATURE, callback); + } + + @Override + public void unsubscribeCurrentTemperature() { + unsubscribe(HomekitCharacteristicType.CURRENT_TEMPERATURE); + } + + public void subscribeTemperatureDisplayUnit(final HomekitCharacteristicChangeCallback callback) { + // temperature unit set globally via binding setting and cannot be changed at item level. + // this method is intentionally empty + } + + public void unsubscribeTemperatureDisplayUnit() { + // temperature unit set globally via binding setting and cannot be changed at item level. + // this method is intentionally empty + } +} diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java index 76003a87962ce..e681e895de225 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java @@ -77,7 +77,7 @@ public CompletableFuture getCurrentState() { } else if (stringValue.equalsIgnoreCase(settings.thermostatCurrentModeOff)) { mode = CurrentHeatingCoolingStateEnum.OFF; } else if (stringValue.equals("UNDEF") || stringValue.equals("NULL")) { - logger.warn("Heating cooling target mode not available. Relaying value of OFF to Homekit"); + logger.warn("Heating cooling current mode not available. Relaying value of OFF to Homekit"); mode = CurrentHeatingCoolingStateEnum.OFF; } else { logger.warn("Unrecognized heatingCoolingCurrentMode: {}. Expected {}, {}, or {} strings in value.", From 8cc08e386bda153b0424e81428413eaeaea75138 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Wed, 17 Jun 2020 11:27:16 +0200 Subject: [PATCH 50/83] [nikohomecontrol] Correct README (#7930) Signed-off-by: Mark Herwege --- bundles/org.openhab.binding.nikohomecontrol/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 71b2acccb6a30..d6b5cdbb8a4b0 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -313,8 +313,8 @@ Rollershutter Kitchen {channel="nikohomecontrol:blind:nhc1:4:rollershutter"} Number:Temperature CurTemperature "[%.1f °F]" {channel="nikohomecontrol:thermostat:nhc1:5:measured"} # Getting measured temperature from thermostat in °F, read only Number ThermostatMode {channel="nikohomecontrol:thermostat:nhc1:5:mode"} # Get and set thermostat mode Number:Temperature SetTemperature "[%.1f °C]" {channel="nikohomecontrol:thermostat:nhc1:5:setpoint"} # Get and set target temperature in °C -Number OverruleDuration {channel="nikohomecontrol:thermostat:nhc1:5:overruletime} # Get and set the overrule time -Number ThermostatDemand {channel="nikohomecontrol:thermostat:nhc1:5:demand} # Get the current heating/cooling demand +Number OverruleDuration {channel="nikohomecontrol:thermostat:nhc1:5:overruletime"} # Get and set the overrule time +Number ThermostatDemand {channel="nikohomecontrol:thermostat:nhc1:5:demand"} # Get the current heating/cooling demand Number:Power CurPower "[%.0f W]" {channel="nikohomecontrol:energyMeter:nhc2:6:power"} # Get current power consumption ``` @@ -327,7 +327,7 @@ Slider item=TVRoom Switch item=TVRoom # allows switching dimmer item off or on (with controller defined behavior) Rollershutter item=Kitchen Text item=CurTemperature -Selection item=ThermostatMode mappings="[0="day", 1="night", 2="eco", 3="off", 4="cool", 5="prog 1", 6="prog 2", 7="prog 3"] +Selection item=ThermostatMode mappings=[0="day", 1="night", 2="eco", 3="off", 4="cool", 5="prog 1", 6="prog 2", 7="prog 3"] Setpoint item=SetTemperature minValue=0 maxValue=30 Slider item=OverruleDuration minValue=0 maxValue=120 Text item=Power From 1babb803c7b378b779040696cb5e6a14be300fc5 Mon Sep 17 00:00:00 2001 From: eugen Date: Wed, 17 Jun 2020 17:45:37 +0200 Subject: [PATCH 51/83] [homekit] fix 100% brightness for dimmer (#7932) * homekit dimmer. fix 100% brightness Signed-off-by: Eugen Freiter --- .../org/openhab/io/homekit/internal/HomekitOHItemProxy.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java index d1ef1a0ae4c97..842c2118543a1 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java @@ -85,6 +85,7 @@ private void sendCommand() { final PercentType brightness = (PercentType) commandCache.remove(BRIGHTNESS_COMMAND); final DecimalType hue = (DecimalType) commandCache.remove(HUE_COMMAND); final PercentType saturation = (PercentType) commandCache.remove(SATURATION_COMMAND); + final @Nullable OnOffType currentOnState = ((DimmerItem) item).getStateAs(OnOffType.class); if (on != null) { // always sends OFF. // sends ON only if @@ -93,7 +94,7 @@ private void sendCommand() { // - DIMMER_MODE_FILTER_ON_EXCEPT100 is not enabled and brightness is null or below 100 if ((on == OnOffType.OFF) || (dimmerMode == DIMMER_MODE_NORMAL) || (dimmerMode == DIMMER_MODE_FILTER_BRIGHTNESS_100) - || ((dimmerMode == DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) + || ((dimmerMode == DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) && (currentOnState != OnOffType.ON) && ((brightness == null) || (brightness.intValue() == 100)))) { logger.trace("send OnOff command for item {} with value {}", item, on); ((DimmerItem) item).send(on); @@ -119,7 +120,7 @@ private void sendCommand() { // - other modes (DIMMER_MODE_FILTER_BRIGHTNESS_100 or DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) and // <100%. if ((dimmerMode == DIMMER_MODE_NORMAL) || (dimmerMode == DIMMER_MODE_FILTER_ON) - || (brightness.intValue() < 100)) { + || (brightness.intValue() < 100) || (currentOnState == OnOffType.ON)) { logger.trace("send Brightness command for item {} with value {}", item, brightness); ((DimmerItem) item).send(brightness); } From 8495b82bb63eb9fd4db631b8f3429940cea1baf5 Mon Sep 17 00:00:00 2001 From: ambo11 <67016781+ambo11@users.noreply.github.com> Date: Wed, 17 Jun 2020 18:36:12 +0200 Subject: [PATCH 52/83] [plclogo] Update README.md (#7933) Missing some " at the end of items' line --- bundles/org.openhab.binding.plclogo/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.plclogo/README.md b/bundles/org.openhab.binding.plclogo/README.md index 5d1e6fb14c9a3..4dcf3219bb157 100644 --- a/bundles/org.openhab.binding.plclogo/README.md +++ b/bundles/org.openhab.binding.plclogo/README.md @@ -317,9 +317,9 @@ DateTime LogoDate { channel="plclogo:datetime:Logo:VW150:date" } Switch LogoVB1_S { channel="plclogo:pulse:Logo:VB0_1:state"} Switch LogoVB1_O { channel="plclogo:pulse:Logo:VB0_1:observed"} -String Diagnostic { channel="plclogo:device:Logo:diagnostic } -DateTime RTC { channel="plclogo:device:Logo:rtc } -String DayOfWeek { channel="plclogo:device:Logo:weekday } +String Diagnostic { channel="plclogo:device:Logo:diagnostic"} +DateTime RTC { channel="plclogo:device:Logo:rtc"} +String DayOfWeek { channel="plclogo:device:Logo:weekday"} ``` Configuration of two Siemens LOGO! @@ -353,7 +353,7 @@ Switch Logo1_Q2 { channel="plclogo:digital:Logo1:Outputs:Q2" } Number Logo1_VW100 { channel="plclogo:memory:Logo1:VW100:value" } Switch Logo1_VB0_S { channel="plclogo:pulse:Logo1:VB0_0:state"} Contact Logo1_VB0_O { channel="plclogo:pulse:Logo1:VB0_0:observed"} -DateTime Logo1_RTC { channel="plclogo:device:Logo1:rtc } +DateTime Logo1_RTC { channel="plclogo:device:Logo1:rtc"} Contact Logo2_I1 { channel="plclogo:digital:Logo2:Inputs:I1" } Contact Logo2_I2 { channel="plclogo:digital:Logo2:Inputs:I2" } @@ -362,7 +362,7 @@ Switch Logo2_Q2 { channel="plclogo:digital:Logo2:Outputs:Q2" } Number Logo2_VD102 { channel="plclogo:memory:Logo2:VD102:value" } Switch Logo2_VB1_S { channel="plclogo:pulse:Logo2:VB0_1:state"} Switch Logo2_VB1_O { channel="plclogo:pulse:Logo2:VB0_1:observed"} -DateTime Logo2_RTC { channel="plclogo:device:Logo2:rtc } +DateTime Logo2_RTC { channel="plclogo:device:Logo2:rtc"} ``` ## Troubleshooting From a1cfacd37ded0d72de07654700697fec5856d87b Mon Sep 17 00:00:00 2001 From: t2000 Date: Wed, 17 Jun 2020 19:45:19 +0200 Subject: [PATCH 53/83] [squeezebox] Answer REFRESH command from cache if possible (#7934) Fixes #7928 Signed-off-by: Stefan Triller --- .../internal/handler/SqueezeBoxPlayerHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java index 855429deeb375..f533af50f0f94 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java @@ -179,8 +179,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { } String mac = getConfigAs(SqueezeBoxPlayerConfig.class).mac; - // Some of the code below is not designed to handle REFRESH + // Some of the code below is not designed to handle REFRESH, only reply to channels where cached values exist if (command == RefreshType.REFRESH) { + String channelID = channelUID.getId(); + if (stateMap.containsKey(channelID)) { + updateState(channelID, stateMap.get(channelID)); + } return; } From 00a2cd55daece0f194d1b3f9f30584da655ad3d7 Mon Sep 17 00:00:00 2001 From: t2000 Date: Wed, 17 Jun 2020 19:50:57 +0200 Subject: [PATCH 54/83] [Robonect] Use global openHAB timezone for DateTime objects (#7903) * [Robonect] Use global openHAB timezone for DateTime objects Use the timezone that has been configured by the user in openHAB settings for all robonect messages instead of UTC. Fixes #7848 Signed-off-by: Stefan Triller --- .../org.openhab.binding.robonect/README.md | 1 + .../internal/RobonectHandlerFactory.java | 21 +++++----- .../internal/config/RobonectConfig.java | 10 ++++- .../internal/handler/RobonectHandler.java | 40 +++++++++++++++++-- .../resources/ESH-INF/thing/thing-types.xml | 5 +++ .../internal/handler/RobonectHandlerTest.java | 21 ++++++---- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/bundles/org.openhab.binding.robonect/README.md b/bundles/org.openhab.binding.robonect/README.md index 9bcfd4c004f7b..5899a0af5cbdb 100644 --- a/bundles/org.openhab.binding.robonect/README.md +++ b/bundles/org.openhab.binding.robonect/README.md @@ -35,6 +35,7 @@ following configuration settings are supported for the `mower` thing. | offlineTimeout | no | the maximum time, the mower can be offline before the binding triggers the offlineTrigger channel | | user | no | the username if authentication is enabled in the firmware. | | password | no | the password if authenticaiton is enabled in the firmware. | +| timezone | no | the timezone as configured in Robonect on the robot (default: Europe/Berlin) | An example things configuration might look like diff --git a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectHandlerFactory.java b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectHandlerFactory.java index 09d7939f21de2..6cdb9c7f86b84 100644 --- a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectHandlerFactory.java +++ b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/RobonectHandlerFactory.java @@ -18,6 +18,7 @@ import java.util.Set; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; @@ -25,6 +26,7 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; import org.eclipse.smarthome.io.net.http.HttpClientFactory; import org.openhab.binding.robonect.internal.handler.RobonectHandler; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -40,6 +42,14 @@ public class RobonectHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AUTOMOWER); private HttpClient httpClient; + private TimeZoneProvider timeZoneProvider; + + @Activate + public RobonectHandlerFactory(@Reference HttpClientFactory httpClientFactory, + @Reference TimeZoneProvider timeZoneProvider) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.timeZoneProvider = timeZoneProvider; + } @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -51,18 +61,9 @@ protected ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(THING_TYPE_AUTOMOWER)) { - return new RobonectHandler(thing, httpClient); + return new RobonectHandler(thing, httpClient, timeZoneProvider); } return null; } - - @Reference - protected void setHttpClientFactory(HttpClientFactory httpClientFactory) { - this.httpClient = httpClientFactory.getCommonHttpClient(); - } - - protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) { - this.httpClient = null; - } } diff --git a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/config/RobonectConfig.java b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/config/RobonectConfig.java index db5b246b5e567..17ecd76ff65aa 100644 --- a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/config/RobonectConfig.java +++ b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/config/RobonectConfig.java @@ -13,9 +13,9 @@ package org.openhab.binding.robonect.internal.config; /** - * + * * This class acts simply a structure for holding the thing configuration. - * + * * @author Marco Meyer - Initial contribution */ public class RobonectConfig { @@ -30,6 +30,8 @@ public class RobonectConfig { private int offlineTimeout; + private String timezone; + public String getHost() { return host; } @@ -49,4 +51,8 @@ public int getPollInterval() { public int getOfflineTimeout() { return offlineTimeout; } + + public String getTimezone() { + return timezone; + } } diff --git a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/handler/RobonectHandler.java b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/handler/RobonectHandler.java index 583eaf317c654..e1029cd7551b7 100644 --- a/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/handler/RobonectHandler.java +++ b/bundles/org.openhab.binding.robonect/src/main/java/org/openhab/binding/robonect/internal/handler/RobonectHandler.java @@ -14,8 +14,10 @@ import static org.openhab.binding.robonect.internal.RobonectBindingConstants.*; +import java.time.DateTimeException; import java.time.Instant; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -23,6 +25,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -71,12 +74,16 @@ public class RobonectHandler extends BaseThingHandler { private ScheduledFuture pollingJob; private HttpClient httpClient; + private TimeZoneProvider timeZoneProvider; + + private ZoneId timeZone; private RobonectClient robonectClient; - public RobonectHandler(Thing thing, HttpClient httpClient) { + public RobonectHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { super(thing); this.httpClient = httpClient; + this.timeZoneProvider = timeZoneProvider; } @Override @@ -277,8 +284,21 @@ private void updateNextTimer(MowerInfo info) { } private State convertUnixToDateTimeType(String unixTimeSec) { - Instant ns = Instant.ofEpochMilli(Long.valueOf(unixTimeSec) * 1000); - ZonedDateTime zdt = ZonedDateTime.ofInstant(ns, ZoneId.of("UTC")); + // the value in unixTimeSec represents the time on the robot in its configured timezone. However, it does not + // provide which zone this is. Thus we have to add the zone information from the Thing configuration in order to + // provide correct results. + Instant rawInstant = Instant.ofEpochMilli(Long.valueOf(unixTimeSec) * 1000); + + ZoneId timeZoneOfThing = timeZone; + if (timeZoneOfThing == null) { + timeZoneOfThing = timeZoneProvider.getTimeZone(); + } + ZoneOffset offsetToConfiguredZone = timeZoneOfThing.getRules().getOffset(rawInstant); + long adjustedTime = rawInstant.getEpochSecond() - offsetToConfiguredZone.getTotalSeconds(); + Instant adjustedInstant = Instant.ofEpochMilli(adjustedTime * 1000); + + // we provide the time in the format as configured in the openHAB settings + ZonedDateTime zdt = adjustedInstant.atZone(timeZoneProvider.getTimeZone()); return new DateTimeType(zdt); } @@ -329,6 +349,20 @@ public void initialize() { RobonectConfig robonectConfig = getConfigAs(RobonectConfig.class); RobonectEndpoint endpoint = new RobonectEndpoint(robonectConfig.getHost(), robonectConfig.getUser(), robonectConfig.getPassword()); + + String timeZoneString = robonectConfig.getTimezone(); + try { + if (timeZoneString != null) { + timeZone = ZoneId.of(timeZoneString); + } else { + logger.warn("No timezone provided, falling back to the default timezone configured in openHAB: '{}'", + timeZoneProvider.getTimeZone()); + } + } catch (DateTimeException e) { + logger.warn("Error setting timezone '{}', falling back to the default timezone configured in openHAB: '{}'", + timeZoneString, timeZoneProvider.getTimeZone(), e); + } + try { httpClient.start(); robonectClient = new RobonectClient(httpClient, endpoint); diff --git a/bundles/org.openhab.binding.robonect/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.robonect/src/main/resources/ESH-INF/thing/thing-types.xml index 478a66b1966b2..f27bf214a62e2 100644 --- a/bundles/org.openhab.binding.robonect/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.robonect/src/main/resources/ESH-INF/thing/thing-types.xml @@ -80,6 +80,11 @@ The maximum time the mower may be offline before the offline trigger is triggered. + + + Europe/Berlin + The timezone configured on the robot (e.g. Europe/Berlin). + diff --git a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java index 9fd6fa89c0026..4790904caadd7 100644 --- a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java +++ b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java @@ -12,16 +12,15 @@ */ package org.openhab.binding.robonect.internal.handler; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import java.time.ZoneId; import java.util.Calendar; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -38,6 +37,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.openhab.binding.robonect.internal.RobonectBindingConstants; import org.openhab.binding.robonect.internal.RobonectClient; @@ -53,7 +53,7 @@ /** * The goal of this class is to test RobonectHandler in isolation. - * + * * @author Marco Meyer - Initial contribution */ public class RobonectHandlerTest { @@ -72,12 +72,17 @@ public class RobonectHandlerTest { @Mock private HttpClient httpClientMock; + @Mock + private TimeZoneProvider timezoneProvider; + @Before public void setUp() { MockitoAnnotations.initMocks(this); - subject = new RobonectHandler(robonectThingMock, httpClientMock); + subject = new RobonectHandler(robonectThingMock, httpClientMock, timezoneProvider); subject.setCallback(callbackMock); subject.setRobonectClient(robonectClientMock); + + Mockito.when(timezoneProvider.getTimeZone()).thenReturn(ZoneId.of("Europe/Berlin")); } @Test From 0ae2319ddc3564f690215c31e941fbe3803b06d0 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Wed, 17 Jun 2020 21:20:29 +0200 Subject: [PATCH 55/83] [lcn] Add LCN binding (#7509) * [lcn] Add LCN binding Migrates the Local Control Network Binding from OH1 to OH2. Closes #108 Signed-off-by: Fabian Wolter --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.lcn/.classpath | 32 + bundles/org.openhab.binding.lcn/.project | 23 + bundles/org.openhab.binding.lcn/NOTICE | 13 + bundles/org.openhab.binding.lcn/README.md | 594 +++++++++++++ .../doc/LCN-PRO_output_steps.png | Bin 0 -> 200517 bytes .../org.openhab.binding.lcn/doc/dyn_text.png | Bin 0 -> 106065 bytes bundles/org.openhab.binding.lcn/doc/ir.png | Bin 0 -> 110156 bytes .../org.openhab.binding.lcn/doc/overview.jpg | Bin 0 -> 72598 bytes bundles/org.openhab.binding.lcn/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../lcn/internal/DimmerOutputProfile.java | 119 +++ .../lcn/internal/ILcnModuleActions.java | 31 + .../lcn/internal/LcnBindingConstants.java | 43 + .../LcnChannelVariableConfiguration.java | 26 + .../lcn/internal/LcnGroupConfiguration.java | 25 + .../binding/lcn/internal/LcnGroupHandler.java | 55 ++ .../lcn/internal/LcnHandlerFactory.java | 66 ++ .../lcn/internal/LcnModuleActions.java | 202 +++++ .../lcn/internal/LcnModuleConfiguration.java | 26 + .../internal/LcnModuleDiscoveryService.java | 264 ++++++ .../lcn/internal/LcnModuleHandler.java | 363 ++++++++ .../lcn/internal/LcnProfileFactory.java | 71 ++ .../lcn/internal/PckGatewayConfiguration.java | 54 ++ .../lcn/internal/PckGatewayHandler.java | 303 +++++++ .../internal/common/DimmerOutputCommand.java | 65 ++ .../binding/lcn/internal/common/LcnAddr.java | 79 ++ .../lcn/internal/common/LcnAddrGrp.java | 102 +++ .../lcn/internal/common/LcnAddrMod.java | 102 +++ .../lcn/internal/common/LcnChannelGroup.java | 122 +++ .../binding/lcn/internal/common/LcnDefs.java | 156 ++++ .../lcn/internal/common/LcnException.java | 37 + .../lcn/internal/common/PckGenerator.java | 780 ++++++++++++++++++ .../lcn/internal/common/ReverseNumber.java | 53 ++ .../binding/lcn/internal/common/Variable.java | 278 +++++++ .../lcn/internal/common/VariableValue.java | 99 +++ .../connection/AbstractConnectionState.java | 100 +++ ...bstractConnectionStateSendCredentials.java | 48 ++ .../internal/connection/AbstractState.java | 76 ++ .../connection/AbstractStateMachine.java | 64 ++ .../lcn/internal/connection/Connection.java | 474 +++++++++++ .../connection/ConnectionCallback.java | 44 + .../connection/ConnectionSettings.java | 158 ++++ .../connection/ConnectionStateConnected.java | 58 ++ .../connection/ConnectionStateConnecting.java | 97 +++ ...ectionStateGracePeriodBeforeReconnect.java | 45 + .../connection/ConnectionStateInit.java | 37 + .../connection/ConnectionStateMachine.java | 107 +++ .../ConnectionStateSegmentScan.java | 82 ++ .../ConnectionStateSendDimMode.java | 41 + .../ConnectionStateSendPassword.java | 41 + .../ConnectionStateSendUsername.java | 41 + .../connection/ConnectionStateShutdown.java | 45 + ...ConnectionStateWaitForLcnBusConnected.java | 68 ++ ...itForLcnBusConnectedAfterDisconnected.java | 33 + .../lcn/internal/connection/ModInfo.java | 500 +++++++++++ .../lcn/internal/connection/PckQueueItem.java | 74 ++ .../internal/connection/RequestStatus.java | 195 +++++ .../lcn/internal/connection/SendData.java | 38 + .../lcn/internal/connection/SendDataPck.java | 77 ++ .../connection/SendDataPlainText.java | 61 ++ .../lcn/internal/converter/Converter.java | 118 +++ .../lcn/internal/converter/Converters.java | 62 ++ .../lcn/internal/converter/S0Converter.java | 55 ++ .../internal/pchkdiscovery/ExtService.java | 39 + .../internal/pchkdiscovery/ExtServices.java | 33 + .../LcnPchkDiscoveryService.java | 161 ++++ .../lcn/internal/pchkdiscovery/Server.java | 73 ++ .../pchkdiscovery/ServicesResponse.java | 47 ++ .../lcn/internal/pchkdiscovery/Version.java | 43 + .../AbstractLcnModuleSubHandler.java | 178 ++++ .../AbstractLcnModuleVariableSubHandler.java | 140 ++++ .../subhandler/ILcnModuleSubHandler.java | 155 ++++ .../LcnModuleBinarySensorSubHandler.java | 62 ++ .../subhandler/LcnModuleCodeSubHandler.java | 108 +++ .../LcnModuleKeyLockTableSubHandler.java | 95 +++ .../subhandler/LcnModuleLedSubHandler.java | 66 ++ .../subhandler/LcnModuleLogicSubHandler.java | 126 +++ .../LcnModuleMetaAckSubHandler.java | 90 ++ .../LcnModuleMetaFirmwareSubHandler.java | 55 ++ .../subhandler/LcnModuleOutputSubHandler.java | 182 ++++ .../subhandler/LcnModuleRelaySubHandler.java | 86 ++ ...cnModuleRollershutterOutputSubHandler.java | 82 ++ ...LcnModuleRollershutterRelaySubHandler.java | 77 ++ .../LcnModuleRvarLockSubHandler.java | 66 ++ .../LcnModuleRvarSetpointSubHandler.java | 79 ++ .../LcnModuleS0CounterSubHandler.java | 58 ++ .../LcnModuleThresholdSubHandler.java | 105 +++ .../LcnModuleVariableSubHandler.java | 88 ++ .../resources/ESH-INF/binding/binding.xml | 10 + .../main/resources/ESH-INF/config/config.xml | 102 +++ .../resources/ESH-INF/i18n/lcn_de.properties | 171 ++++ .../resources/ESH-INF/thing/thing-types.xml | 634 ++++++++++++++ .../lcn/internal/ModuleActionsTest.java | 199 +++++ .../LcnPchkDiscoveryServiceTest.java | 58 ++ .../AbstractTestLcnModuleSubHandler.java | 43 + .../LcnModuleBinarySensorSubHandlerTest.java | 77 ++ .../LcnModuleKeyLockTableSubHandlerTest.java | 173 ++++ .../LcnModuleLedSubHandlerTest.java | 84 ++ .../LcnModuleLogicSubHandlerTest.java | 106 +++ .../LcnModuleOutputSubHandlerTest.java | 198 +++++ .../LcnModuleRelaySubHandlerTest.java | 114 +++ ...duleRollershutterOutputSubHandlerTest.java | 66 ++ ...oduleRollershutterRelaySubHandlerTest.java | 89 ++ .../LcnModuleRvarLockSubHandlerTest.java | 64 ++ .../LcnModuleRvarSetpointSubHandlerTest.java | 138 ++++ .../LcnModuleS0CounterSubHandlerTest.java | 57 ++ .../LcnModuleThresholdSubHandlerTest.java | 133 +++ .../LcnModuleVariableSubHandlerTest.java | 89 ++ bundles/pom.xml | 1 + 111 files changed, 11954 insertions(+) create mode 100644 bundles/org.openhab.binding.lcn/.classpath create mode 100644 bundles/org.openhab.binding.lcn/.project create mode 100644 bundles/org.openhab.binding.lcn/NOTICE create mode 100644 bundles/org.openhab.binding.lcn/README.md create mode 100644 bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png create mode 100644 bundles/org.openhab.binding.lcn/doc/dyn_text.png create mode 100644 bundles/org.openhab.binding.lcn/doc/ir.png create mode 100644 bundles/org.openhab.binding.lcn/doc/overview.jpg create mode 100644 bundles/org.openhab.binding.lcn/pom.xml create mode 100644 bundles/org.openhab.binding.lcn/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties create mode 100644 bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java create mode 100644 bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 00a90cc03e31c..13b48f0bd423b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ /bundles/org.openhab.binding.konnected/ @volfan6415 /bundles/org.openhab.binding.kostalinverter/ @cschneider /bundles/org.openhab.binding.lametrictime/ @syphr42 +/bundles/org.openhab.binding.lcn/ @fwolter /bundles/org.openhab.binding.leapmotion/ @kaikreuzer /bundles/org.openhab.binding.lghombot/ @FluBBaOfWard /bundles/org.openhab.binding.lgtvserial/ @fa2k diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index c6e18f0eea673..a66b090c2eaa4 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -471,6 +471,11 @@ org.openhab.binding.lametrictime ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.lcn + ${project.version} + org.openhab.addons.bundles org.openhab.binding.leapmotion diff --git a/bundles/org.openhab.binding.lcn/.classpath b/bundles/org.openhab.binding.lcn/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.lcn/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.lcn/.project b/bundles/org.openhab.binding.lcn/.project new file mode 100644 index 0000000000000..5302a4a4a785d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.lcn + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.lcn/NOTICE b/bundles/org.openhab.binding.lcn/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.lcn/README.md b/bundles/org.openhab.binding.lcn/README.md new file mode 100644 index 0000000000000..6226424c7e662 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/README.md @@ -0,0 +1,594 @@ +# LCN Binding + +[Local Control Network (LCN)](http://www.lcn.eu) is a building automation system for small and very large installations. +It is capable of controlling lights, shutters, access control etc. and can process data from several sensor types. +It has been introduced in 1992. + +A broad range of glass key panels, displays, remote controls, sensors and in- and outputs exist. +The system can handle up to 30,000 bus members, called modules. +LCN modules are available for DIN rail and in-wall mounting and feature versatile interfaces. The bus modules and most of the accessories are developed, manufactured and assembled in Germany. + +Bus members are inter-connected via a free wire in the standard NYM cable. Wireless components are available, though. + +![Illustration of the LCN product family](doc/overview.jpg) + +This binding uses TCP/IP to access the LCN bus via the software LCN-PCHK (Windows/Linux) or the DIN rail device LCN-PKE. +**This means 1 unused LCN-PCHK license or a LCN-PKE is required** + +## LCN Overview + +LCN modules and their connecting peripherals are explained in the following. + +### LCN Modules + +Active LCN components connected to the LCN bus are called *LCN modules*. +LCN modules are addressed by their numeric id: Valid range is 5..254 + +In larger buildings, a second topologic layer is added: *segments*. +Valid range is 5..128 or 0 (= no segments exist) or 3 (= target all segments) + +LCN modules within the **same** segment can be grouped: Valid range is 5..254 or 3 (= target all groups) + +### LCN Firmware Versions + +Each LCN module has a feature-set based on its firmware version. +This version is written as follows: \[year since 1990\]\[month\]\[day\] + +Each component is written in hexadecimal with 2 characters. Examples: + +- 090101 = 1. january 1990 +- 0D0C01 = 1. december 2003 +- 170206 = 6. feb. 2013 + +### LCN Dimmer Outputs + +LCN modules support 2 to 4 dimmer output ports (number depends on firmware version). +If the module hardware type doesn't feature physical dimmer outputs, the outputs can still be used as virtual. + +Status values are always in percent. +Modules since 170206 have a 0.5%-steps resolution. Older modules have a 2%-steps resolution. + +The time it takes the output port to reach its setpoint is called *ramp*. + +### LCN Variables + +LCN modules support: + +- 3 or 12 (since 170206) analog variables for general purpose +- 2 regulators with configurable setpoints +- 5 or 4x4 (since 170206) thresholds (trigger levels) +- 4 S0-input counters (since 170206, LCN-BU4L must be connected) + +### LCN Regulators (additions to variables) + +LCN modules have 2 regulators. +Each one has a setpoint and uses one variable as its value source. +A regulator can be locked, so that the target actuator keeps switched off, also if the value source is in control range. + +### LCN Thresholds + +LCN modules since firmware 170206 have 4 threshold registers. Each threshold register comprises 4 thresholds. + +A threshold register uses one variable as its value source (see [LCN Variables](#lcn-variables)). +Arbitrary LCN commands can be send into the bus, when the value-source falls below a threshold or exceeds one. +A threshold can be locked, so that the configured LCN command is not fired, also if the value source passes the threshold. + +### LCN Relays + +LCN modules support 8 relays. If no hardware relays are connected, the relays can still be used as virtual. + +### LCN Binary Sensors + +LCN modules support 8 binary sensors (e.g. motion detectors; hardware periphery must be connected). + +### LCN LEDs (legacy name: *lamps*) + +12x multi-state variables can be used for logic operations or visualization (hardware periphery must be connected). + +Values: OFF, ON, BLINK, FLICKER + +### LCN Logic Operations (legacy name: *sums*) + +4x multi-state variables each representing the result of a logic operation of the associated LEDs. + +Values: NOT (all LEDs off), OR (some LEDs on), AND (all LEDs on) + +### LCN Keys + +LCN keys are data-points in the module with bound commands. +LCN modules support 3 ("A-C") or 4 ("A-D") key-tables (number depends on firmware version). + +Each key-table holds 8 keys. Examples: A1, A7, D8 + +Each key has 3 command types: HIT(press), MAKE(long press), BREAK(long press release) + +These keys can be locked. The bound (LCN-)commands cannot be executed, then. + +### LCN Access Control & Remote Controls + +LCN can interface several transponder readers and finger print sensors, used for access control. + +Remote controls can not only be used for triggering commands, but also for access control, by evaluating the transmitted serial number. + +## Supported Things + +### Thing: LCN Module + +Any LCN module that should be controlled or visualized, need to be added to openHAB as a *Thing*. + +LCN modules with firmware versions 120612 (2008) and 170602 (2013) were tested with this binding. +No known features/changes that need special handling were added until now (2020). +Modules with older and newer firmware should work, too. +The module hardware types (e.g. LCN-SH, LCN-HU, LCN-UPP, ...) are compatible to each other and can therefore be handled all in the same way. + +Thing ID: `module` + +| Name | Description | Type | Required | +|-------------|----------------------------------------------------------------|---------|----------| +| `moduleId` | The module ID, configured in LCN-PRO | Integer | Yes | +| `segmentId` | The segment ID the module is in (0 if no segments are present) | Integer | Yes | + +openHAB's discovery function can be used to add LCN modules automatically. +See [Discover LCN Modules](#discover-lcn-modules). + +### Thing: LCN PCK Gateway + +PCK is the protocol spoken over TCP/IP with a PCK gateway to communicate with the LCN bus. +Examples for PCK gateways are the *LCN-PCHK* software running on Windows or Linux and the DIN rail mounting device *LCN-PKE*. + +For each LCN bus, interfaced to openHAB, a PCK gateway needs to be added to openHAB as a *Thing*. + +Several PCK gateways can be added to openHAB to control multiple LCN busses in distinct locations. + +The minimum recommended version is LCN-PCHK 2.8 (older versions will also work, but lack some functionality). +Visit [https://www.lcn.eu](https://www.lcn.eu) for updates. + +Thing ID: `pckGateway` + +| Name | Description | Type | Required | +|-------------|------------------------------------------------------------------------------------------------------------|---------|----------| +| `hostname` | Hostname or IP address of the LCN-PCHK gateway | String | Yes | +| `port` | TCP port of the LCN-PCHK gateway (default:4114) | Integer | Yes | +| `username` | Username configured within LCN-PCHK Monitor | String | Yes | +| `password` | Password configured within LCN-PCHK Monitor | String | Yes | +| `mode` | Dimmer resolution: `native50` or `native200` See below. | String | Yes | +| `timeoutMs` | Period after which an LCN command is resent, when no acknowledge has been received (in ms) (default: 3500) | Integer | Yes | + +> **IMPORTANT:** You need to configure the dimmer output resolution. This setting is valid for the **whole** LCN bus.
+The setting is either 0-50 steps or 0-200 steps. +It **has to be the same** as in the parameterizing software **LCN-PRO** under Options/Settings/Expert Settings. +See the following screenshot. + +![LCN-PRO screenshot, showing the 50 or 200 steps for the dimmer outputs](doc/LCN-PRO_output_steps.png) + +When using a wrong dimmer output setting, dimming the outputs will result in unintended behavior. + +### Thing: LCN Group + +LCN modules can be assigned to groups with the programming software *LCN-PRO*. + +To send commands to an LCN group, the group needs to be added to openHAB as a *Thing*. + +One LCN module within the group is used to represent the status of the whole group. +For example, when a Dimmer Output is controlled via a LCN group *Thing*, openHAB will always visualize the state of the Dimmer Output of the chosen module. The states of the other modules in the group are ignored for visualization. + +Thing ID: `group` + +| Name | Description | Type | Required | +|-------------|----------------------------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `groupId` | The group number, configured in LCN-PRO | Integer | Yes | +| `moduleId` | The module ID of any module in the group. The state of this module is used for visualization of the group as representative for all modules. | Integer | Yes | +| `segmentId` | The segment ID of all modules in this group (0 if no segments are present) | Integer | Yes | + +The `groupId` must match the previously configured group number in the programming software *LCN-PRO*. + +## Discovery + +### Discover LCN Modules + +Basic data of LCN modules can be read out by openHAB. +To do so, simply start openHAB's discovery. + +If not all LCN modules get listed on the first run, click on the refresh button to start another scan. + +When adding a module by discovery, the new *Thing*'s UID will be the module's serial number. + +### Discover PCK Gateways + +PCK gateways in the LAN can be found automatically by openHAB. This is done by UDP multicast messages on port 4220. +The discovery works only if the firewall of the PCK gateway is not configured too strictly. +This means on Windows PCs, that the network must be configured as 'private' and not as 'public'. +Also, some network switches may block multicast packets. +Unfortunately, *LCN-PCHK* listens only on the first network interface of the computer for discovery packets. +If your PCK gateway has multiple network interfaces, *LCN-PCHK* may listen on the wrong interface and fails to respond to the discovery request. + +Discovery has successfully been tested with LCN-PCHK 3.2.2 running on a Raspberry Pi with Raspbian and openHAB running on Windows 10. + +If discovery fails, you can add a PCK gateway manually. See [Thing: PCK Gateway](#thing-lcn-pck-gateway). + +Please be aware that you **have to configure** username, password and the dimmer output resolution also if you use discovery. +See [Thing: PCK Gateway](#thing-lcn-pck-gateway). + +When adding a PCK gateway by discovery, the new *Thing*'s UID is the MAC address of the device, running the PCK gateway. + +## Supported LCN Features and openHAB Channels + +The following table lists all features of LCN and their mappings to openHAB Channels. These Channels are available for the *Things* LCN module (`module`) and LCN group (`group`). The PCK gateway (`pckGateway`) has no Channels. + +Although, there are many **Not implemented** entries, the vast majority of LCN features can be used with openHAB:
+If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende Taste") can be used to hit a module's key virtually and execute an arbitrary command. + +| LCN Feature (English) | LCN Feature (German) | Channel | IDs | Type | Description | +|---------------------------------|----------------------------------|------------------------|------|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------| +| Dimmer Output Control Single | Ausgang | output | 1-4 | Dimmer, Switch | Sets the dimming value of an output with a given ramp. | +| Relay | Relais | relay | 1-8 | Switch | Controls a relay and visualizes its state. | +| Visualize Binary Sensor | Binärsensor anzeigen | binarysensor | 1-8 | Contact | Visualizes the state of a binary sensor. | +| LED Control | LED-Steuerung | led | 1-12 | Text (ON, OFF, BLINK, FLICKER) | Controls an LED and visualizes its current state. | +| Visualize Logic Operations | Logik Funktion anzeigen | logic | 1-4 | Text (NOT, OR, AND) | Visualizes the result of the logic operation. | +| Motor/Shutter on Dimmer Outputs | Motor/Rollladen an Ausgängen | rollershutteroutput | 1-4 | Rollershutter | Control roller shutters on dimmer outputs | +| Motor/Shutter on Relays | Motor/Rollladen an Relais | rollershutterrelay | 1-4 | Rollershutter | Control roller shutters on relays | +| Variables | Variable anzeigen | variable | 1-12 | Number | Sets and visualizes the value of a variable. | +| Regulator Set Setpoint | Regler Sollwert ändern | rvarsetpoint | 1-2 | Number | Sets and visualizes the setpoint of a regulator. | +| Regulator Lock | Regler sperren | rvarlock | 1-2 | Switch | Locks a regulator and visualizes its locking state. | +| Set Thresholds in Register 1 | Schwellwert in Register 1 ändern | thresholdregister1 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. | +| Set Thresholds in Register 2 | Schwellwert in Register 2 ändern | thresholdregister2 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. | +| Set Thresholds in Register 3 | Schwellwert in Register 3 ändern | thresholdregister3 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. | +| Set Thresholds in Register 4 | Schwellwert in Register 4 ändern | thresholdregister4 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. | +| Visualize S0 Counters | S0-Zähler anzeigen | s0input | 1-4 | Number | Visualizes the value of a S0 counter. | +| Lock Keys Table A | Sperre Tastentabelle A | keylocktablea | 1-8 | Switch | Locks a key on the given key table and visualizes its state. | +| Lock Keys Table B | Sperre Tastentabelle B | keylocktableb | 1-8 | Switch | Locks a key on the given key table and visualizes its state. | +| Lock Keys Table C | Sperre Tastentabelle C | keylocktablec | 1-8 | Switch | Locks a key on the given key table and visualizes its state. | +| Lock Keys Table D | Sperre Tastentabelle D | keylocktabled | 1-8 | Switch | Locks a key on the given key table and visualizes its state. | +| Dimmer Output Flicker | Ausgang: Flackern | N/A | N/A | N/A | Action "flickerOutput": Let a dimmer output flicker for a given count of flashes. | +| Dynamic Text | Dynamischer Text | N/A | N/A | N/A | Action: "sendDynamicText": Sends custom text to an LCN-GTxD display. | +| Send Keys | Sende Tasten | N/A | N/A | N/A | Action: "hitKey": Hits a key of a key table in an LCN module. Can be used to execute commands, not supported by this binding. | +| Dimmer Output Control Multiple | Mehrere Ausgänge steuern | output | 1-4 | Dimmer, Switch | Control multiple outputs simultaneously. See below. | +| Transponder | Transponder | code#transponder | | Trigger | Receive transponder messages | +| Remote Control | Fernbedienung | code#remotecontrolkey | | Trigger | Receive commands from remote control | +| Access Control | Zutrittskontrolle | code#remotecontrolcode | | Trigger | Receive serial numbers from remote control | +| Remote Control Battery Low | Fernbedienung Batterie schwach | code#remotecontrolbatterylow | | Trigger | Triggered when the sending remote control has a low battery | +| Status Message | Statusmeldungen | - | - | - | Automatically done by OpenHAB Binding | +| Audio Beep | Audio Piepen | - | - | - | Not implemented | +| Audio LCN-MRS | Audio LCN-MRS | - | - | - | Not implemented | +| Count/Compute | Zählen/Rechnen | - | - | - | Not implemented | +| DALI | DALI | - | - | - | Not implemented | +| Dimmer Output Memory Toggle | Ausgang: Memory Taster | - | - | - | Not implemented | +| Dimmer Output Ramp Stop | Ausgang: Rampe Stop | - | - | - | Not implemented | +| Dimmer Output Relative | Ausgang: Relativ | - | - | - | Not implemented | +| Dimmer Output Stairway | Ausgang: Treppenhauslicht | - | - | - | Not implemented | +| Dimmer Output Timer | Ausgang: Timer (Kurzzeit) | - | - | - | Not implemented | +| Display Set Language | Display-Sprache setzen | - | - | - | Not implemented | +| Dynamic Groups | Dynamische Gruppen | - | - | - | Not implemented | +| Free Input | Freie Eingabe | - | - | - | Not implemented | +| LED Brightness | LED-Helligkeit | - | - | - | Not implemented | +| LED Test | LED-Test | - | - | - | Not implemented | +| LED Transform | LED-Umwandlung | - | - | - | Not implemented | +| Light Scenes | Lichtszenen | - | - | - | Not implemented | +| Lock Keys by Time (Table A) | Sperre (Zeit) Tasten (Tabelle A) | - | - | - | Not implemented | +| Lock Outputs by Time | Sperre (Zeit) Ausgänge | - | - | - | Not implemented | +| Lock Relays | Sperre Relais | - | - | - | Not implemented | +| Lock Thresholds | Sperre Schwellwerte | - | - | - | Not implemented | +| Motor Position | Motor Position | - | - | - | Not implemented | +| Relay Timer | Relais-Timer | - | - | - | Not implemented | +| Send Keys Delayed | Sende Tasten verzögert | - | - | - | Not implemented | +| Set S0 Counters | S0-Zähler setzen | - | - | - | Not implemented | +| Status Command | Statuskommandos | - | - | - | Not implemented | + +**For some *Channel*s a unit should be configured for visualization.** By default the native LCN value is used. + +S0 counter Channels need to be the pulses per kWh configured. If the value is left blank, a default value of 1000 pulses/kWh is set. + +### Transponder + +LCN transponder readers can be integrated in openHAB e.g. for access control. +The transponder function must be enabled in the module's I-port properties within *LCN-PRO*. + +Example: When the transponder card with the ID "12ABCD" is seen by the reader connected to LCN module "17B308349E", the item "M10_Relay7" is switched on: + +``` +rule "My Transponder" +when + Channel "lcn:module:b827ebfea4bb:17B308349E:code#transponder" triggered "12ABCD" +then + M10_Relay7.sendCommand(ON) +end +``` + +### Remote Control + +To evaluate commands from LCN remote controls (e.g. LCN-RT or LCN-RT16), the module's I-port behavior must be configured as "IR access control" within *LCN-PRO*: + +![Screenshot, showing the I-port properties for remote controls](doc/ir.png) + +#### Remote Control Keys + +The trigger *Channel* `lcn:module:::code#remotecontrolkey` can be used to execute commands, when a specific key on a remote control is pressed: + +``` +rule "Remote Control Key 3 on Layer 1 hit" +when + Channel "lcn:module:b827ebfea4bb:17B3073D6A:code#remotecontrolkey" triggered "A3:HIT" +then + M10_Relay7.sendCommand(ON) +end +``` + +`A3` is key 3 on the first layer. `B1` is key 1 on the second layer etc.. After the colon follows the LCN "hit type" HIT, MAKE or BREAK (German: kurz, lang, los). + +#### Remote Control used as Access Control + +The serial number of a remote control can be used for access control via the channel `lcn:module:::code#remotecontrolcode`. See the following example: + +``` +rule "Remote Control Key 3 on Layer 1 hit (only executed for serial number AB1234)" +when + Channel "lcn:module:b827ebfea4bb:17B3073D6A:code#remotecontrolcode" triggered "AB1234:A3:HIT" or + Channel "lcn:module:b827ebfea4bb:17B3073D6A:code#remotecontrolcode" triggered "AB1234:A3:MAKE" +then + M10_Relay7.sendCommand(ON) +end +``` + +The command will be executed when the remote control button A3 is either pressed short or long. + +## Dimmer Outputs with Ramp and Multiple Outputs + +The *output* profile can be used to control multiple dimmer outputs of the *same* module simultaneously or control a dimmer output with a ramp (slowly dimming). + +The optional *ramp* parameter must be float or integer. +The lowest value is 0.25, which corresponds to 0.25s. The highest value is 486s. +When no *ramp* parameter is specified or no profile is configured, the ramp is 0 (behavior like a switch). +The ramp parameter is not available for Color *Item*s. + +``` +// Dim output 2 in 0.25s +Switch M10_Output2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#2"[profile="lcn:output", ramp=0.25]} // with ramp of 0.25s (smallest value) +// Dim output 3 in 486s +Dimmer M10_Output3 {channel="lcn:module:b827ebfea4bb:17B4196847:output#3"[profile="lcn:output", ramp=486]} // with ramp of 486s (biggest value) +``` + +The optional parameters *controlAllOutputs* and *controlOutputs12* can be used to control multiple outputs simultaneously. +Please note that the combination of these parameters with the *ramp* parameter is limited: + +``` +// Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported. +Dimmer M10_Outputs12a {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlOutputs12=true]} +Dimmer M10_Outputs12b {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlOutputs12=true, ramp=0.25]} +// Control all outputs simultaneously. Status of Output 1 is visualized. +Dimmer M10_OutputAll1 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp only since firmware 180501 +Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp compatibility: all +Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.5]} // ramp only since firmware 180501 +``` + +## Actions + +Actions are special commands that can be sent to LCN modules or LCN groups. + +### Hit Key + +This *Action* virtually hits a key of a key table in an LCN module. +Simply spoken, OpenHab acts as a push button switch connected to an LCN module. + +This *Action* can be used to execute commands which are not natively supported by this binding. +The function can be programmed via the software *LCN-PRO* onto a key in a module's key table. +Then, the programmed key can be "hit" by this *Action* and the command will be executed. + +When programming a "Hit Key" *Action*, the following parameters need to be set: + +*table* - The module's key table: A, B, C or D
+*key* - The number of the key within the key table: 1-8
+*action* - The key's action: HIT (German: "kurz"), MAKE ("lang") or BREAK ("los") + +``` +rule "Hit key C4 hourly" +when + Time cron "0 0 * * * ?" +then + val actions = getActions("lcn","lcn:module:b827ebfea4bb:17B4196847") + actions.hitKey("C", 4, "HIT") +end +``` + +### Dynamic Text + +This *Action* can be used to send custom texts to an LCN-GTxD display. +To make this function work, the row of the display has to be configured to allow dynamic text within *LCN-PRO*: + +![Screenshot of LCN-PRO, showing the dynamic text setting of an LCN-GT10D](doc/dyn_text.png) + +When programming a "Dynamic Text" *Action*, the following parameters need to be set: + +*row* - The number of the row in the display: 1-4
+*text* - The text to be displayed (UTF-8) + +The length of the text may not exceed 60 bytes of characters. +Bear in mind that unicode characters can take more than one byte (e.g. umlauts (äöü) take two bytes). + +``` +rule "Send dynamic Text to GT10D hourly" +when + Time cron "0 0 * * * ?" +then + val actions = getActions("lcn","lcn:module:b827ebfea4bb:17B3073D6A") + actions.sendDynamicText(1, "Test 123 CO₂ öäü߀") // row 1 +end +``` + +### Flicker Output + +This *Action* realizes the LCN command "Output: Flicker" (German: "Ausgang: Flackern"). +The command let a dimmer output flash a given number of times. This feature can be used e.g. for alert signals or visual door bells. + +When programming a "Flicker Output" *Action*, the following parameters need to be set: + +*output* - The dimmer output number: 1-4
+*depth* - The depth of the flickering: 0-2 (0=25% 1=50% 2=100% Example: When the output is fully on (100%), and 0 is selected, flashes will dim from 100% to 75% and back)
+*ramp* - The duration/ramp of one flash: 0-2 (0=2sec 1=1sec 2=0.5sec)
+*count* - The number of flashes: 1-15 + +This action has also effect, if the given output is off. The output will be dimmed from 0% to *depth* and back, then. + +``` +rule "Flicker output 1 when window opens" +when + Item M10_BinarySensor5 changed to OPEN +then + val actions = getActions("lcn","lcn:module:b827ebfea4bb:17B4196847") + // output=1, depth=2=100%, ramp=0=2s, count=3 + actions.flickerOutput(1, 2, 0, 3) +end +``` + +## Caveat and Limitations + +LCN segments are supported by this binding, but could not be tested, due to lack of hardware. + +LEDs do not support the *OnOffCommand* and respectively the *Switch* Item type, because they have the additional states *BLINK* and *FLICKER*. They must be configured as *String* Item. When used in rules, the parameter must be of type string. Example: `M10_LED1.sendCommand("ON")`. Note the quotation marks. + +## Full Example + +Config .items + +``` +// Dimmer Outputs +Dimmer M10_Output1 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"} +Switch M10_Output2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#2"[profile="lcn:output", ramp=0.25]} // with ramp of 0.25s (smallest value) +Dimmer M10_Output3 {channel="lcn:module:b827ebfea4bb:17B4196847:output#3"[profile="lcn:output", ramp=486]} // with ramp of 486s (biggest value) + +// Dimmer Outputs: Control all simultaneously. Status of Output 1 is visualized. +Dimmer M10_OutputAll1 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp=0: only since firmware 180501 +Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp=0.25: compatibility: all firmwares +Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.5]} // ramp>=0.5: only since firmware 180501 + +// Dimmer Outputs: Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported. +Dimmer M10_Outputs12b {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlOutputs12=true, ramp=0.25]} + +// Dimmer Outputs: RGB Control +Color M10_Color {channel="lcn:module:b827ebfea4bb:17B4196847:output#color"[profile="lcn:output"]} + +// Roller Shutter on Output 1+2 +Rollershutter M10_RollershutterOutput1 {channel="lcn:module:b827ebfea4bb:17B4196847:rollershutteroutput#1"} + +// Relays +Switch M10_Relay1 {channel="lcn:module:b827ebfea4bb:17B4196847:relay#1"} + +// Roller Shutter on Relays 1+2 +Rollershutter M10_RollershutterRelay1 {channel="lcn:module:b827ebfea4bb:17B4196847:rollershutterrelay#1"} + +// LEDs +String M10_LED1 {channel="lcn:module:b827ebfea4bb:17B4196847:led#1"} +String M10_LED2 {channel="lcn:module:b827ebfea4bb:17B4196847:led#2"} + +// Logic Operations (legacy name: "Sums") +String M10_Logic1 {channel="lcn:module:b827ebfea4bb:17B4196847:logic#1"} +String M10_Logic2 {channel="lcn:module:b827ebfea4bb:17B4196847:logic#2"[profile="transform:MAP", function="alertSystem.map"]} +// conf/transform/alertSystem.map: +// NOT=All windows are closed +// OR=Some windows are open +// AND=All windows are open + +// Binary Sensors +Contact M10_BinarySensor1 {channel="lcn:module:b827ebfea4bb:17B4196847:binarysensor#1"} + +// Variables +// The units of the variables must also be set in the Channels configuration, to be visualized correctly. +Number:Temperature M10_Variable1 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#1"} // Temperature in °C +Number:Temperature M10_Variable2 "[%.1f °F]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#2"} // Temperature in °F +Number M10_Variable3 "[%d ppm]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#3"} // Indoor air quality in ppm +Number M10_Variable4 "[%d lx]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#4"} // Illuminance in Lux +Number:Illuminance M10_Variable5 "[%.1f klx]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#5"} // Illuminance in kLux +Number M10_Variable6 "[%.1f mA]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#6"} // Electrical current in mA +Number M10_Variable7 "[%.1f V]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#7"} // Voltage in V +Number M10_Variable8 "[%.1f m/s]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#8"} // Wind speed in m/s +Number M10_Variable9 "[%.1f °]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#9"} // position of the sun (azimuth or elevation) in ° +Number M10_Variable10 "[%d W]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#10"} // Current power of an S0 input in W +Number:Power M10_Variable11 "[%.1f kW]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#11"} // Current power of an S0 input in kW + +// Regulators +Number:Temperature M10_R1VarSetpoint "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:rvarsetpoint#1"} // Temperature in °C +Switch M10_R1VarLock {channel="lcn:module:b827ebfea4bb:17B4196847:rvarlock#1"} // Lock state of R1Var + +// Thresholds +Number:Temperature M10_ThresholdRegister1_Threshold1 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:thresholdregister1#1"} // Temperature in °C +Number:Temperature M10_ThresholdRegister4_Threshold2 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:thresholdregister4#2"} // Temperature in °C + +// S0 Counters +Number:Energy M10_S0Counter1 "[%.1f kWh]" {channel="lcn:module:b827ebfea4bb:17B4196847:s0input#1"} + +// Key Locks +Switch M10_KeyLockA1 {channel="lcn:module:b827ebfea4bb:17B4196847:keylocktablea#1"} +Switch M10_KeyLockD5 {channel="lcn:module:b827ebfea4bb:17B4196847:keylocktabled#5"} +``` + +Config .sitemap + +``` +sitemap lcn label="My home automation" { + Frame label="Demo Items" { + // Dimmer Outputs + Default item=M10_Output1 label="Output 1" + Default item=M10_Output2 label="Output 2" + Default item=M10_Output3 label="Output 3" + + // Dimmer Outputs: Control all simultaneously. Status of Output 1 is visualized. + Default item=M10_OutputAll1 label="All Outputs ramp=0 since firmware 180501" + Default item=M10_OutputAll2 label="All Outputs ramp=250ms all firmwares" + Default item=M10_OutputAll3 label="All Outputs ramp>=500ms since firmware 180501" + + // Dimmer Outputs: Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported. + Default item=M10_Outputs12a label="Outputs 1+2 Ramp=0" + Default item=M10_Outputs12b label="Outputs 1+2 Ramp=0.25s" + + // Dimmer Outputs: RGB Control + Colorpicker item=M10_Color + + // Roller Shutter on Outputs 1+2 + Default item=M10_RollershutterOutput1 label="Roller Shutter on Output 1+2" + + // Relays + Default item=M10_Relay1 label="Relay 1" + + // Roller Shutter on Relays + Default item=M10_RollershutterRelay1 label="Roller Shutter on Relay 1-2" + + // LEDs + Switch item=M10_LED1 label="LED 1" mappings=[ON=ON, OFF=OFF] // Don't display "Blink" or "Flicker" + Switch item=M10_LED2 label="LED 2" + + // Logic Operations (legacy name: "Sums") + Default item=M10_Logic1 label="Logic Operation 1" + Default item=M10_Logic2 label="Logic Operation 2" + + // Binary Sensors + Default item=M10_BinarySensor1 label="Binary Sensor 1" + + // Variables + Setpoint item=M10_Variable1 label="Variable 1" + Default item=M10_Variable2 label="Variable 2" + Default item=M10_Variable3 label="Variable 3" + Default item=M10_Variable4 label="Variable 4" + Default item=M10_Variable5 label="Variable 5" + Default item=M10_Variable6 label="Variable 6" + Default item=M10_Variable7 label="Variable 7" + Default item=M10_Variable8 label="Variable 8" + Default item=M10_Variable9 label="Variable 9" + Default item=M10_Variable10 label="Variable 10" + Default item=M10_Variable11 label="Variable 11" + + // Regulators + Setpoint item=M10_R1VarSetpoint label="R1Var Setpoint" step=1 minValue=-10.0 + Default item=M10_R1VarLock label="R1Var Lock" // Lock state of R1Var + + // Thresholds + Setpoint item=M10_ThresholdRegister1_Threshold1 label="Threshold Register 1 Threshold 1" + Setpoint item=M10_ThresholdRegister4_Threshold2 label="Threshold Register 4 Threshold 2" + + // S0 Counters + Default item=M10_S0Counter1 label="S0 Counter 1" + + // Key Locks + Default item=M10_KeyLockA1 label="Locked State Key A1" + Default item=M10_KeyLockD5 label="Locked State Key D5" + } +} +``` diff --git a/bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png b/bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png new file mode 100644 index 0000000000000000000000000000000000000000..d86eaabe932fa6888deb4a2d95afa3fd8253570c GIT binary patch literal 200517 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 z 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�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

  • +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

    B{N;H}+Lo@~-&)(1eCv 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.lcn/doc/dyn_text.png b/bundles/org.openhab.binding.lcn/doc/dyn_text.png new file mode 100644 index 0000000000000000000000000000000000000000..64167c7b23ea3da5b7f42b1f3f4c75893fceb8dc GIT binary patch literal 106065 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

    -DZ!fI-O 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

    *11(UQ%p?#Wx0Gz&4{CLV%U5fRiKB8 zBn;4F3yEJSHb1$0&Q%_RRRa!tiw{$xBLs-2-(K#(JB7;RF%vd$e03S@4bopUC@m-` z=1+$|xB_6P1;Q z%M@WNZqs6WY~w7=tEko)#bqMR?fAF93`w4-(&GR2@DYd(KCa&r<99f>b7|{D6qK`6 zribA@y3$y}0Q3Yh34MV}lg10dH2YvkJZY%IWX}=w-o7tX00~6qAmw6Zf0R@YUx6L$>?d<5_6H}agfn9a_1cFFTNli?iafJ?U zGP6Bg!M%Rc?7I0*e{60*QWS(9kLl{j>XH9cqYH|~U!hJ2NdDu;!P_F&aa4X`|NQ>3 z(S%3jfL5^9r!K*`_M>Q?E@!Ho0*+k5#5s4$tXKu^xHba-w;eFJT#b^I>UttTMtC=w zQ~M@#c+;D3jUOOt_#QX)R{j;$3s8apP#V+UY&u+s4n8`Oh5y~GDi)bBDMlMN4L2ZS z%lQ7H{qnX2Ij#}ERoef%1lg7>Ub^=Pm}WRu^Bkj(en*)!n)v_fL^91B$ z^&3+atK!Icn*#BdiCrSMst%fG-4I>; ziBm$9m^hOpHPTd{{p)^wuvG6s3-&5O;<5}EBh3o61Y5;8Zlv&770DK>>F4a>%Q=Cw zr)%dWgJpYV6gBfdWIc_bMGluS$rB|S#20kjm_=J{c4k)&dT@rLhmxje_&d6~Sp!a`|8++jKHs06%_zXgieEvl2_o!XdFb6vMcE;Fvj&Pfx+r~d4e5Y+N7 z7-lKkE}q90vE9B$8Ft?ms_z)%z8Sl;aeaG|=8?g}XxT14GUwVy>$-mXP_q}SOTFpo z;$bHye!aVURrURjxRpwAnF~Us?ZbnFu)x=-EpFZVt!XfPiw!avUY==n9Iz(xYL?7TUjmS<(m+Hs%iB#*!&i`e%3{ikbrc_Bf+*O2 zM@`&kxwoI>-@jnF4GG~tPti7wMDFz|{yOc?YC|J@=EC*pk(}DwphbnGg4+2ql&Dz? z^6-5OXIAX)(e;`7i}3jY%Bc+{;cb*_pWUgM6Iu9)X+zt(Y9nq*Y*9mp)>_F1td85& zbGYPKTv!}QmS`mJUq`%aIyl1#EYiWEFJa9ni3AQ~)9n-{PjXD*LCHu)_eCyoE%Y7A zqXRM5iONG^hQ0!$?^5NSr(iQL=94=rE#YBzmN~qc6nw(LXhB!G{Xj0f_3n&*Bu~r1 zkxx3r%o7*WgN~gulP*G+LQkJaF}zu_CjrDra3Rdt1C&B(NoA5kLU9Dz;EYoK;L+&3 zCI8%`un`gdyT`)lu(?n{N*H8pwCdt|@NnCdSH@3mvIM^d3V1Y{~PH2 zbj5G7{tRai+BlGsV89+>dYflFa-(IuaJsBkoK_5XokTwq>iE2^-x%X}<$XFVJuL1w z_Py{YY5Qs8Df6;Ce)VY*EPT%3cT~A_DezgarIrlVDn2M~Q4V~N>VCcqq{Si=xz4^P zRo?sL(9?Vy{ql2s({HC4chlgzXPsszk&HnTj_(LxKJdVKzlV0Qxz#d0qYR?Am9Md9M1|qIi>& z8uS}rV72*vgyf%`zap`d^ubJ@HDB6;;D>i<4(EFdK^Um!FjRI?J-{T&&{Gk_4iyHnh3qpc+|el zS~3&3o-+2o$n*8I9CJSXN6*ZAUofp&%NR+L;``ZMp7qyWIG5-8%<%IxYULb{L_~hY z%|Md+W%E_tDU^`QVX!G>G$vHaP3Qd;Nlrb^T3Da>W50nJvvxv;5X|m^ExCDb6LJ0h zirjain3;_dYPVkVMO}Mdd&Vg~yUwHjRA~!6;4%KU|D=dtJ@0=ubUZ z>ZIdz-s;e)dwodV zY`CU#iyIHGP3iL~-ykIVl_X??9I1$`qE$)j%#ODYfZwyA(2VKRfFA-Zsd9Ov z346K&cgT0;4<@T}(I-f>Bz&_tPZ|F!t** zZFbeYqxy7*?`Jlk<79_E?rr+N589?b?xCIxWZ!p z0q#Y`_LS4gPpvt@_C4mxsmjx43v*J8X0F;j&CdIJrG4w`()=)QrvV=Ea*JO5*vr1^ zUDuN!5{|bjZTT5IbT3{qd2w&L0~VVMHlp|RXT}k(@Dk{OQ8TfW9S}T1OAd}$y^~b} zMU22y-7krs+-Lo+q^=>-vgJj|M6QBoGd8hAC(CWdiGW^#fY+VTjC$(58)}OohTN&d zX{k(9$?c#ZQNVAB#E%bdumX4t7Wvq3_c}MYd+8xA(EQ(o|KtTz9FOsO0tGR_mulGo z-0EY)x?5hw-lWnm#ith{p2~i?18kx>FbDt`nQ8YTnPlqpORW(zS2({0g2oYCuLy{l zf$q*a6->Zh^qjVkh0=|4QdN1}4?I=EXq0X(xwofLU7_sC9EZ;Zq!PXR{zQ*CHDtfM z%Y#VDjM#ygGQq3ztV**X#ZLQE=I-|6&~GgG#JBblnCyL)u8Hj02pdW#~2LnogcwF!O}O)vCFnHN(in ze^dE}vI6=W+SG0wr1{V8kDm?GySy^9&sM4kes*$y-10gRs?ih+fSJ3!m3RDRA}JzE zMAJ!dFh!-1!Ks2d3!&p~;%<2%|KPQ916;l)v$=d5Ix@ied|xN9r&Q=Yf&!8rzy~@+ z$XHIk@bN#;V=$iUj{S(Mp&b$aZRTa_DmwJKfsyb_)A!o5`wm3kUh8Xn@zy&hhf0$9 z^2DjgIOm0nW|*i|XQS{_?<6zsz1j6$mWeaX6e5_F<%fK6!|y&RiQHGS*|C)vspzUV zlZS?s4NxwM3N%%EibfY~Y_`xrp}+H8A_dwNR8?6m9a;!1iEvZC7x_0cw#n&VMU;|8 z*{tZexh5B@4I;v0LX+5I}Iq*F`GECgXg#bCz*B&&+Z~pl0tf zNy)$rUr_SN6K-3`O4d14lDIoFJzY&av-RsR?eQvYy-B7`sF{%0sR>4cGNjISt#M7# ziw}%460rnhnW|E#@{AC|Lc4)fux3NN2f2Q|7cgn@N?JE2aIJ+HK<-Z7#+NrHV_Rv9{MVg?r=$*Zx4F9ALqJnE8~8#qk{`biZNFM8vRy4?cU5xv}ddwO?AUx z-{T=*@OpVBu=q{X&Zn6l^*$B)X7?_@1%dI7ub`**BI_GCb3&CaAETQ$P8160(h?7n zAuXL>ooYh|c zo`$xt4g3#6E`LX{Jh$DXZTU%h{)1cka7H=voAVvUUZBcJ)!W1I4Ps?MjYF-)MP^~F z+7h|Lo%t-;tNffDQ!lGD&x9R_k}f*gbB$9$JmDbxRUr1d`D-&T`wu>yhVss+S&kZ_ zUHi?ZE9;)Kxl*3Rbhgj#7@v4O;F! zf8G0gpK;ptbTVG6qz+TO{lu;Sh0%s?FvgZc%QnKxpf23sGObvJiMAn@N8BGPaKE24 zP3SYkJoln6%kR!~<9#wRU+ZPF%zK8p({MYt>#}<4_X?Afe4RntcofHUTojU^a3bZ5kYb^onV)&Z8~_gs1Jj-~)d@8Wx(+M;7qPy(*r=L7=of+SKT-b?6G zp(LH3U+jP{LxTMErtqs|=* z6|w?N>6P?R=7V+uhl8a}8ZUPfM|&I4k$3Biea>%e&Jh+0`H_SDKmLrErC77AkQEVP zL<~}I-0xO}F;NS>%^RG+MsZ7$B&p^4e9ooSzFMez9K$@d4eF$?P8sQ@gpVc^0{^1` z$;Uz)h-X}`l@@abc=7C=#Nh+K#O+ZNM~%^C3Zw|VqP^S0T3Lxe=b^PveT52FVHn`_ zXok0`GO%OR4@uj)#=(7JWd7&n+Wh+1;&m_al>2Q1q;dbqSHuu+>P_y4u;wIh8B*X! zb6~=7NPn`>Q@~#o(p0S0V&#;^8joUVnMO)ssf{=hW}vlMsM;#jVjC<&(?BEvBpgeS z?aMOU4o{@hS}d$O!vTee|CW}B$n7%DC`n1(lnx}3&D5r&Inh}BgKIP2FyW%JuZhJn zRrC`JH2`Uut0=hLt4c>44A0z17MI**RbmP#Y}y_!7JNQJ&+nu#kn3}FvSdktwgYF9 zTSDi{g)hrr9HfOq#1v$f>}V>{JC5-cpyT@JaL2DtWy~aRcAtB5$*o0M0vxH@rv7ZS zIjY7aCEBs3Rl~u+QB3iQ$jXQ~v=YmmVX+PIx<|F2Td_#R#wrY}NENC5k=TO5fpG~S zzu7|IZcK)cK>*M&n3(4cP6-fIYRo5>-@j01vVI|`Aj3vRLmuD^;rnZhH*o}&KR0^+ z_L9zy3!ZNV5``IHA+)#VVk_F&zu=UVRZ~;&LHtM%b}S$zCL9^YxsnnSf~*aTHKLlv zGEsXoqbgT4hS>V>paSvsq6u}mQkauszxDMNrLZ;{MR$N$U{81=2}zNLc#h;(Q=x(K zM_lGO@Iu0Ote0WYlyl2<#Oi033g1$h26v0qno(UJ%IpEa8IEK8PS`gg8chfY3|7b@ z2`U(s>lc=5bhmwYzw=o<4gd^H9YZ?R=_wgc$nqk2gMMX$$~t`Jok5N|HUr@?(rofz zOfYO?ktG&5s%(4O)Xn?&Iq^arLJzv94T520ZruLpxayfC(p#%^KZQ2mA2>+wrA%SZcJO^;ihMtHj5(T6aYF0V{-@xEUHcUIbagtc#b zRPxskYx33ZC-u#=q5#7_`7e3$D4L@xUJ*y#nS&1x3(M*w9t#_2VA=767dRfA z`fggB`N&D-FWGoxbYL~vAQB%IuCq(b5n~Oxy&XW#!PU^t$DkeQE=)Q_ODM(@ViDqc z5mckEWr+|Ose}%MVbwqnomPydor-XT|6vr3r3$SgA1D%y1)m+!{Q9d|XYlJ6fJnBZ z#a}#D*dZ+7{+LNLEsRPwd}MlZSsV~tzgmvNnPnl)sa`!Wrru;nw2SbUAYN7+9v&q# zpoAqcs9ppxW34LglqVFHv7X!8Xs^cq#szX!>mR`g>#Ajgq%Pc-xKb4# zpa*VSe61`XW_oat_m729$_5FJ{9E`$|B8bs_*ZG}z!a_MigvGT61o};sOyl1snOM4b8J_VUfXENaeozQiZ$(aW(!hyWmZ5T7RE;MK} z=FEPPDXgE37=G{wGM$qyNsuQ`v9rn>=j8zBguK!3f?7@+r2wLI=~RtG7Mb@T%niE4 zobfa7N9p_X*ju)Ju)>Me7T*Rgo0FDYx4?20oEVprl_ea;5aZzDN=-?L!RG|G+_3#2 ztA@F0VYS)V>`Axmza4OEtT?6LZgbh5)Ly4;{|b3KEG*zJO?}Vje;$?lO%^8m<;#2E zd7Pp>zK^J(T}jHo#BafNBH_d1OZ~c?0l35;?>Bo>=WCj8`{&ctOuo)P25!U~nkyS@ zq_^z4yl@R)M;v9MIz_m!4S@}!m)nKI#YO0|O0ABj`p#!-eo?wWAlSyF#PZdrTb`=1 zBjc1;XBJ}t<4F^rWzuKf=BRxC*{%(n7u7Bt1rL}ZKXmovezrK-P{l~xs554hj0Id~ zDYl$zne@=B)}c3>!o0)1#irMO_`b~LOlgTPKx|RqDM$qmLADLi+nYJSKnUmhu-x>E z{x<2hV+c(WAU$7HlTbpdfR0Pqh8Yw>s=2K}@9PYp)r{m#q#Z5VRbd<6iKVa!r(LmO z+i@^#tNHSq>{LgnMNMcOn$(>wc2XV*aDpEs`meI|d_$FpZ<)6!PahrFXCz+^$vyKA zC167|fDXg*e@JY3sy!=Wuv8BX8Zyk7Cw9In7M=E|koiLc?|h!`?Eu{82u}N($!>fm+kLqEZS*T}&iz4(7g=5vsELx|-$3S*&Q%6N?=?$Kcfq%$i1qd!eq68aOUA1L4#R z+JWna>=OrnNm^69u;IN?OR}v{C|#n2=)mKcxw+?fvRn}n*XKlRA|k0V>?mACg9d1o z%eG62;c(W&2d_IY9dGvVZLo-3p>X$SY3IaO_Q>MKs+QDhLuMQX%&62l^~%9j*#*wt z`i#8*=Q&pk9aUegMZ)KkQ<|CCa{$>;ri%NoOg;7%D4vO!f6FJe6Bn&Vr+f-JNVxmh zV@KABzp9am8N-#6_{5-xja#6GVULO|uf58xaZ3vtPd8K_&MLj!RO|61$w`04Ubq%b zCm50H57*EUq9@dx-FS39l&AABpO+#}p$>Y&>2dyyGh1T_INWa!)ln8#YG(Do5n2t1n` zBZ$Dk9%Qq`LVM|pc|vYu`4X$xbM#(&>0LPwG=ECXg(%(hm?pz*LU!|equx+O`I;L= zykhu*!^s7%s0yr(ZF5iX(l^9Ggn@!6R+|F4ixej7?BD(4s7--mXUzD?4+1-v&}!kZ zXN~zNGsq5xIz20~Ozk>qZ1pbMqmQRX9ndegqf~iOtknf_QvJqCLIuo=$r~1}gtcT{5 z^gQ$}*=w7>Et8h(5hR;t3W5l`j8A>2Pe2#6;R;=#@@a0jkB>cS#;(iFPt(^p@3-Lr zfEA>Z7G*={)YyxkeM8sEa7#1#lQwWMW9gCj= zQL$#q2@G6eRDb9HLZ@a=2-qFEu%&%rfgBJC2@qAHLGIiBI1@}L+>IuWN~zI3NTxac z7yMfJPF6zyH8>2n_QiFeV2=ATPviXwZn}8`hr7Ag>T3d8a|+9Im47z2kIAOmn()t3 z94*GuC-M&#c3*dhx=WNIJ%@ks&5eg2KKp=)DY@vMs5Mh<|1uLq>Ih)IBP&uD_ zj|Kusp&@C|p%q{hq$&_AU^7Xi+i?D$79dRi6MMkoVY=E9f-#BGnTRdPG@i0fCw*Yb z#?67JYE!(F{P=3%~82nJxvn z`1Wx>3QUBsArrI)%G6nO`mnISi%u6`g(QrT})l#a*Qqba6Z)7a;~MIkhVEcwG_1jN(4?)FeW zCouA|lOr6LMFhWZUVFGzNu_%Z*rIy6fwX4CW!oUK-`P~>-G8(-Qwv4Zg&VmmQmK-*{kuEIo@hX3?F<)ZRZgqq*rwCE~R~Xtq`Df7Z#nE_bnh6wKU8LX4{mDgT>ha2cD~DmsBY)#! zrT(T5U0|JA@)bUiX^?WCvJ?RseO66Dl~$~RspSVUfe%i=om4((1mhMiF8?2+QM9Av z98)wKxTUDq)n%>R&mTRH2u99ZfbU&la1VS2M`0;HE05bgY?_p!6ywg2351BQ#`?PcS@7F+QH#KEPc z7?#GOzzjwdir_VB%HZ^jE{;)_0&@@_yBQ6vKc1XFh^K*aNr^IS{_F_Dc`({?a1U7W z6bKrwrzGq?USrf%{3N45e1?F8ip4AkhGCbNop%iL=Me7=S7HyXZ7J?1MH5RERUt4! zD{pD8jtzl91*2*Y8nsWphxZ5JnPpWX*_7MJflQ+@L5K^<_((rAc`13dd%gtf3+ZZ^ zwNKU$(2_Q1ySs{_+RbetG%4CnZ)rC%uF8u{v99FX{#T!^$^#LG!8Qv6 zr|YQ?v5E;dLWXRf$75P)wuMxzojdz}!aq<^mFi6P)LK{`9`((J_1~aG#ZV$3u%wNo z=Lz;Z;Xl9iCVrHDbZEZ{&3cl0itO>-=V(an(jnkzb!rKbh>WfdZVPYe>?Ed0j*rhf zX@AJ4l$+b?@+N>pL_}m3lQa{Hfvjz8Y^<#>od_H9lclAkq$DNDgBkz3BvpTtXx{-S zOu^EhKYunir|l^j0j5!9vj(sFxi;hKBT6$QZT^WJnyIWV*6O^rzPM?5dl^CZjKo^g z#U+30_NLOAo%X&8YcL+$tZ44(c3;7E%I#4pE&6=dXT4nS_lo1`Au-lq)vQRCqE%ve ze_79w`@Eew^UW|paEj5WznSfk92}W-d;Ukpy4%n4#euD6tiAX&Bt;{p=^3(z$n_kV zG{Z&=>9k%fha!jdJ$FK`uC6kR$Lc?;s$NYCT$q}gW<$BSNf_WMzL%G;pDD@7ad1zm zu0ta86BDsGebK~am!T1}aHV!GD$Zesd2vvh(6vVS7~A8BcQ#kXVhPBSwnxmt5ZStr zmTJaW{Q8-1{ADsG8_5Rf9SgOL^ss+ADBrvL-5hm(!EXM1lXk>eJ>7I)@!M%WH2=V` z=VH=f`ON8E8}BxeEMpR-BlY*V~ND2+l_8VNpD+!#^~e%3bupi z9VQiC%nb{YzwEaqc~K839^uk!4j2cY{dnb4OdH(bGM!B4h{9(xXm#9aw3>&Ka^9V; zHdxLUgolSiL6*d-xfB!i)N%#%q=m#{z_EH_l#onpDb-}^h=~Mf1@FORSW29qV;$R3 zo-s&){o(KfAPRYVoWJkKAZ}mii?l^E-Es7Td<_8x1w7J|z&vl&PzI*4Q6KA7z~9Nn>XpA?}GbRYxh?i zM^J2jdEyKonci0k#<~iH-4^z{niXXy5x+Wg-~tmIR}!{Iy7;yKY)R*iXubMa&Aykb zCzNy@9WoSza+#8aL&(0~PT^a`I0Y{`Z0ppq^;we%xPR7EU_q%uvXydIz4z5RdY6K} zRIV=)HvVxJQYgn;IW)A7vp9S{2N_lvFV}yZPt+Q3kQu7BTDd;|R|?eMaPVHWnbRH1 zw->tV(vL%1zo4)%7dpBQ4-c*8iy0#4Fl!Pd`g;8MvauCBz$8{%OxZmgO4hO4cNQOD zVEUPV<3Hta`Ycl^ZzX4?uZ}be-Bb$8B9!x#j$yxL*Sl_Ym6)auCk1yhaI%4U_;Gzd z3%*5mI~giYZ)z=Yn%l??ffl@5>whg<(0OkyLwB*e3A*7Alk<&UiQXsN33uBb&+kO~xTO!VUAq^Hw|&gMngXL>y5+E77EKyE-h9oM4ghhs6d*UHE8>Ub=* zM6BZ*C~@@+O<<6;*=GTp^W=K|a0Ml$a5L-C)r)8%(le{2U(wF|%x=bBpQLF=z$fkJ z?j2NARLB(V)nqp=L-gX}mdo`s!cX@l`l>!7O6lzR&Y2|yC_Tr8u% zTa!T?Q9EkzpEX;90p5(*lURMB7o00=o_srkfs0slvn^(d>?I)Tp&2fY2z-L9gS zL|A)8Kxg;-?(2m==lBA56u;L{A;j^OC;<>LOH=_IBDDODL@?*-tQ`-7yx;nmpJ z6OBiv!Gs-4noX{4t&}p;SDqC2j2N_ zU45A+7X!h=z}Rd(BOwQp8|9JfHqLRMELW|c;g-@>{Y(xFgr~6t^0-~JyPYhZRR$bR z26iJLAg9I~-yTL&&Ju2Q*l#|%2~@&!^PpkMM9GdIa}{6>h5+Yh8)2zBvvRrwG`v zFa#koQ4VX1MpLS)e*92rxV||_?n&i(uEd-cBJ!=;sB2r{ zV5Tas_S83allWbh6jpyGl7?@;xl5X8ihPI$H&idwYGJ497UcaSgUYUmsRg%c>y#MB zYSudFhGN`OU+Q5AD~kJLT8xoh{IPupc3S0H-OQ5r<$YOOhu@ZN14LQlYd4a*$$5X! z&GA%Gcd4+%YjRq<(`IpvZ|SkdkBiQ+Ew_sCkEjz^GJlN(mhgDAT6_*1vWj+m5`&~{ zP;CeWgXrR1wW?mtW=KV*N7|S1FZH-0+0CU_@PR5Z#;EK@$tGi18#f}RySAP2z5mo_nv-?6rPkzymC?2P zs=bb`Zua|UnsRb3t(Nxh-^=sK`}GQtd4)cRP$cqkCVC*)pu^RYn}~_-qv7H??ShHf zjRNVv+@*NRdE*c9KaE*Le-Zq4e0*G9UY?!~mQq#MfVK*j)a>rnWs3hujySz3De^wKG&1i#O_VQheJu1qBQa3Wg2thcO&oG0d4VK@t}a5FKq4L&wS**mEsQ{sdUE?m0+h zH}@B{S#P6fZ||+lCkJwHaA;^~EG{nEx2&JsL>7Rtu?I?0l;JleV=uo|68#{q`t$B} zD*wbwUS8hT*4Bb(PRKY~r)mjmp!Tp8$qY{q&s+@InnL#pvlQ?^9!yL)N6hn&o%M$2 zkj9Ntq!8x?FV(`qz=%e3R7ln7;*8f)QF!=sRwJqQo7td#T)Mc7CZ)%m!pqW$_p&bM zE~QdUX5m3yL>3Q!PdgULJQ4k=7kr|tz05UiX{wH9SGwKyacV+-bR0cdOA{A=6`Qh< zsxZ6`j<9QlXj4{?JIM;&Y~K+CNzUY(A|ptmmP)R>K)G zsdH_n*u<7E+qBohDWVcWDWW$!UGeQ!?(z8<`;KbvDHi;}N(H_a5+@J{Koh5e{7SA0 zDe@k7Nlc^(%{Tw3TBH>-rVxF3VbEOMA3jB*8E;;LY|m1D6Dp1v6%}ntv*@cKdye06 z-LW`C;qjRF4Ue$cKfJF1@~b&HB`sOwPPT4Q9Wi4+)|p}ei%d)XlR9bn`1FLL;}Sk@ z>Jm|%&>xdNdcNZ(HQ>LSAV=T2rWON^Z(9d}!~?hL$;lcAG^=;js~s(pGZRhpnjt?O zWlK%u%-nWm=v8p9VN=mNS@<^$+;_*4TlJWK9KYlxP zPSZx(v=bk_}5czOM%eW~|)c{4DeqSX5| z(=8;JM}P(rj}AsxRalU@~hQ$`Faq1ZUGcuuyHO&Nh!r#PB)58@9 zY<)fM1#i@D*OVS44p?|5$S~l1B9k2kvln1_DA|8{S&LCz#Qhs3gDz&Fv~BA%6{>8g zZxPjULeD7w&gxt8WV%pKwc+FAaOq!q7#ZnL5;8HQpCqZiZDbFIauZ>6mO|`*?}qSo zShTEvVGqQX_lTzwl>K_=f1xvv^~#>h8(V5e05`i6>(mbE{fz9=`6vf33iFY}swA5* zh2TBA2@VrR$p9@3hiLTlp#U>2RVkHG!Qzo4Fr5G~B)oA8Y5)sj4A<-!FqVkssrrp5 z-j|dqLe0oh`hM)(b%h-^$a*w~(^DN03l|54F`^_omLkx0e!w_1mLfMm$%Mz>O^WTiD@?Bv;riR|Ex<4Wd=hYB;cDIS-KsqO{E?9o#NB$?7QZie({ z^aMIZl$3ZPJ+;8D-Va7H=3F zu9;;TDhMIhv2)(#NIk8f*I`#cSUKlDJ=_n$B*Bg3nogne%!1_X1Xt-UYez#=JB4rrj$znetf#A3)-w$oawCkpi4{elcO)^wVUBtk8!d z2=n(pP&wBwFNw{)dr7sqTgHp1@^yHS5uj_HUYl*eP{mvGTgi9k^6LT5g?SX0ZdHHsem_b#p{Q!&sGIuzWqJ&$NF7J2 zyYPbJzhWPDP0s5(peHmUBT)v}`PYf3$0EsrO-)TNPj?r#5=tijN@~Y@CVB}r@f|gZ zCV+rWH{x;-PR%=&LIo41kl&zFhB5nVFfj zS6_RqB1?gSp_0VTKp0%_a10SNz$^>^axOcV3!QUEiKFjHs<`=gqQ1S9x`D_+9KoDO4^iW#vAcD~jH6TnmAAPf65VQR)gvoW5wv z&m95EE3VX_q2KpC)s^8ob^*-#nYTyeUoG@8{_TQq=N%W_5Bo$Nn_Hido?n51KX1Hd z1g|G&3uTsuoWqTWfT9dTI7a^=QqpGY9MS%yio@^H0*PJGn1+L8D74I;ZHfZv4}8qX^7lg8pX zE>kMh_EQn^CFh3;l0!uhA!IltiMp&JhR3O0=)bz*h=D3KPk#0VseSsyF9eR5Kl+}W z$wMBDHjtO>K3;D+oL)d5AW9BK8?WE^jVtE5Nh_!9DLA7>h%D-BQIVOCqsRRE=g(Mx zXkYVf143|YWS!Zwe3}LK78!XfYQ>mo) zUrR|DM`skCv{$!WQNIZFfj`7WH3o1jF%(iE)hu&iL4#84wOZL0lc4$?;0c)Ok&J_b zeUONdDsL>aV6mjz#06oHw#{bPXS2y-yV7?M`$A{72945r)jMjvcMqJV z9$x*i;_hgJo8r~x2XM(zF1b3!C+1&`@BDb+H~l@^v?%s5iP{Pp(O1CZ97n1yNT`d! zke`)p5}Aq<>#9y!GP1yPued#L*o?<29um)GU-SkxmuRq$faLsA%FRBUV)wI6LK&r;nlwz_=CsVbYlbbiv*RFa>i(dV-b9l2Lx*b(VUy!IF;Pk5J$AyRQ zrPmhpTo-Vty5@f}_67^uoX2!E`1bO&!{H24 zg`tL7$m8)>g~cjWGT~R>dR$SJ?v_BXf8EPV<-ksOY^?BUIjfqUx|S9s`pn(i)zwu7 zCwj<)fr&}r_CdxaU&Ywi7#d#9zT>PJx~9QN+@mw4n)^0l^(^+Fr5x)y!vW+N~T(PYPn$9 zs!<0d^ifht+9%>hK*Iz8P>sIHfdIYT?@Kjc0POtz3FyPXdQYQ+#3|?J;D=>>7xPeJ z@%l|#pi%Gup={)wr^LFATuz35KJ!Fk@85VvBWGs2A+V9^VCz-JECa%Dc-bu40CZIL zX5h>xl=98G5`KhRq4bdq z3=H`yrBjR8L8GMW(|6~u?r|L#Lw6V+i->5p$De{7KVF4_tObiDT_qQpd)~5yn9cse zl}M>0lmTU(^dh&qPO^l~yYaULCN+qt!Q99lFR%%!j4LVQ7yBZvcR@DSuY6D6)`#gz z(fN&n*VLD}e#4+mCiZOY9(&LF3~V;_(Za!G^yqqY`mCpQY`&OJ^G#`-eSnY7OL_PU z;B069s%(c*GBHp(jT6_so3g65nUJ>SHUf!S_HFN+!xrNa@tdKMo1JCgJoolRm6Q$n zi05~CNHWhx_~mu;I+fYr^{TMNz-?%yG~vzD{OU=0(<{^E(2&ef^WbX1_iZJ{;`<}F9?}dC2 z{e(lU4?ANEmse{_Gueh#9fNd-=4s3X=#6AEUX?C2K>rXM(CP{H;QXqE4#<=2-OBx0 zlho@%@oXai@Sn}U_$rOuEamB^bGcFR=5hCoujPApb#bLEKFrt_# z5n(a3(x8VipU^=5wZqQDnycB#To=M>*?+!iTs)-w?jZYWX&xY!M!ybBW!cW zQ5%bRl#dmN$RPNA^Y39-`|Qy-v_lGZDQQqSBDr8ueA;) zUtixz;j`tEr6;Pznx3t?ie&^BvbNVi&x;O2yX|?G;XsQK+?uCbm-U4Q-86&vCEvzx38p}*VcFz>4UATApBjZA)DcT(dQE^i;5vos1wj#$AkQAo{QPg`p* z=9OMvJ}V?&y$8DEb@H^|_H&4H?EVi6u>~qWuvnWY*Me z*yCBJZOeN%l&dMoX=Uj7GC$FEF8gJQHn-(x=Yc_cOvKik`v$cU;Acoli`W3yN+aHM zwSQ9_9ALo@xQ=SPHgW0dK<`WD99XO}qxqsMH6UQNnQ(v6TwA^#K4F z_Yiww^lS$bviir^?3|)#-@ya?pPBAE9j{It@!>8`Kmd@H%+@W?xES&DOVUV=@7Nx` zf#5@@3-&R-k49_B@`LhZ$1d}>cMbFy*VDB(lFitgZMlnOkMR9=uIAA6*QaZbi%IRC zlNUNoM3YE&&yz<|Cq0gT+MMq7gNM{m7pv#5gp(4<1v|D)JtArUd?A(sD_wQD=&+@s%5aqn=*#8Le`@i*gnQZDdzB<3{{nn9O1aLb{*_l98{OzwQl~ZF1qV{ zKHcqklr$oz3vTXqUsgoMoK2hZ{nB9%mj|1O8%)4Xv$k^k>-6LNuU~VlSuekMHmQQd z!UL)j(0c^0{!Xw>d*4*BrFCnJV(p#HZ2MX1y1221b?+5=r1kv0C%JoJ;pCycne#Sa zZ~^vAd0%aYyqodfDMLD_QKO7BY`5`pmUfn2E4u4xRE(AC-N-JZG0w#m&x z!CkW5`z-^yR(^4>+Qv4v{kjvdWbUfH;DPwHK6K#dm|d(MeuC#P^5n0R76w>FkfY2o z%lc~R?S4SdWsB|Uw1MkwKles#c|XoNey;xgwT!Yb%Qgu{WSx~x7p>AUnTC#vZEGmh zeWd?qGjZ-Q+)B%|_9@rrFxx@X&I2Roi#$t2!Y@j!M+|T(_YabAz00kRH5ogU$lUh)VTO_tqnhFS zdZ8{DF-*)TXPUEn>nm*KZ(Rot|H8wnJzYDPpx0A%R4Ye|A-jq3 z_JkkJ5PaegT;T!$Xyg5j4xaxcmI;Wf3EmrGzjg-B1bC11DeLY0Wy*&Ul@CsqJ;a8) z=h8k8LjjZ^^e4pEqvHv|L!$1|ifJXx9e9N>3Y3D#zSbh;l{mIGaY4rph$<5+B_(yC zp%-seKp-K)sZ7U5TA6TbUF~JlX1nuYaPF6~$j57vMwk&5itC?lBfizl1M4+{hidhSPq+9fKK_G&t6YG^pRk9S+%7iTTo z7*#j&QDFsF%GW)a#Zwuabi%@~Y<>?DP;dc_Tp(T$K@J=@yiD?4iMI&jc07mon2^t{ zAiu?tiC<6M2ho=q)YQ}(D4cJ=jKhZX@il7oLPp4kwTzLKAwrH5KmpW%F1%ieZh(?N zkMy;JZ4L@&HbBPIL}+G?Eh;du z79C~;Ec!=j+(>15u35>3G$&tcP1J^=c0?&*pK8v_mz*dbf$27n9S`j8&bO?&k~AC< zftSjii)Fr?LJNb72dmqX*YSB$!#^|M&7#QG+wLPS-jkBxvaU8)lnT2z-|D1oB=h$j zGwR&0&14qI)C`8Ec=&zp?v5egreOTV%5V3d);DB~Mgcq-JR8Im2Liq@$C^fWc0U>n zjNB0NddEX*>?0J$Psmyh&?_d6vFNF|O0m-ST@NZpGbu0C zhP+l&EElN{&zS+eEXG4X33}7n(t$kP!rZ|f0WL9r|DNuH%Q~O8+umMUPLsRN9TiC% zwE!h~BD7zt4xO9qgJ&p60@ngm6se1G~#XKVeqUu>dXrCw~Fpn-4&^JhX=`AjL0a4p9ZB z7w#uKAdi(D3<993u=U5h`EyG5Hz;$b^4xiWgvi@q0O8m`I200l4nQzNum%elO}8Hq z-b!4rp9fIct(qVcZvOd$r7TId1ZDX#6=@2aF=>i`gpO>i*+n@FI z?N~{L&kLzY!mw6N%Cy^LRTfZLTYXI(E)5^A*PSWMl$x*n0hY5@cQm??C`U=e>TIGX!jkRHU6&3Sk32KKOSHCg(+xZ!1}Q0nyd7^~)B zX1bm)4j_sxMC+yEu;})99MXqvmsovLclUCZkBhhIEpr+!=Oa%-Wwq4U1dZjUT$=YCX}a)BW$D_C#WLp`j5$2)_loue*QBvbxG)$#I)Aw$^8? z$NW~9yyLL?0%c6oVN-ht*>#IEj@!w;;g+nPFE$;7HpYZBXrN&rl{`TafYLk~3q@A6 zz}AX>`#3opdq${GMrl-OImi^-9}^J=WF*O9?W^VX-GUn}JZg+luoul*#RQMuCp*B%;H#ub<|v?SiZEB8(e%ke1z=o%d7Ht~(RWnZEH#7>&7qeGCsh$xNdqBJl?q8wpqy^jMglvb<@n+LE0NT1b)HTR!f zY(W_n_!%&>le0g^_;b0-Xhig8I9oJ!Aqy9#AB+ly0D^&L$rMH)vaF`vOBRBb3hMja zrB8h%a5kj+m#0D(XZ8G&<(TfjTi*F+j0a*O`qu@n6hHPV38BFjBsY21m_(nPoRlc# zcx`<-5L3f>aDQ8Emis^_55*L9mZ_C2EibnSUWKondIn0Z(lc#Md}W?SfbknmNJ#rZ zg{p1$@D1g%j+d^~cpxd`WLn|w0~k!T#5)iGHcN&=iXyvp*<)Yg>QU(a()iFCgPumX=YfMW8_@}%-M(OA&CCOP}xl& z%LC~tngma#FKjRLcIvRPSOjg;*fp5LyiQw?RC#$dCdUPbeVAJAlSWem`jItxw3{Kj z@t+h4t?7&;yt`FCq4N3Zxx0t?nzq}&S)|!nSRS~)(OU1@b+kGvOxn=0=@8mBlGd95pc-2AA zHr0G*)iUepV&RL-DkWU%WU=3zhkjg;edT32Z*ci@OOu@!H-E=GUomLJSH9i#L9 zu6QBswurEf0GlnBk3ryh_$gRc#uy6#_&)HU_p>jHyap}WGu@e6K`AtZ0Hh1!p%D7v zWaqXc-;izPNm!7JJ`ro1AE=G(xw6wzPCy3GQ+>t-In2plRV>hDPM7fNu)GA6!d^p~JTVeRQ>Lt62(p&5P6 zw$z8Iw)fT#YTm9NBxKe!0caoqH<|>UKgFTU%QWvn5YajKMt%Hxo-U4orDkQj8i*-w zVq)^m=k_1(EA)!(w4JgeABiJ-J8hUj2?U`b3fL3|X|hX8cC4FwsF?tU19$F1Vcqcd zYdflVJ`^DuF(ZC0qA+YC50B=6k6{|D^C!YbL2Yg9W8=E7#lfzwur1xjXA0jpaSj%H z_uw3w`{m%KA+&WuB9Mxbm4-$bhg!QA4PYER8UBJX1G8w12{Vsw0uq%Kg@+LX!VQCs z;n5_u_VZjWI+;!a`Li6LGJTKVebC>7w@P+-1{j1NOEkG@-YGk2aq@F7gonn4)0dnV zoz+zY`a7hJ%1|#8IyyRDREW&R3F2Wp>qPygeN9b`1FqXn9C|-cT#_E_^70(*Qa-d+ z^VX0f)t2MGJ8bZ#a{IkNK73|+Y?8g#dx_>iZ8r7=Mr2y}BoSW1@$toRsc0T;_UNub zMBkmYNnj(t@^_`&9@|?50-V9_lgY^27@zu3lHnv-%h?0f8?GmS6zrnM0=JK0 zb96Y(T)VK0Ygwp`A%VY>IqkD^ee9dGpKf|u=CyGW+;62UkPrwsgJe16Vtdi<_$f4T(k&%|YT^}5q1rJd7f890mMa0p7Z6y~X0{NGVE@`Lp!k^~od_p$^P^BRCv;D3 z`_GPHtSCzQL9La2PrfUKvQ~w(fAxj#nzpU2vm1(~;g8)o{}7Nz*Aa#$cNcoR)!S)7 z+z?r&&oYpXJrsHa+obV9;y@691`*cAl=0d54I&cul>Ks;ix~K>r(yqE$P69uo;{S= zzSQv1t%{Uyr#^xp)e&SmI!)AmabPJY?_d|2=Bgv)>^ypQAEM>Qq{_sole--bGDV^s3?F43Jr+{Y;ua0YEI*D<_a~UuSo4gn_TW;Y7Y8Tg zQP7#)PMo{>6EHRn(XJpT>fiKC`gDd|;omfs%SL!mKw-)A8kzAz?3{-1O;$r8c`%qG?`(6{EfQ16^dnER1aGiV*~5U#6LMZfzT@b-x-n## zWlf-E15LXUOqrzoI9YO$?BxVoMW)s`*TD$*>E-qQ-ZQ$6fV67rt729JSVM#J{d@W% z=z!TH^`$fEE-r#WdMdjr=UD3M8x+}OSM@s_YoL535TQZZ3Fxv^L_q5}oKst#@o87y zIFs-DLEzVMQ@mAf&F3Q>=RlevK>p|3Y}z+ciaW3=d>}vpKP)nZjHQ*)?3fL%c*A|* z%rCO!1nRgfW)yvt;(XkE`+-e>{9z4JKVs-J@iHbYYJ;LEi(X8WUIr-h>popTBK=Zc z*WR<`4DnN0J_bj3Kf$O8zu3Se(1UAz8U`Omm|`88sY6l_eP~!Do5n};MQU| zi`{?XHeTzp-R{xt`IM@)Y{B+be&MA%GuY|F_I2-GiCwpYyKa9>EZpQ_V)lmfVDP7M zD_sk{zMX7xP=DSN3qew`VeW@^jNXj&jGR5O&```diGW8H001-$R@J96I)4u*xg5^t zJNpO#QNVDUhFTzmf}|DZCmU{aU8whsjdbbrnk^7#-G==U*TG}~=o(a0H{`*M@rLN! z`gSK+u`A#AODkhwD%k)UbxZ!xO==lv#cvdPGl*k!9hw^1bl; z5$xKkVcA1Tu?Poj3i8nl?S`A-@B&tLB31#PA@R!&liF=k`l z&Y{RdXq(UsbG1it$zTDnB)lpgByjVX;S*%BKv0x6EqgReMHPei)k^KW=M(O!eUBEt zS6t`u&cdv~U7&yvrvR1MoCZ2%QuNNoD(RxlIor~Fg3M_zJ6YA-E#s(u=Us_n+nu~d zqc^Ot$XK@XjD`h}=i|y~_G28~wT-r|?#yqfbjM!biU)V|ec#+nH$x=bcL`35KvOD! z^jes&#=t0@&tPds59#vy+1k>-a_+&;^9RKP!KXu zUY_+-QlMFrhIjD`&k0Z0^=ZG)*F$E(H#GLgF;R;dcp8tO_tbmQe3xLhf2X#*X^kSi zX-mq%50j;*L~q~TBZD3i^DB?f`ta2<9gPwK!(oI5zi|izR29}Ymp0}E@f4Aq0sw^i z=-@y&*aTvB#vfZZB%L&R#7#*@;cMj~9Cpg$z?LzW+WGYPEiaqR zcDJ+8#t%8DP&O1UD2`Ve5--{WaxU4A8$B6Op8^D_NUIA4Cnr~`IsZ|X(&fd`41zcQ z(Uhh3O?n(k&B^*^iybzG^g9rnJlP87=vhNPLi_?f>d^| zw;Hu5Im5YWM`ro;)Z8d?jP(>^b;6DFhO@o25iH#Mj)X+tg*FCP!en&8BaW&_7;~3$ z*hWpcQOYw64qMz{5$&Q=nV04prPEs6Oq|m+@f1${^OVg?S&^d203k4?gnvHxVpBy6 z9LqX=p0rFdxux0XDJMZA?{noY4Vn5?OD;EJ6i0Bh6_NPq+QU^GqKIQxH{SgQNxXLFnr(zAmAHuRM6<1*xL!Is#i_gzrbyD5*KS*Sfx{Gw~ zkCsfSst9Zrxk1Sq)G=t)*_Wf_^NE)+W z9x%RDnNhIB>ZM1nrLJdJb?pnb66k~e^Xi5@Onor!hmRWi<=lp`F?@O_lSr*NPhW%) zwp&rt{diay7yA_NJ;L#{3>8wL%sl_O(2px>(pX=@K?xjfYE9u*r1aylYinHy2oH+; zZn{xT%m2LMkc}Td^62Hw&G(n8^n8R3Wd@TNOe9sA85mYu91LZuXDjKMm`+xR29qa9 zoXBjK8-nNEe*VJ_M1sUWv!)SL=eR0kwYV+Kwks}k`yQ`T_|?)|C6_ z?>x%{n{fZCUJVo<-Egb?*kv(w&PYp3OF@zE(i9#+U>DA@l7+7l`TCAG-%5_nTDU4@HU@t#`$ux2TeZ90 ziKWNlkarWSnZcdqC#AcffA9v>ez1{F9n*Vr|M24SOzAj}9{@zzWca~S<65}1UdqZ9 zTa@?eL4?UL92j`VNgULXu(0N!aWIxyg+>BAI~3KA7xYJ-+1pqLU|eQr;ou+tB=i<-bWA0v_T++A_2H)g;C>${erEey?E-& zWmAYv_mgA8d2%0buWAA&ZCjC0Zdb$a9ZqxN=B zOz#Xxu`yj9veTL^F-h}(cZ@h>rYEFq_xRac&;3O**isVI{2`0F4(y9;@Y+kP>ArUq za2gMvb3b)xAH;Tf_Ya?h{%>xk1*+0^utp zwB3FCi-?WY0hEKa{%2>-4*eYU^=2olh4!W0XEv$F=&0oY55H>ai|bV(--)}={omtw ziS1oCGlh1m^Q7x;NNwvoY&{(43A54{r7>e`Kk|qL7#9 z0CGJN)|U0X+Hr9Uwv#m!sCJW&aS_W@F&(fw3(<<$C z+kX$p*!S!|B6B48eY+laUxc{Gr%W7da|v_vFr58)GTgv)MlD#>Z8GVb!k)v%n*Y~X z&9zW8i2dvG%Y0H{p_=)mms7=#YMvFgmslzS_4?*xD*ihQse}=#Ph;HBt-aymEPtPC0gI zqci6^9DLPn-|LFpm1$i(RBl+Om?>{MDEoFFuIbFwJZ+=52zC9;S7xGQmU?S{GMRt% zvzrqnE&9_zXHSrunx4$Fv-H4Z&El?ryi83zEr;>nQV(kkC1c!Kh=6piiwG?43BNU4 zH1ngNJl-lejqmv6`t@|NoBip$v#@w;b{YFfbiydCQ{PN`Jno?dWm?O>-`-F^_@O28 zyrj%j$m+FQ*z)?Vv;1&ZOY^z&G%Cgo^MK{q{$ak5adA<(s?=QoUYr1+5;SJN^7{I# zsauwFK-lgqiHULdQJH}6wBX~ku=hB6ivrQc+uV_ba&~%Fg4_Pj`RN&#U@k^-BY0*^ z&k3a5)9StwMUn(133!bQ12T&jw-JGN%CU_@+s%jAhqwgH1|((`<52+r@C~1(L-eA> z1l6bR`{hNd_t1!VIo~{{mwV2LwhFb}l@7gM(mrTDR4w&i7*>3Kn2)c+-$T#ku$yb+ zL|SC?wP|A8e-7DZ@$>q<_484y*>3AJV83jZ z7L6ftHN0`%qu(^;e*8BOpl*-M&Sly!1yOQCi+0hntZTe)!r%5f4f^HKJ$Y&OxC-Xw z^ljy5hUI|m1xp3eXg7H6BE|L8on6;(zGV6GAR1tCqA8u^q-=ByV{#k)5WPRU=2!k| zUri@I(bn=X+PdNcA{bfmro)XCak{@Y(&(RNRnBUy%pfgY$fx^7-p) zLVrFLycjA9B_O;&9^q&KG{f&tJTG8UN4JYUYO{mKx6Z$72t|Hf#}0lS zf@*(}`W1L1OpIN4v)%vOFipq@OYbECQ^)|EeaU7dT>)MlUM+ zN2O$lbmM-zE!Vfc=EBf`NZ{w~koOBzABmSu!!a`sZIO=|op9p2J1-q2c>fsNvoXJI zAa)+D2n9#KLyRuYVTOlVo7{xLVjmBpy^;8sMsw=Mj>;m$fQQnfWGIM$_+lj3J|iF+ zuuP#B%8>+5EybpDcXq(m)%ctdQYP)>q%&M>IU&klt7YH6YJq+R2lqWCuIloQ5bG(h z)5b6eMp{CnvvfqpJ5cJAt=F5XMeI2hz%=Eh)^T^a=QM}srZT_AXX^Ft{H=aN$!3dv zb_Ki?D5epj>)=ykT!`|xWwVx>oRC3S_Uv|>=d|5<`CJh8Evx0Y8p)ya*YUSo!j#C* ziO_M!_MZ<@p52-}N=EPH(D2~%)1f@s3go@_p6L^b8&SNF*ji`zeVPfIf6rlw$`_qX zDLppMgTZUMn=`Z4OLI7erSz2?nm)3Yw1{)C@!4*ih{xM|hZ!Gh-Izk7Uwi{QBP0{} zc-ENjDP!`*(8@=oEN~th^;2qB6L z+S5AR%-7cYMZV>n4{pO!5ZdTm9!t>-PCpw(bbz||NBE|>Z&_*W4Z$zkpWodKiPYC` z5=12MbASL80KnESw=(g~-bt|k7DVO@)wah*N5l1eg#zOe6NFn)DXO6Gh+!z5Q}OQ= zS0m4uM7A2fEIvE*JPGcNAKajTCfxf129E*3!Fkt zP#VQG`h@?(0_0?6Z8I@lRcf7JkG%POQg0Gr(Wvmeo|Z$_nc+HoVdM94d{oIdohWM4 z(Gt2}Jij^G;m)##(7pP6jePCNI4UC$vZ!H-CFV2kPr=8)X%YFB9sgr@Cs$J@mUjpj z12Y%^;3(!+2MbldOt2fDKp0{OlthNc{Mk(`z36R4P2A7!-r8K2AT~=?I+y2u7BA{2 zb;wXIYk;1sd+tusG??$9k#x1o%dCiop=+kY%gAJ?!J>$|$l%i1XXCO5-DO27mqX#M zD;_WBRy>IRz8+aMl=kf9_NFK1!bQF(JNmDV-g8^pv1qMhs>UyB<_;ofq)o?_z)TS)=0 z=I(d5nO;qm_HKI47hQLVy#9T~nW;Yl8S)WiWqr9y;sJ8x`LxOKXzl>~+B%LwM962y zk*pq2RvXd||NSN;JOVrvy_!JF zzeRIcB3~#5$+yCE2Rh-uF;+a?21NpkOVkX~Hf;MvG)h01{WRkoF%X#_WgfpMAlkoN zZ7I6T_Tr@xk}#SL)G}clI~Up8YJJW@s*rWI7qf4kVR9q$zILb|J`;pxctB`dB`pXE zrw361SJe)v)5W8ixZn0i_!q0=lgQU{XQ9YPN|{VxZkzVJ!D_^$9?Nk@Ty~V1wpd}vcq|%^ zKdyVP#cJ1sBp~_uIa?uZebI07tVc1o_K7!WbYbK9S#ov>y;C(vLvxJQTE7rg-nh3l-FY z7{Bk`jj@$&3Q3`3dRQCubebTyR@syn6IBTrvxpxkoM@uk4z)e^Xz7Hy%o2uD0h@$^ zRcg74bkX_zijuJa4$ju_j-1*C|HIl_M@1REZKFd7LkiL&B_JRz-J!IUfJiF{NOyNg zcgFzIFqAY5-6Gx1NDked=i&Xm@A}qR>pN?GXPx6;xMpTQ``P>6_rCMGhKo^B%7fJD zWx+_)AaSPrujVv*IMW(lg@g^|Xgoh>*otnwsoSx;(5Mr9%Ta4px`-?h^pbvN^5V~a z1*Q?ZOpCf1SfnFKg0mMVE%=m}oc@m4>ft~!U7UkJT(vRfCon$kCJB?cKa-BggS;OP z=nTy36@E%L$uRw%e+FV`p$#0)_>4izkH}$^k8`{z6WFL_9e5oG@v?w8$i+c0!%gSZ zbEmZ_W54qIt1PUqQ@(_TQ+}vytW~Oy;N|?C^N_xpK=jfxc(P484NB=??c? zt^4^7y+YEJr|3eRkL_E@|Eion1~^biIbV}v14vudp|Ye#IgD&Nb%2MzJyEEWG&RX5 zJ)ollb1a5J;~A83QIT0Kak}sW=Od%^z9$mA#-a|VxKYe;4s&=LUQmqYFCm=|gHEyv zn{vs6*FF;Vto2=#YgLt7i0XCtYSg&HA_1$>u^cU=HmWxYc0SK2!I+e5Mnki9ULZBci5uRO!9_&Ur?>q z$w3sSGEKe7N40&6KPf%?Du;fz^?AwT<*>Oxfo!rUmz zmQ!V)9mlgG#SjWAs>_O*M=r-L+b#|=qsq!kpmyLjg5Mti_6fuYJ1Z-Z)Yyjo6rU6rh%s&0&(&F_ znJBz0wYlgHC3r4;)PA--kuko;>Zw-S_Rumd4bFCuF)}%9uIXYnXgtphj(@QxrE0m< zhlh8AgsLj+opV-+e#q9}YQHqLC3d%R87+FsPXDhMs^8y;+m0kl;Db!_0&#oLqRjhI znu$k|i(OX z_>5nYwA58%GSiSjag3`X}E*50QCr~P8~mra7GhN9387c>s=^ACjVL) z9>_K*^h*-Z%Hec>J_Gl8ybsF(`-?N-{FF56jgV7Rj0_8lqbsH#^F3_^dUwOZ!{2Ae zjQ)$|m%!szBPlY6CapDC$M=;_qqCtrjlG+FyZ2xrh2q`RDK%s7ZOufvxyFFmhR-C3Ns%g4-0% zm>=}*yQwJ&3CZIIyN}yWaiR9R;oQ>Fy(poBhKHM7Cp|dOd*45@OVGsuXdFx|EIl^O zfraipup|XOLOo8zy8Z*C5YrE^M~W?A)>S6bXAVmA_2T?oPPc<7j|-%TB#(U-8*nYq zqCHXbAr>}s?6y=Nv74HiSCkEvn4av90vk@nTRxSY4&RxB9FmK?JsciQ;?q=^# zAC=Ey++GNQ_@dK{9#E2pgJ_0A3XZg8(1U#KeRSFkkO0S+y*l;p7k)^NrYNvXLhew{ldI;Y-HEVrR=n%(&7?y25SILR-os(P*~U zM)xl`ofK!d!(q*o$aQ4nq3gtValy${g5E6p@$FLkc(==%OyKMZ+Q6+KRVr+{{_;nZ zD}TFz)mx<5YR@6b;SPRxLQrOXsc{Y?(%r}gtV;_XD)7=+=5y(nTK29o^)_Yfa zpFUh(j!W6J6nG6)oSbyfme;|*TtN1mejxC+5v%wgJU7!rn8iLBAAf=9;VRu!$i zVxEQoXGa;ctLvo7NH|sEJeAL2R6Guortsu#iPztR5d3&k6AxrG0^50~Ky8`aJurS? zO<)85QaI>ee2=QxcK`hi5psJlRRJ=HW|(Z+u~B#UM$uSCThw_SC^@n;2V{DPAL(zA zYVP>>xKjG~4{r0kREUg%w#A|VEHf)BdJaEO@Rei{HK1Pn@FX!ByvF)wT2#%^7<4@s zX1!ya4IfsQ9`8zP;DHL8uO6okE4+~l?sqO^`Vf@-G5X^o#lLmjo4;tAH6B^xNS5WQ zI(LuaD7C+S1^TkTX9~9yb$#P&y;Zx*7faFq=g*Io{??)*a2lo1NjHIVcTW#l(8{sv zuxqunhDP#$hPJjl-0L#K4iMK(3gC83-E-QTtxTbiC_-^)-Q&iC&fJ|Rv-@0JevuP^ zNxk$OT0$q<;(5(?o@l+~s{IfGR)OLus1a%)4FtHD;hBp>R?$}4jSVDK2-KByR+V-U z^|j75tc6D>qqMi?${>l?JDMJ-&(RC*W+^(ODrLH?1CYZO+|J+6w#I-q6M$8(ie;hB ztM{KY9;)6<37`<<<&}h-yWM_m-bPV$0LKY|A_-(i8}Iv;?1oTU?qwXA-m&q}mT~!Y z8L%rvig}U>`ZQ$Mz^NN^H6NuIg@IGB_m2Ec*M-3$FIj-W9M1(L_Y%%H?|;5mQQ;-k z8u=iPYX5xEVUho!)Q5Ktzeos99@Ie^9v%+NDo-I9D7B&Yx!*Ntn`x$<(R0yLJB?aL zAV`Ra8gq4KlQdAO5pWyo~Wp2((sZ`-e+9{peYILT}!>r4+qZ>|0>3U6|KYea0 z7LXgPS;0s(saMmub?h!dw})+D3(iimNT9I8C;5)NFTtd;JcstD;2tduG#z&~4AL`0 z0yzc^FKXO!TD}z;??iUW&(C*Ra+;aaG*}vJrUe+`J}N3&ExE~PFTXL7!Q1)855oI5 zxDM-O1(~bA)T%a3UcB5|OWm%&HTo2!$LS++almIYZ~sR_ca-xa z#eTp`BX#6%r%uwP$-Sz#k+z5Qwsk`s{|0~3R0i@q!7S|`l5H=5Wh35rTAUw<40;B_ z6IW53x;}q8qYQo8^Si+>$p{V`-SbHCv$u3>NSVm&hH{w#>ZsN$8?T@JJZ>(ynrCi;yrT2sF~3=7O#C zZ_CdVw9Gjzo6|r!{U~|!^K`dxD5_Mqv5I~UpWkgcR;@280ssJ}r|$^`(yoUgT%LKw zX*{{ok@AX)PQ6hzbg!NSj)DxydTx2WJT_sWz{D>22g8W&yN_x2Et1Dhv7Q+S=jWl$sJo3zxS9?U~UkL?P~nR>UVm z(%ubCJ<*~^Y7!UD-uCxM+$v7gkiV2eID%>z`Q6k~b5b<)>!&x4dKJp9DFt#&PF1w5 zss0qeIsQb24j700wfC7m57%77LiYQ$Gie3eA6H<2@pOX)GUek|92gm4XJaEq%}$+u z`2sb&x%uv5!3F@<+I#-T+7R%zbY#F>L>VaA2_SSyjEMx_m#d&a6?u$_09gDdkb){J zb)f%?Qe^6VwjOcPHS79PrVgOh@!l7UB~UxVL9MG;wjYTRfANK}fApuc33`S?1to=- zoQeb>NH%4{r`{B5@RNu4xlFqj*z%<3qAe$XnaQIlzD3|as+leVE@_y{;ek5lU2E4i=PQXK ziXXb&^{OeyO9Oca&*T?YiXk8bC}{}f>kw7zsjBnObKbHShv3EPDzHiw6?ZQB3GlVa zpLRKZCf4DT&%uetZhcV{z|{gkZ~#XfaL<6+y<}qnpuN*n>^5~_zuac0#z&AThD{F! z;kWCQi}@fU!v$1Z3c4Qs1Ax$)^(zQI*$yvRyJ%}`b9rcJX*C-5gx!Srac+L#1=#{R z6rfJ7ZLXDO1y-Gf5Yw$?IjIzm_g_}6WL$R|6dGLKR(JSoh4M1hVFr8;2Gc-I&JNPN z;AmEd54pw0#$$jkoLuxQD6|T6Sgtlzu9y+}+U~2s-lC3&%JA<1-u4J|3+v2A8Ee&9 z36%j5OJS=g8v_8`+#+N3c;^fE;Sq>~R+2J=e+=O0uCcW)vjhK*{~saa(S-sk>3Oi{>Y}*H`i1~ z&OQw{oSj12V0k_kL>5(TN*YwYTQ;6RAV?ZCrd{S$5rg&gn{;syHUs#sW<#=oEr#FH#qdva@mKfXQ1oWL}Jo!G!Y zKzZ9wGGIS}4=}l~Wq|+E5SlzKCeX+}_%seppi0U&et+PDY>uw~rvZopZ4Z&3#vHcc z{+JjDG#}6eny(DK{Qqb8RMGEZ+LhvkeR66P~^X(@ZX}^i%%d7Pz_TQfW=)>Oq(681LC(8=k15Q`nQljc^@ zda{cCsK+u;RlXhcO_vuDh7tb;&=@e?a%qHS*XSBSoT#VIF+A&WV8fvtqCVqI3O~^j zbP+L0Gco)$jvh|ZaxxxnT`!~Yz8ay+X}ohbK2DG`8~a=mSasI1*y@c4U{P@?_>bo1 z&x3Ja#n6S?JuIEP4^Hx~dDls0*Q24SoaNTBH-I1{BS^d|N9pdqW2}@+T{YS?-4WunHiyt^@bJb&7HytlKXtDyoOOpGN7} zT-@ICJ)D_VR@$|>Z*}y9FGog1e6ORJ=8Jri1i_P^Ofq23GtZ2}rC%e&n=UQ?Txpz=7Ixr93;Kg95wEM9%3{zoT9T21@< zHT?0=qwW5jwj)j4(=m`v?tH)A08!NUU+%a|zuVbkCgZ_!i)NN+IMr8MDgor$zF2jp+s*O$x%4maLm~ z_ch|ybD5_^6QIw%g<4cFmtZ>Psiu}K8wKKZaoC@n6bwPgCN;e@hlaqrD|2i!y+5kpuTyfu!9H3x6hY{I#fz@GnC!Wfm z8tB^}fnCKZF=ZsPUi%_B`bw0VI`WE0gE<{r3SfdXoX0>e&HZ3R*?`+&J_3uqw7i^n z{`3bqu;FE^cQ`VP*|2X!ghY{)T)c#sT1kniiN;%k^f)K478_53=Y_@hvucxX_3Z79 z3=L6ukT4NFKOg2UzW~AnO=Lc3-cRzrM_;`B_k=+cmgAV${Q^*`RGZCU88B#V5rADV z0r46E6tD8wxB`uC6DEVDR2bh#(Xsl`~+mzmgU*mhOMoX z*;#%dSjwLp7zRp+3j*=eCm{c+f+Q{(&jnM7UY=30r*ZE4hnr~3aJQC;?|N&YQlI60 z7jJS>a*yDAI*DDEBw|P5O$A^HUDN$Ti1aBG&>n(fPV0MM1OzXKkp}A@ z+~&?=Xh$QW5{U9`bFnuZ^k$(UBVqj$vuQn^E6mD--N9{IDpyJ>rXLQgDMdV%F8{s9 zkBu!{{%kwX=eUH*;h2FpzRZ3$q8iQFyHw?I+8cQr{R-aVF|#k$q^hbxPYU}wt;_~F zCwL_a+*3ug!LKlYRaifx4C_hBYzL#GU6TPo#vArCXIk`ka^Th2lwWhqRq%)1>tddyY5O# zDI4-F2MHaB~Ms1pRSoD{Ks`U3&*>+5Hxy5(=>83_fgAS*Rl znVA4vb%j~lH`4w4>oj2ST8hNj*!c8?qZuB>37)bRmzThZ)ibeO__j)^uyQl=+@A@b z^k%fHny=+;hEZr?<@n_sTEBtutd)pjpv6fL1w$TMXGx+s>A>)T~~lI2b}#`We+La5WgJ8mA#L_EbArxsfE zT`SgEv!}C#jsVaKHAZ%A134F{@UI_oE+0;pB{e^wifv@8NfzUSb@{D&^NJ_@{nF-( zhemAv^DBIs52`h%fbzHQKOb5aDJNnsmyf96V9D<2X&$1Prhi!bFvr(JPa<3XviBGt zuU+XqaA!hEjbc^t5l;M)V5H#?mgEl~^mXx6UzU7|oK!UKO-*M|cbzVgmQC{GYCin; zpP!h6f$PP1#*HEhWLwCf93xlkj{AQ9>8O0j@k|zu-%+QyzSEOd|Lhz#2 zfVBJ5fgac@MeRE&|Xe(Nv-&MjoGHL$us|s3F32+=l^*>Ct$HZ>0F62 zGgtib<+h_3!5DK~YCmCi`p^7er-HALK$0QZyD%tk8*WGOF!_I$9X5PviwYrNSXR5r z{LfckH+xF~;_`oBAO3$fF#op$3?5a_{7)^w{~K%de>>R!mjMhLT>dStLb!Sv^8;hl zlKFIj%}aO(Fla>*^G=+QXqYaxAKds zUS;vY8;%%AqAA2^NU;SqPrciG`q2;B?~hRA;s~Yir78;w-2c{)B@eZ%md!k#k5%OZ zNdq7UazS6sPA*_#sby9Sh_ssZ}lH0R^JfnKGQFYc>>jCE@d z!Y(449Ex0=oMv-z&kek`sjAr;9S0kZm2Owu+rIUZf@E!4=q=}vVYmo~??HHiKDM}3 zAM349en4o=`U*!jIeT^4+(zo*Yp&wG|YLW+|g?P1M7mRkIEE4?GUhK3{Sf$+#3d`}2E+Rx%qe zKjD+P)@Q(0Iz4RlnEexrf{qJiWN-4i?lUNVG~c_z=n(&PpJ@Qxi2{)ebrj^}mqSTq z>3lYdav`z0@LHc`eAFE1=3r{6Lh^hBJz$vI5caS!(S#xU|6|$uy#fZIgN7TPwUbpC ztlstWSA+P`C7($$WfOw=2grN@xBfZBM@0oHe%C)9hdvGa>8wV5%3xIFVbDjt87$23 zph(j5d1iJ|S2%kVm5?A}Tw!xKLq|)CY0}B!)=|kF8w9BJSXK76GnXv5!%(w@{?!6j z4KJP<{JoVowWRpWe%y;DgErekOu+q&F2I*UitfACLC2LzGg_!KAch+>ZQuVCPX zYt;i7C~p;}r)LS)Is zRf#0c?j2q~#g^8NZ{7|JL{kd9z(%77wZTWepp#*b!9;pV0cwOz#q?y)BX14ko`Ohn zwHJnu*MAj&OlABVWh_+lVR#4-0Z_@neHDcV`URlcW{r?!#ee~jIYMkyQqzP__`aBv zWM~qM&m?zeTs#)%1O#mEPHWrlH&U4h`V=!ns|#TB0v15Rm5iwN$BK|m+xfHx0Oi-X zDxU?nLFVt`UAS(hMdbCNN)#2BH`-2@E;j4dFO(I}XD&6Jo?ZsVP)({ilI$uFS8yW6sg*1MC*ji*Oll<8=C zRlVh7f9#O)rXV4qf;M5jXPQzkE7Ga2Uv^#V@Ho8xu^k8a0;$5@mR$t4LV))afoGT| z{n^=B-`iW#@Y8naco=Y`6=0X!3y1A~_b-$le*$I{%G2E$DA}QE)cf7U*lw6dkqA{{ zqGpdx5Bio_CXy_9iN5f9qvhsod#cdJQ`Zy7qteMKZ3f^OlDb=4KUDbK&fc(3zxzrr z;(oR>!6zcpQc&PEl4BPIoCHW;ETNRDhEw}5zc!DP&}RQm0*0viU9GhzgCPgkLlkI} z3yO_ThSxTrf*|g(Lz= zREZysCL0he|F18le-`jmYkN3Gx$os&IO%C6*4_l^B(I*vAk2=h%HakO38W(`D zPOYpAuFS84J=dL+f&*yHowb4*YrPAiyf4j$N`PNw4&l)Bp%V$5k_I(@DaLnZ%j+-f)@S!oH?X>&~^+3jWEsM~?(b3n#`NQ6^UcCij?AmbB~{<3aS zaE9}Rl7m<{7n=HJzK)9ql(VI9=xx+u%k>ftbJj!AM1awFF{_&~a#FS#)DTNQnh{1P z`KeW}OztG@Im@KeE0?x4qCfT=$^D7Vs#D?@4oT))8m2*3U3#o0Nc z+_=b)pzZgsCN#b`Dr?lwRQF#0_k{LO#;_d5%(MRW2>j=>$jXc_CymyvG#&Fdlc(CW z?7!CUP+pvKwYjfeySRj|v9&(jq?gMr)w^Cl+@~jbR@V)Rt*iuZCOxkC6+SqRmv~%S z>DJzsJjXq6z7IQ?o^NBDT|7@(@VS}5k0sCYIhhk{xxpZFo+@)$wC@OdWVuHiKI~?e zZ7vJ>h)-cD-yLty05wtNPb4rE`&AxeCn)H=UJV=7;wO8|=+6fJ{#^}*3vsJ*vLd~I z5jc9;D^SleAleva^B&>Bzv|p?a5$UIqV1_AshjwXXHc38cZ}`0cKoEGy6IzvU`tT# zA;MJ$%XwbwDbL{euiek4?Y{0C;35>E?I)|z%D<>5LjtK)aQJmZT`mjO(9iljOwi7V zc>Vj=R;Ly1`nkfl>Zv=Vf-_UI~^{a|>1iM~QvS-IbLu!<<0k7}jG=V5C-Yza%*($&mh zC&kF7t@`BG>>-01%($0fXSty?r3-#=XSn9v&+6i68O8rfpK z4EEvp9+Ja&a$>X*s~3(c&q)ddonIjq_nmDP8!BP2`Q3A%k|XoNFX+jm_yHO*oO-Us*ipDIQ%Tx%o0&*Ui$ID?@qEKK? zyg_8hC6bsr85wy7;j=y2+K#(l8m5RmrF{XxKBt_mvB9|P_pcS;&~oStWZN95ARjv` zo0)puRx|gzy1Sv-ygxslf9o(`qJ~*>0g(L)I}rb3d_a;rNeQHm|HJ}X=-+JYrY%Tb z{I@t=4&MY}GY#lB5!C`4CNUoJkchZ_YbUUk=M3jSl?@E*nMnXcCfeXuvp1LY5^p`o zQeh?M_hIBlAIj=h*0S@@ldG_Rl8^n&NVB~4DT>CLwtamvB|T$eEiGduyeO-2DLt%c zlk`ZxuR>-;5>bS38ZV;8{HA>F*N;r|pP>}*g@#4>eIRxi<>7GoQ|l7ps4u9`-EB<1 zxG666*Ge-_22}?Ong$K31f10UST&c=6Wm!NGlS+&o-PO>?#l%-Y-lP0;oW3)r-D(_ zJY7-071pB3o9Q{D@@I=TP!_!vv1^XYpErI>B62X9(_uFNp#Bt%!Le29y^|&dR(@!( zD`>udnftXmp>v`Q`VOR`p*Kbf;sa7KM!^q`(6fJCZNqw=?5-QxB+Hf5^n%w{g+6W% zSaG*UL&@uptD!RX@D_C8@@$gFRb@(JIa#$OVuU|Kx%qzlAyv?Dz~LU1TJ|i$QJ*(Z zC3tO6z_!=pk~9husPeUAo=OD^m@KX&V+y7+m54*mN?75`!m92@3c_)Qk10g-`92f1 z9wYc_pLCBo=}fL7`e`(gao$R3-dw2}WC-NfCzL`tHaq2DkUZ%3z0y!9Gkv4ZL+z!^ zdifmg&-cO4U*Zi-Qj+ModFU%9@V(9+iEt~QZRT0Xw%z(o6VN<9w!-yh!CqJsTfv^e zall?8!E*kU3Y!8txDWEDme~HZn zl$1E2bm1eGk?ZL>xa0Q;{p3KP1g*s?`dEi3gZ4r|TxGzsF!Ky|UVyW;mB;xQhj|4a7{CY@II(4N~OnMFQuyD{Iw197gJ|G9320#Da~mIXJfeDZO2lWwbi} zk$pqE-)H+bdlshCw-YDUdhs%fAD(D>JxGIq5XkfF4~6Xn2uX16#lPua|L25}rKzqw z4L~A@!nfL$u&{9b`E>*0}{-*MV z&zGP{y%NCdbktDQ2oG8R_%|YS_vRqPwxO60I^X%zKhr8!CSZ&Z`%|J^T8`qPv^u-wZ-afZOnst%+BZrQI&eo6{c>gq`o6|Q@3iamLc&M?q z3nskieu8|&O#JzLwy#N$e;z`o zelTBk-oS3%n^!Jm06QE?p6=6iH63BpsT85~2~;j3fP_%iD+i@n zk+&AhDK`Bfpa(rua&L!sNReE(8b^s(|3bs>nQv~twHYIEcf)Wdh3 z;_uPh=8v56eCW=iFJGjSViHeTP$}Kh@!I7zE;c*v4p*5Q9A7a#dm*F_b050RPB3%9 zKKMRw_@Soo0zAf@ZYOQ*5;#MPloEy+r(fwXaB-5-fDw=7C7NnW|0`5KYlb$!jKhSK zc=iK;2mHDgo#_07>(UF~;u2_%eg7^IM!ovf3laTT4z+sSoB{f9YD|A)0NYucH@m72 zaqRl1HDH&|lY)X1)BQGes2DX-h|h*OIy{S`v3>0lwnS+8UX4)i?D{868C6z4T08}x zo8BN$Q*@}nYHST@^CSe_`9L`D16_o(8+JXl{HtA=y3sjLMEw8|_Jj-^RJW0cgU5Ij zH4-*wFcemZ5KBNc08ntLQSwAfYYV*YOq?l?F6tidxq-aPIgm=kF5#=FCA}>Ty=1* zZ`b~w{2^s#rCpi80b&gs%{`ew7G>k_K(&|r^?dT8h8vs9F&px%UDI(q5uErW++ zF`?23RTrT6EXUmnKpYrQ^2t+up3?nsTPXjqXxGFP`3G}}{_+=Zf43!kbTPM!Vp9Xx zr)R{Gmp8dvGei?<)E@kNHgfMZhWpO(La~0?yAduGN*ar&1zSD-2|w~zI3%-O-4GMI zUdKA2uf~EYvdN$3Y3|R}IdxT8Sq#wjir5||CbB;yX-*Y)*ZqhKM~W9}l<9HQ zRW}URALdvan>q2b&wZ|k;@O*<6iB&t3+tD?R*nlFD`kcpMe(yKe6-!#KazTStniK8 zU5q?ljYRpY@wQwpxm?6Fd>P>@G4NSeT#>JQTr4kf*?&$S)$GAYxichW^s|}EykbR` z8CM=a3F+{_R6+~kytqbWQHi*718wrWywh$obyiej-ktz+drwafP@w{a2GPGZ3&@x* z7sx-yr4)9Bjl1V5oBV-m>794pNj0HtpHepNcb3=G`1cz#CtC}DcWS#{u_J1Ni`?`m zc)$u1TW=pSTNZBNGc9*rB)mS-?grjEXZ0hVO&Qo(4*M}9%UYMX6s`-4??+f}4ZRk- zuL&$3u6yix#2#bMl;OH9#jp`8EVK%b&C!RMdY$JUxXqQeJ*rz?E0fFgJSADW4{c8U zYBql{McZr_j;*^Ind_)UWcD0?jFhwUS(8*2>633xvMMMmS5)XsFUA(UukaLF>1Mzp z7Ct{wC|>>-o55sM(X?HTTz4PoQn<oP>l~rPrzFZzvAWo<=LT&F`cionm-A zI@>ngl^ySvDpOr=`Rv9QF2)dfJ}g<~O9iao5VBS`GW#EXxjIDAfqKB}zW$jPqguS3 zC*Iwh?TW5|-hL2nGs9tSEqK&aX4iYvy6e84E3||uY+NX&PfgO0Y&Z_@Obukjr{fW% z$G;tO6`mZ~nxx&oeUgk|8Kznmi{0rmAa>Ev&;Zn|13Aw+D*{QDfww@pyZ!nf0FQzn z0ko8)6|7jrUJNspx!hgZ_0AUOpBt`59Stc+X4l-sY814Z?N-LNw6H}xujfit+|%JO!lR{TqOytmcq^4Nla2bb-YEo>yrK2&@3+uTnlLpX18EGiTR`0xH8`syqf z8#?g1p%S1C(%~=^k}3)*Vm0$k7|7g8sogx z_o?~V-*(f{#P;#}Wc2oNKDT&^&O0Z-ulqeV9_RLr%O!3TK7%iFUVlV!ON+?q`smJ> zYR^d&|C9ar=H>U+(Z~=gByw4tO_d@|7>yGq%U(;NV!HO1r#xe!(fS!KrO+w`CHW7FF&|ex9K25^yxiaZ zMz+@Uu;u`>J#LbFpkuO-q}x-^mNEQZ0RH95^^J?@7 zJ4qBG?lr1QRhv=VZ^PU*{>0CjZl(xTups;PFOxquo(E6_EBJ@clrJJZGeS$)yYkWF7Q1m-N#RD10p%^&=AXj$dgOUBB5CF_^pN`3|A5FON8I47Qz zXBHit2j^J-I0te_AI@es4A>SQ`j!4r@%{Raa94f)%Jloyy1!3ZJVZG#MNA);t%N0n zxM_648s=`pp^n2laV9SHQ$?-I>BT4YpR3u+5d~2-hge;)ynUhEZC9(|z~xT!KFC!# z6X@XD%9{px9Tbs9I zmAx;=CruIbeFw>C8<7nU2lyR4J_Zi1`^PF3iw|oscJ0;^b}HF zq2Vp@z|TiUqLV0JUrNuEGK&BtjuvklJn>^GbAo}$>(OtEbO_8C3cb01m}uR(v;7-e zfRXcsZwkyVk{~-cEKsiaSl<>F1)CC({9Mw#5goql_Vv*(0b1f-Sbgr}=_C|R*n&Y% zaIn9*RBOC%L=~;Cg4bqbm&*K87iG%e)xxdf=SMT~kLe4qe8uG*5hHw`sc<`8i}d%| zL`IozFui69VwU^Y&~i^c^w4O;eV``BQ(^=ohv?Sk!Z!Qa5BRk|yD(aZk7VLPrdp}+ zmSX+(2h`M*|oEw>J_IAN4d<9ZEwLsVanv zz)P&j+gP9UNSM z(oWz24i0{dkQICZFfu#W@omz~tGrdMt_-CPn&e;l!(WV-1ZB0(&luG3zJ zAJAKy{gJ8{ULRvF+r#o0GdA#W=@+q%Hg!(SDRe4-*b9IUUH{8{-)fc&e@9)&;UPO1 z&%UJf4kkRp=Q+M8pp|MYvi3LA=f~dF{yj3hRBrE;PYpD=xP&HyU^F77Lruo>wi|24 z@v%l{{(yZ$f~!}`d^gM6BRGTeY}kA3={&9i!E4)IKVt?8Gl(yp!mZfDuD>ZsTdIVl zNAvP_vOWML{&!j5E}wmpk|vAZ<>U}spV!(NPV3|9wftL5%TG4@vNQ`$(PVKCqdjTx z=&f4txr8@qet+1kvbOL(|FY7q+$)RjMSCL7qoJtzoz0in(WT9Cy`WIv5w{aR9-*JP zAIfv9&g$cBIPzCZ3;i#b$1ZG`TTz2M9aPDqftB#na8rq*@nQw`evHd;o&%*F9fG1# zprOfJ+!*j$^mgI>TJPZw)zxOTQt<;p;;(LF{n>D~$N8p#rQN76`P#eQbAJ?-d26== zHc~|-dF&0{x1qO>$BC1>h z@N#zD0kEUt3?ZPnDm^_t{Q0!=$3PVhjZ9JVUevBcYN-5AqOr%-63QWXMdKaD8hZ*N zY)GpmkuZ`}z)`$Pr(RdJ8?htG&0OulhQUN5OKE>iA(iW#cnFW#q!RW1@CXj*pC}xW zi;%nBvpx7z`5v9lf8(XtLym0r>87=a2Tta!R#Y?%XIFtTww&BAQrODDJFjt#WBci3 z``$f#Grj62-vS~N<0?`T;;CI(aQ0Uw7UQj+WUM)%zr{b8P`J>dNCn$-w~7hg=n~RT zQ_>XMM`&{921R6kSbWehprUaLkcobofz_{OsP2OJW8Lp?I9P_9Y(wq)X6=7!0os+V z!xRr%REb@*WDH-kC?jGgdNo5c#5q59WeIrw-N1-7%c2;j5U`&8Mac9OF#>bcH(WGL z{HbT!z2U4BR%IAGl#(Zl5oDYfnISM1vr3rs&g<{g@v?Gj0+&g&Ec>v$i-lmwYfcSW zDxoEdo*tL-uuB7~ru>HF>MY8!6WKA?X((gApUR(_d$J)Ln3`wlL1dQGHS3%_Lo#0h zb>w#J3(6e?We}|umeJtux%GSv1_)?d8%a5`WwU_Ll#7kCmdjF%oJFL+7r z()GI^!r6(fIDsPwvmpj{=m(f)@%uxBgxZPBAa!$HU>x+BWA!n--k&n zah5nV;#K8ShK;b?>0b=IQwtv16LL;6)(#|vxHGqUACJM88o#t1PdBj*`G!%){=nib z+sV7kYYZqZ-g4G9C$2nJdBp$9KfVd}SJBeK00GpMfO&e2o#KE3q^qN%Q#)P~$u8~1 zIRDl*5=ZruERx2u#SH{Gh31H{`fMqh6%zz)Vx?iKQ-8Hc>(Qv~m`p-#$VQt~@nRDB zTc);iWG7Wp0+N*7zX=4P%QNKz7NQ^{^2g^>jDGRv(~t`={*R?^>Y6U9IA)*^dAb^s zZ)hsr^c1A4e*6nv3c{vZq#sxJ=gp3M4!v>nMPlLkqSu5Y55_Ml_yH1sW>_5d45Xg7 zsDf<%yCYlH+-2d@dtyB5#ZruC{Pd{&3@?asxK4tXzg&jehK<&IpU}lo5AwxKu#F17 ziAL#)t^0`wFhtnd9d8b$0|0*NDk`I%{&}K8C$tB&Ht7Ah>@ID~VR$x2EBRonobC{WG|eH&JIZEF7}TFibO==%elh$Q2c!h-jP z5!d$*`3lLcw+BuB)ZFJqG&J2+`>7L!k(44gA2ej`zDOlL+#^0V-Y>5gTuT3-!C*~@3FLm^p8Qr8 zU)6HKai+G+_)^cMY}aO{wxWnJ2RSEaJdZ>|+#MrhG;VQ`*J~4U87(JSv{R0Okvc*p zue3QV?C|aqVw!mhK`h)EfC92YSE6b@aP(qeU?l$6KS1tY^+^PP6UvJfLi)ekH|pxt zS;iVS8udj-#KL~V=aFs#;iEe;tp^H*#3e-HKhm6dI7S(@EkBG zaFiJ4zPoVP%;Pg;mabTK{$07jW|*SBpqNKE8$ex`&Gp6TnD0rF)^3e7Rwr-xh?(*g zS@TEvPUmkJk_B^F!oP5G{`>MdE_aXiJ;sHTU#BlJ z%6;2sOD_ivhUdtGDv5)q%yU2rui1~@8dbk5O-1S+O``+*yV~_o5PUCAMovQYBk`@} z#I`VDH!n929B2L{F?A52QM)-zp9^bvd*M~XX~bfY%I|vpkTYC9G9H52pYy|4Lc=|*24nO*}#(O>V#b!49b~= zsNWtR?%g>*2X~FNdffs5k^pf_CIZJGFLG36p-IB+!=)79SzV6!JUDa)V!)uW-i$xO zl15!V9Igtzp+}J|$$~VM>_E(G1HtI<>pt>|K^++)r7W)7i-qx_5HaCPs*3yFdnfgk z8X;4-2yL0+Hk6>xrJ`c7Wp8(stgXrBW}U89`UCbj$41pDeCQsrYvbMAcG{3yZQVwj zwR|~Mye30jTB2KQQLfa)lX=VtB1Dd$5v=y9?}UHcmF(APG@W+6wA9*&;6K}6y@%gg z=0CqbXFEIKdb~p1;W)0GHJJ=SGOYw!F3JFl&E^>Ct;1Q(9GA^5fgGFV&4`%3ho%jH ztP`O>{S}W~phod$xv|5+V&g=iGP(UCT5@(FUUzplw;tWHt&BDsp_E+Qx_u*%H+0Pu zb2)f5a!R@efF;h(e#t}xbWlsF2CT)Fp8kJ!#E94dc<(hphIYg)TnZ`75QkKZZ; zP+948n3{+hi(PcX%5H}?E|Xrl-*zMI`pq-Ak54sgGx$mRf{;Oq7ffkCR-(P{=AaZE z2Wx0f$`$(8#v5ilyTic>=FzNWi?zSTr+glkCz{k4gazIm0m$L($*>J!6L%Mxje{Bt zOHKW6Dhfgy*poLmE{xZNW}SKGEnwt-88K>3&&;fi z9bvKZUla>iA_7Z6ARmPe;_$T-jV_C&V*Ba$?g+#8d)DUuAoqHlVL+&)Ij`q^$>UHkUX-*s3!|K@vd=}~m z=;trCm6Q&DgAr(OBXpB>$WMhk{T9pCWwIUPSda_pV8=!5u?xqo1Cy8DIf%N{JvhwmrfLuCZ>1JZGlgVjk=zCO5 zBIvvBwH4qyHZoWWt%}G1VeeZ0oq|we0qR8@HaI7KFreCz8|RN$Fq}34u`VUtYcQ=C`9*BVlv1i#2&_B8iVbqK zX80#P8x)+7gJ0egzp>H56yanN8iPc+3!JqF!Jo=k(^i5G4^vmQ#rku0Jds(lua`4o zVRKm?&Sg1DEvcy~n}UkQ2t&L9r>gngVuiZvD=_P!$Ga3H6dnsGXK__SAb$Yj`x?M% z0HQ4&9><J-ZZ8OvlcA+Jsv#tDCL5`M2o! zud;uVNjmxX8SJOgyA|B_M)@(RV%?Z9w2qgie*MD|UrC21?k@9UnRJc{*z;O9)dJY4 z%_*oU#Hb~pB+Vj!aY#ZL1GckF`UuZ4V(pjXa$=~;(0CC6d8pt>DB5V}@Z9>3MQIz? z!*A>Sn?tS(*%=Yky4)q==dM@FkxE3hNKvvv!3@iAZ7-Kf@5xBu>n9i3JMB* zMFp%+8nMLi(gf>)E8Y{-MnM8K@ zPYB0)n{S8N{AoDNh*1aIouyXy(~njWJ^gi}m$o7Wc#O_Vjvr9OVU7d09E|UK&cknZ zyv(zvmt7lZ%Rw?9c|&kDUavsG9HU119&eop2$hurqN4EOaE9AdVqu4^=9|1Mylo9r zDUYSqmoDY12RJ=m+N~z{0#7a6LgmtqwV&xstZ=5Y)R$A%E8DcIK9!A(5ZMGG4&k+S zqxHht*}~x3UEw#W#2-e7h}uyI2>1h*IUgUN)3sKwj*a%ejfP%DCKx0U8YIJilYnLd z1Y2vVeZQg`hEWD*8xq#Qf%FU$lhMk1F-uvS_jSA(;}NFT4odaz4HN^QhFD%aebUt2 zRBr2cO?h*Hk53rFUc|g8bBPVXI7HB6VTKO#t_|%wvHUp7b)m=}vUJ*biHxK=mb`T8 zXqpT~S#*i$M;WopXxC99S$UBxMOYhXSy~c9>I_QJubP7-!tOT{Uw!3~pCqK$q%iV6 z?0;!_V~IShd5wE2EeHipBCiYSO)tuWt1RH2TV zH)&-P0g-=l4Ivvg+5OnLZ`wu)56_etiN^W#W*F}Rh%f+C=1u?G=z=rAH}*{r7t`rW zOnt%3ksCUcRIUGXAtq9stnKc}fS1P~9O>&}BvRw}xa;PjXe1ozliXLa^A6 z7M9d64DAH^f6x}x&T8PkJ)`G?qA8Fd+n4!uj;}NaX{sOzOt_GPnEJ_9T~Ub-HrmKF z-d5LiJ%VOhE#s87)np#h%mdnuudhMf??*#&aT)u(HOqDQ>NLvH#kSb-H#*7&N4^jf zkU>O@aRNB}2094uGxE;*-WSc-=EqxIG_Qf)e91x#_Zdij_{2wNDpTsT>XOF2bYt$iSwD zyOpcF&8$T0=Y~F=i!`V)pGsdL1jw>q#e<4UGeaB}HzAn@~b9bE^kHJ?5z@2yxY68v%HRd8hi3Q#0j zRvb9MnqYHt*T@DAEoi8WIxWJmXzEzxCja~6AjzNvpfZ?5?Pln2)tue`E;m7D32!1H zzo)&H*3xZMB!D+UC+2e|Djim>De(8@Jl%|AYI8viYahFCUeRTyuQq%n@v-<6*8YX+ zK~28KDO0pj3{i@jM(ApADC}cMI*Gx$D?o z&=RlfX7b11$>$T5sATmLrJvP39$QxO%JPX#4GpLk!&%ekf~2z+OI2roU&nut>{rPS z7Sn-KcP!DzW;v?06P@{TeCzSMW@N~~y?7m8YDyS5T=66MO^s(DHf7&`T8yV1Gd$U5 z{&oGi+H6NN&L*aUoAj0ebM##`fs`G?3=KvMsKO%sNz{i2;*^u3de2FQpM-)AjTt4` za`?wn>9jmbFKT1aAPQ?{KpJBbd~X1%;aVVewY-kA^Rjo_+e)5h-mFh^+S(cAIGwSI za?U@9P7D!W)9!Ms89s?A>3$<#LnpQ1^CXCMzOdZ9D!^bS4|a{=Z8 z*f3FvjA||%OiAp;JVN_7{3$5%rj@^SnRJ?xGU^(d;2Qij>TuI)YUH{vxbM`k>F=vD zVbt*CuoW_rUgsyVydCGE+UAs5is(Nrfw&DjhlS((-Ovin6^y#}{kw?-wui)CxcR!=ZI9ZBcJ|RV;pY>33 zqtZ{@|EZi7)M~YLc%7)HiRw=?eL3`~cmHCUO8+pcSiM$;`{uaS{Thz+K?oql560mF z8DD^i)!E+e3#^W0Wn}@4Kp&3a|K1WW*pgd7$a|#W3s9Es`V$0#nG`#HBYqaxF)0pP z)vdCuOi(kAx6p-)2itM6<8pdyqN!R|Zj>C~TtPEqeVY_xw@iarGs1;Ie@WfkCxi`* zk($`intpoi4z4tEv2mnLL~-euY+BiCQQ-)KQ~K1jQgVEW970y#5F(DC?2>qa^^JEM zmv!(^YAHMd9dBXQv6T!9RVKcAM~{ek))N7yVniMm)7U|a05jqnO9BYSu6~AkTyoz; z)WSh4lht7I#COp6`D1LKr#S0F`=L6fBsp@K7B&%-gsM)m*y=sL_DkD0-3dE(^wMD&m; z$nnVvntb5e-_-Qa!{zP|M%^o*3#!F#+41G!5&%p9N`ACCCk(frG9lhNBZt<_;{6Rp zxB~V3)9tZQ;Nw1!mj{46rH3f6!7Zx_RVz5iRyaK#LDM8XewX{L+S$K7AKVvP3daot zUS@~g+@6K*#(v4M4(xHD@E>%sG#V-OWK8$8ilZWhnN=>=_&g5^zc{{33~%ugW9dDP z#@*i^Z3>>Z)&}~Ay68v8BF%msoIDM@HmViA9K&4`hfRWE@7{jwC*-OMc#aT$v0RTY z6&5<>z3SU?I{h(UYW&oAepcw9$$(p}cM&yw(DnRp^;>)x92(4w7Rn?arNN@z`+5Je z(W>Y#CYpnuo67Euj(eb*Nl5UV>paNg4Dy3}X|;-D7l+}k@y{B!r}sLJ+)xu*o=(r% z8h38cpO`h@+ow}P6b2@flS?N6aB~7Dt)i2@*mIiGvz4}7soWQTu(j3)l=jMBJpV*v z;+M>V(KD;ArA8p9Fca-%v*FyKga#pU&>dFt2Hh<@(pTuKo`+J#Z9bmPXPHuek8rCb zJ>$z?yXG(0$X{cdz!lIFQ$jd`mEr#!PU9|U#B+ zT>R?#Itmp(B|DoDQH;GZ8E+O7CTyus^JBm=ASC=kVcy%@n@DHfg+Q1=pjm8|pO3HS z)oTqSTMlDnO@FJY5Rj<_UDjp~mK`^3=sGB!&ese5eivS;dygAw*{?VTt1>`=V)~#63uWn`!4>lQj*EB642S^uYWm7^tzW=&#|md)_`ZQx0aJrE4^F% zJJVK&lBG>#wefklT(Gj$kKGAB^_+*t>$qHS)Qp4}=jHD$@pv=Syj79pu=~-ToLoW+ z=0_N4vdoFd!IS{I`y64$62&Cm#@kAR(xNGLl8IWux7#C!>qM)eZ2cFH!P&5`w{*>p zmq}v6y#<#+5pRVmY80=-!s%{7^a%DLi-1P&aZ~ln?38b$LG6EvmQL9klY-oQ?}x3v zz+HRg{m$U|o7@*-;pE?=aF}^rcJz9#{Pwk|)!t~-ODTYUSc?)bwl6^>`d7+yGHb~v z%xv_$aQ)#}9Eiw{jQ?A{=?eC!spiERUp$_d)fgSx36l8u(29bp{**f{J;ULTL$F{< zi4Nag-kmWn5f%eb=EM8JvQ%7$CW~6mp%lLEEE!mimlPM%#m>ESBjTL_FV>YJ+BdIP zbklaHHVeY6hJ#m2cU-TU`ZN*$nY{wuF4andA;)1a$g;M;GQ2lo;w-sOzRn% z&9@gax;k>-o{`^YE65&)q<{B$y*p?P3BBbeF$isGvk#x4+x=kMp7N8Y>^vsz@j6>) zS`RKT2(arB%8-zD*X$@FSM6uBQf8bwt^c8fLihQUndqS<H=cvdtK6&A zDB*x0!;R)N(m3|wu1h)DsA3cbq4I^fyHl$V>%8AO^r63eEdd3a=3t(pJSpd>2{DY_ zPejI|l|Ds=wWd-Lp!r|Ti49L|v;I_eQd+Ou}E zyQ#nF4?^k3C@e@>r0InPSuAZQ=V#_Hcts@o>LPjuv0w{luaU%}I(j*Rh|tW&>HG$0 zmDfOWF0UEleKTFwZ4B3DHd8u^(H76a0KQ;qIGD~ZGaC}L3cOV3HDA0N0W5=`_mLwX z@&CJ048WD@a^Cp`Py+4ib%0rWsm9>>PaL!7@zUwr^ZBy6)aRhA0=m6}fdSgg-##EW zU+X4(hQRo5|2Vz-dF{V7&zq8s)0NJULCvdrX2O`F4Y{bIM}6+n*I)b0JAsu+tfZaf zXDxBR9*7yJR7e-j`U||xlQ%5y6A}y`onpaBJtmu7MR38k;-#FkLU|X~)AQ15Avzo% zMVggEQhA}t*}SQzXhAChB8S+vv_=26t-M`_gUVkzR_^ew-r0C^Z`|IyLU(Irkx%;^ z<2*1&7lwsz#8iM8A(H%kAY8=@kwOPiwy@A=R6FdCDN!231j+lwMa@rx`ME2aDO=pP zJ|JA_fPc{WT7W>~PU3p*;J@6KlpFBqG|sbJ6-C-c#k8V=+KnY)LPa}L?I6qrt%$V{ zSw=C(lP4SvR-}x$)(jgWi{}hqYO$X~fvbzSQ z-efLV)rP66M=P|hG1F=?0Ku!gr1jOLT$ruJ$T9k4I2xJ^cEZy0J%n9YLoOOZ??qcm z|K*JKs|6%^eDZU{uw7KAv{#U`QduL7_U|Ug$G-`7I7=hl_~-UtnzpB32NW4_op=yD zapG^18D`%*xmRQJCaIVoqv!6EF-5Qy*-_nO`*wy}f6JY>+`6$#OW$Cw+cqKz)FR}6 zZqcvCiG%xRM2jf~J&h11nzganCWwa`8`{e835KX5L*LedFhSS#Ym8Sf4x?=2&{&# zzQqqX&zoq#ElmIft%}n$@shKim;n-u84hdMXozZZOXj)o!+7@l^S^wX?P?5ErM0FUUt$wu9(|8~Nbq($L6hpGYyhO;o83v*6~d2utJ#&%ov?+< z48i;$M}?|@lMDXBQ={7XQ#yM4Je*{%lSGy^ZZsTEbWPrxgy<3jj7XCSOZN4dwl2}} z5U?p%R+dSAw~ZBaSK59b%rkD#n*~5~8%IDxf_-_Ku;t3aAm(9ldK;#UL}jF9{OhBX zwm&i=lWB=g%EtrzS{xw_jga^1)oUy0Fa*KS|8d_iz}2pzlHd(5vm~N@!%8X7_de{A zYH%b7bAFq>fER&E+}By=A{ySI}AI-&cxq-f_QamFrVd*A~9p zrghrS$*E2MGz6G;2M3ROx*{R9)FB;ao=uB?x9*07g`UihS0vCXM(DJ_|xHRR>qezGn+R)XZV#AKyHwn*SiF$1V1v(!T!{^E&2nWKf`0VjVEdYK;}{ zafTTYO%YutKaEJ{LC7enD!z^RWh1WShEN77m;y0(M21!YPGvzNdfv->e|W`myc6unN=42ko6!jP61mhyFZ@B+ zdA*9{1E88k5KvJxN>6Z3;B*CI?4KF(6dU>E2vVG1l6fUT=zQ8EAY>QW1$d|+=erYK z*_40W350kH#d9=NRGdeej(T3OSXwWJ8Zz`Y}(13a~V3-;%V@UxVfXQT2#`^tof}^W3Rl4-0oSKcq(-Q$Yiy_9NT#vM1g6PHM(zr!7APsc4ooiA9ZOHBGYj7y}R_FElZK??Tk>_I;5?G?7` zMWa+yFcPXWTSofqic2gzfbl71 zG3`Asg|g=&>}zw8Lw!MpUZt<+hc?^*1{p}BKg@;!)c5S*cyL-o@JRB~<69Cq z4Xy_dHots)&^)N-s;*C2V(Y=Pu$slXfWwAQmC!>P6jEWEJUhFx(H;k(l~ps>XezP2 zFf&k`=a|0sfqNxw)p@!())um^+d2<=m>niSfXUp4cZ7X+C|!)w3D%ZvB@sODzVBXL zU6m$%pRdW`kCR7O-^;m^4+dkwk>eqPD%L^(=ow|Ob*9u0QrXw=5qO&_1+T$c(2VlB zA%s+6Hs5BMZ_7g{7D?2@aj00>z|=J7aZim^P?1tZ6&tLmc|2c?_Oa6lnlfNB|LBRIwXp&XH{#pxgtM01L;i z6vw3O?5mJ4r92^z6B$VsJY%R6bcF6tp>yqz7W|5}s-onvP}^xNgtnlA9Oj>8zrjdk=WWvG5Rp=iSH9~q$=EGH}vyhrN)8X>sYp|oW5=_j59CK(@CBw_k`bx!U3 zPaAQhqZ3kKGiGoq%cs`n%vHgX&9{h^czAdmf2ji*s@IZ&K}6_&8XE5Z!Rs|rHynpy z)v*Ubz$Uk%*Rh#q7j0seb$TZ3<$Kt+gK#TQGnMqiy z077YhIY8?daib^Y!dINL2?tB43QU)S0MiBL4kpk@$gNt+3EX~m(xuu#-GAaZ zOF1IzBXpS=pN4bTNrIm1h-{&O4M0szPP7k#6EpcR(gd#^H9(0yi+bsDXTwF|{oQ2& zPLXY9Nzj*h+Qy9zyZs@w@g#aJ9FzFtphah~P|jxbG4uN~!02*%HIK7aUVFgwbWv$G zGG!{Ms-d~aQZ6t4E3?F=qJo1Di8^(1Z{=aE@ZHB~98r}BcL9XM{NjF|D;NuL?lWG=?^7>TynlY7@3o4lXhxuTEkc%WiwewD9ArNElK?gX|h-5gESZTx3YmHZD3g>g^2FE9!ej z>LEK$H_TIKjhhsJ;`+$y+4{ns|0h1OAER00{OGp?%r4N(vGh=4x{RKc-{Dkz5`SC2 zhrE`CCYHIX`j_UMim3 z-%EW=ocd?S_jJ0ncr9F7>V-+xFKUg@eKC1?Z8XWx2|B-yS1j(rmN)QxKAwq|ZZ9Z2 zC*=zmnzhl|nsFnMem3W>#QfH8BBk@ENDES9YHX}BtazLVGy+_I5j9aY*Wds{oVdZT zPmnDC2?=`#S)X9$@q`0K#x4yb*4mwrI60GbU$6L96p8c!MAQqRa z>YqN3r~K|^K<)M^yWIy50x_Ai;yYr#m$#%O`_(#t*vjsq_f4IYw5WUa_VfnAsg#CU3QD43c5ahu@Y)kZnca* z&3E1u=~R5%dDS)yvbrotG%Hce6QWGo>^V7%DMaDcsL^Yzuzm@ccg(>X_r?YxV@jH@ zqA68!cBJmoOOR?7@|AeXE5jHru3G{u-46&7>Pfji1-~LmZ5ywx6Tf&qyX8J!?`5a& zqS&#GjZ-TBf1xvtHS#fvWZ16^(J_E!tj9P=-cUxoQ8P}gS8_Dgjhz4waHxnU4 z9+Pl=$G4W27W+5T1igcSKLGE${ea*rQyXD&TfNc4m+c(2znjd+; zx!%2{9*!c$(TN4KuT@IC9`(1JzNR?QL4!Fa!rP~6{Y>`TJ(jwG^_!2?T?cy{dWY-k(={E%yv5{KJE#r5Wn%kP7a=u5;KQfyb|1cFMJU&8K1Fr;fwtoz4EUvR*5{ zMG;y8>P{%9gTLO{;K8!Bvd$Loh zyLnDk{x;*=3+UuGn5@JWOQl9Wb6*-y?<`ei1{jNCg3orzS|S zFipJSMDMQZ)xR%WO5a#GP2?B3&x!kS&L*y?t~b}q(1|vIBlt*W7hb#F-yRBc3IK>m zgmbeX-}|mpwUxS>ZqD(t**u>TG^8P;l>8*fU_dAgP|oS@^C z?0m?=HSnd2lL%g;)~11oZ(%Kh7^#n|ZHHTPbGd9mRbwNuX2Q#9@7YfSCZ6KX*AD%3 zDtTcHw}Dx@uy1%lz?O8{AEg}13@$HanjLb8ynQ>0on|L96CHNM5h?RAFWIt>NpfpT z+EqVXZLNV?G-uJ7Hm^m*Nrv*D4<{zc@j3xgvcz8xT+6oJD@!VHo;l(X6M88h*!z12 zgJBh{RxHq>HAPaYoABd$uXnPC^!CWeDjcDbMq+uHzH{!p#xb-fCEDi2Qwf`Zl>Be@ zf7E_n$$1()Ak}O8aD2yuQqpvP&j=@*AVbRe_s^*?XLAlV;*b|Jk-$-RinHc-4Oy&@ zl2}y9WMdNK{j;1b)|%@B{C23QjsWEXPFyW}UbM=j|2$oAtkHi^{@S)cJoUJ#x@@;@ zOXzmOY=5WxA4yVgrW=BlcY1ZtI~y2(tu;>{-8#CYu85DnxG}5E%}9%a5VpIPw=2y8 zt7@uosy*pPMCnLJz&3m%(*iNP>gNMpX_x+tM`F(8qn)Tk*EQt_CQO&a!t+U^i!p8K zjElOjEZmCqsdCQt)+d%p|6ietMvs||js}Zj&tR8eT9(Ci(?X&vBAPL@m9?_@RVRyD z*kiCx<0B17eNX-G77!5b|D~>!JO2xSF%`=Ytc2M%#xf&9YwfXrlqY>6>yzDmYOhIM zvFEBs)Xi2Z9Ty|a0tP{kklV+7uQb1s#^@lJL0%poFK5Xg3HY9U$@b&ho-YMf;^v2}YW2Dn{b$x+xsx-8VK@{`64ZZP zN8MCe| zG@VVDP6JjQS;*D!EzfK1qZgWQ+*L-gQrP)}TcS_Dn^R{yWkbJ;f`6$?p6uhZq4#4_ zm-N%TbM2hIm4#knEmCZ#tYol8@~%&oT)t8jJV)~VGQ8OQq)@7ofa<8hU0`WuXh-*I zF+*v4u#C8R&gfSv~;e6)Da9R~u z8CoL4&lwOM(TlRg&{6fgLuZZNyYyr;tMr`9N>6dm$KI4si7eX2g4DH4-IUDi@93hK z?g>W7ah=V(Okt(r{SAs~O;=}h8*5aUSgfWm3RdR7#tgn{Y8igP6TRMu++t%`I*_Vq z+qt!xq9@hS1J_=t&Gz<1TWH%0M)2Wc&@;zj@UuJbRmqUgWzPlZxP!3PJQDMwoqxB_ z1F>crLHo=^$M|`lXUcm6 zp>W<2gcw4d{GS-(F~~#klEUCMb3suAD`+@Em!Zaz4zGzAtAj$M|1kFc8tAfp*Ac|d z`@I4!eC;n&X);}ucMG{Xq9q4z~0hhIpmu8BnlR2Rxs~LZj`fD+!J_v zVz)~nix&DGoToTN>|r#@17bw-uaiP&z+?9VsYzsjAR>RMwBIhC;p3tHvnO2^mQCck z{Lh{=eWGh6sFE7UFIb4F1=}p%%81MW$()^4L==k#sfM!InL-6a5d}X$t#BAuk<))9 zaO~Ooxi$ zl=SFM&eqHW15LavLsvz!gOlENX1gFnq&Ho+@34swmV$nI^cKQ|s3D|62rDs7q6Dl*cI7{e*SE?C=&21`y&!y$=@xEoj!V3X6jKtUs_Vk?p}Uf*VV zl)X)O^gxSb4m|rL25%0KMO-h!=NoRrlNY%J3jMx7f-I~kQ0_x++Xa!QM}xu85$ZSZ zD9B=EHQ~&mzKxh8MA9HYR{FVj#~Tg?A;JGklE9W!2ct$ikK(UF_nd@_&U5rP9hLXx zhm>etIw)iX{soH#vsyG`w}}US->GqlO$`T&f<(Z5mRKZp*&7jIS}1sP^tX69p5pn| ztL9wqzf~ogOVZY#Dnn9B0E!rxVU4H-e_bcA4=D`%i)G@!cDZa`Gk+g6L~_6#49FvZ zX3>8=Q?h|6C=W*5!JafrgFgMJ-zm1{1DfF>G#Z6;MXPg=!GbtP=30D!IZ`X|FREx= zBqu17ZH{LRw&wx(pWOjQ-tuumLV_mtptIa6bvQW90*H>tac)@~# zK`MyWXn2wJQlm?oY#_uf-TiCSYACT8g^&uZh*_J-(djJm_;dHQ{3B}Aa20QFZ(G|6 z00YTC2e@(5CoOf;vaX?6B0qzI+bt{=qNSYwvvR|_!_BocLBC$J@KxipS|WyxThAeh z|7TIn^x2 z@3Y`|4|nM&!&`Q$f)3?W07K~H-r+#`4BQtc3N@>5@~cHA>veGPkoRMErqJ6B+n#Kj z>Tg_UiDV$?XxH@gy5NWti6*Vu10|6{MU#{j6I1Buit#ND5DVXFq#E}f=c|gH@r)^1 zIvejKwzaEDA1td<>-WJ}W-?Qero|x&NDO3-hgKf&V5GOpaVxSRVutGYMfaPqlS4-j zsj+|&qgSWz(UC-}z88bWA*(~XV-Bmp(-<5p7Fsv6TgfZKbChBg1Fp+D;v@@0JpsArh|+5tR=CCvbJV`W>8Aize%;^ZiVAKY9JJMAYcs9iX?xqQ~3evZ6Yb zqN9=?38iowmW1>qC*a2N8Q!3vC8ibt?9SH?k50(%C zg{TkFz{!b#oWRszE@oO-=VB+et19A8fC(=pwPNN1gV>Jdz|5iPLS!UEjXYwXhju}5 zGvABfGReNPx1|Xi18otS-zVVn6xd|$wl(~M)I}qjav4!UeJF@YS!k4}9(IYuu1=h_ z;l>hUVL^Ade7)N3WNHoyjK}%PAyW2PO@SpOGPx#DfJ&#I04j4X&O+6}yUotxE8nHr zO<@8zdpLMi!jGYQi)uisz%VA)prhh0P%D|lZQV&8ed~QNtxNhS(Z+%!4ycJKjk$nw zQZb7;*q-XjRgn(V?EqJi|5}b#u#TSJufv)>$AE>Vw`*ku9*g;GfU&TSqJo-Xya8v( zzR@PIRv2#q99E-O84QkXzNF#S&gPr?q&($mQ0DPzkVm0uMUGPQBNZBOk#%4*!76y# z(dbb;Z+Ep z@zmBSLkSg?{i~_)37;_N`%;MEEFL#KjSS)};hNGqCb`_VpV2pB!$0L_jm*9&Y3e=W z>}OI=e2lV-%$(AU7}o#eYZc+G)%AAvjMB|FhK-)ZW3x(Jk7@_em2Nx!^>;aqcwsfc z_tW#bUF}qW*DMab*RNE4rw+mW0f+^Sxt-i-Z}|KcBR_ql`Ce)Ey8GeCTea1FwIMOz z(bUoI&;Z@yvMUBy(xzgj*!{vm{Y_(ABL)&3YjuFTm90typj6E!=T;QQlOB%3uP$YW zH~mk$exe-L@U!1N97qGC4b$-q55lEeLRP=0>OsG5!|Rnw2FBKWrN$vI6m2-P=EW3I zU=*Q1;a_QE1*VPO@e1dB%;J?g3c#rGgSHJi23jtFJ8&zv=SJq~BIX;l!bCB8HIJ^( zpExF~Wrr_6xLudRS^Fs+I07t3pMwG{Ncde9hNq}xKf5XYa2B15$CH(hf|MDv$f&0F z@65za9!5xo_K^+!-f^~G-FH>xra{EiEmqs5+!E6Ac=}>79dc;ZX$aO<+;-9PGO@oF6o`HL?!JfWcn5&fmUX)F!{@RyVHFR;wSVTrhYF+ z_5CwRX^M8@=bflVCGC~pdzoEZ=T}2CKZb~3-A5bDK6#0`ykt&yKg%7v9-q22Ny8Cv zeyU!g!vq;CvajTH2&X>}MbUgabW3!kESb#MQ2O0<0n+gLoSy9Z)R^FxSZBa@v)N>M z?-Hc8;o-$3fVFb{`=wdmOU3>X>&IhXa-ur(wozBb9k{LFQT5WQ$gc}+?Hg*J266Ipr zOgHBy^Kz64(mz*c^xQ1&AJ8GFYZDFS??Zn4*=TdwO`($iZ>v;lo+Hx4JJ+bed~_&* z5I>SUyeR<>2_llFqSmgD-kTOO1a;WfvjoW4t3Ev;8Rt1I7>&@ z@FJ4Kb^#X^>n%F%6j_=bTSs2;SDv$gmYJejP+Vfz&J z{&XEIasM;SRL)WBZO^5+IIKO=4ziR+ImX1n7s|TCQC;2k4*$;dBtB=4tjq)_#^T`V zgYmhV>aXa1_nk_u!vb+0k)baO&t$<$N-Tm>5DWM4{Y1q-O5CEubB4|u$JW=|BTVUB z{o6i6(s8^Mfod2@Rlq3R>UF*eeEuXN-no5OAg(zr^n3zT>;maRHSB#U!r$>^RfshY zXA`pfdv^{QIoW^H!^_wO;xJz7_)EV6=~9}E~= zghYaWgX>M-f`s(yJTVF_??{DB<_0|N(WFu-t}a(1V1|VSKCK?3jrq?^N9{ zHm>Zu+GJYpi4lB3P@D%k!XAg4_Z_(q6;%!U@^;{9FsG1a3_UYm#Xf#DXE00SDW?XH znMrBU`|#3bA~ipB(dYb0WG!z9?)50F=6+xWi{b6Y$D_s#{#Q8%)IOu)u^11=egp^{ z!~j`IPmf^w+MyI0cr{@NWoKRN_BmCZ%33Wy`HRSfh=Xp|;0K)gir{5v@ydOxB zM=lFyLF+zFRLYH99|8_%c%>;^qB86}8NSD|3lrICig1h(WPHuck(||Y6-)g-$3j+S z)K)+Xf~M}k4?HN60+qSG-WF4!?~B;380bKr7;)ywCHNJ{_(pa2Np0z03hO(-1vZSrs?;VjLIZQ9%(Z$R6RDEDz*tNoap}S1kC^Q0%%0+e(q*siqlRw^Xo8LQcYFNXsQIs zyouKI*<(=8ae^j-?Cc>vl?!s|93qeUm&-%`NM$al5olagc=H6)=%Knq1EZjayy=Q9 zS3g?;VmWF>BH?Z`1<#*umjrt&M+5?6t~-qG8ELKW=jVcp9&pf1UsVIAx32ccXs((f z!E>LK5Md>>&A%-EwGv(tFBRh9WY+*(5lc8>Z^ZmSIb2^Df>RR{e02;LJ0B&o37wAq zhgCa;+Sl67OYigKK676Fpos<4>!XBhigb?fuhdsq=lAqTRFbk;oBph|?q)Uat8@Hm zGO$G4Pw1$D1lW>Dp)&yT>A%CSwJ_1plp1t+wlljHD%x3sA$5#=Y6t*SYWh2XG}NR> zJV|a0I`*RCfeq?S(c5$i9<*9n6tdD8-bDIq&5v{{ST-;`5b2eGH(9hWx0Fs^NZok1 zzjY?6TMQF>1)-~p$LA#vH#i<0$^E`X&dMr>I>vg5q#!YzeqChtUI2qkag>j%&H)X9 z?c#h}jnvRWhDw8JxeG%`YLX;{iByz99HNrY#aG-f8=3fn)N^zs7hNQ&sY)c-U5w%%u75;9o*`xgRQ4j4b)oDSNd-PJtKP1-#^03TkM zSWG5M7Eo);1Y*V+rg0}mZE{SdC_M%v$CO~;Ki(@FOO!kL6xde~9wA$(r)wt?2s-+@ zx{p!EU3b=U@hzAyt;Vi-sslTp)7L<|D3GNB5|{^%Exarg$f;zp3gC=ue2p!R2EpU2>(Vi4CD(MA%S6 zQAkeLcnLb*Bw}cpGxCQDM}QEIuG@7cUm>T**5meI1mQ8xGvkwK6HIXmU2tz&6L zF`p8;8&lO_pnAgBu0lmnJb=A62bw!a;|q0BK&}wgkxHVC9mzhV#dA6TDoahbjRITw zLR%r9^!q8fMj9J_Ozm}}BhUYGe+mUD^OyAfnNWj#v{A|tm%yK9A;5Oo6|1bDTM34f z>t*;5ZjB73k`EhjD@;Da6I8u*PymJ&2?i@-8iGVnB}22JMd~?rUUTzn3;5`v6R!PU zgdjnYNHdqOW6$!`Q3p3Jiqss6^V+o`T2|;3$miFvUALK>(GY87a#*>#J~`-UDZSm} z2AL1uA3?MP<0g z@y|A7SV-fCn8kL?D#A9VomZLK^;cKppu@Yvw8i%@%lPx69pH>j^#6u_N>SSHx>Olz zD3gZKsqg|1j^F%I%B)aj(71^3uwu=Z+$>>vq1~WW%d2{Kja>DA-4YJ}x@(8fd_e!p zl6YKfp@O4FLRM|_N=;q~4I1luL>UTYX;33gZeFjYG)f%A5LvS(QRk~jLjQM8^mzPV z`K*e3CfWR6N~+kDA+=26|E>KMfu{FHgIQ;3Kqf^PLN#&5S7bFQt04IB!Hg{#y#LoPKnlnm|DRb_emI>RwPx_0&Eyb8FyUhul!)fy*RNkME-s8FB*K>B0fN)K zM~+3Gk?qFJ|Bj2Jt%UX8p^0laO}+%BSeKoex2`c)TFecx>7dm_jha8rE}F#GFWSD+ zNgI;>FaII>#V&;fSk>FPRNHLpeG7kMBoC*jr}xgqh3y-WVtX)}8RS#S705_VzU;{! z{@+<(hd!BHjJsCLLX#KK%9)x%HXN;VmrgymXi?A{sVOKZu&}VOB_C@MKL76wn0Vj9 zMb*K8D~^=3w!*gmQwVsDgGoR@0N6**@Ny+sU#E?wi2V~(ZDuSG2ZRHm%~DvbDS*-y z*qsJhXfq;2*my~iFr%;Us`gWOx4`Iumoq=HP~*ncaD^HhSl-~aH3ibg!Yg(>H3!b| zvK%t+g!d|!zdy-;S=g3tTlzH1wfKOl$N0YhS3{d=3K=Y3R# z9USb3Ei6UApWvAWt%?1WeL=wv>%MVX7O~C~;XeruBKr4%l((PRMHL1n?0tIEXIR_i zOWga!#M$P?+Cs-6A}kgnl-SLt1VWf1r{}u-)!eh6?2BQbk57zn%x>GKdNaa#dox0o zmZL5&lv&I&h~fE0H~;nXF0yI@t0La{{q@eZsGFDNLGA{>8&Fc>cJ_hDW1w%xFj93} zu+04UpIdCl>9(jWhcQb&wn-@L=8>t!6TYT zfEb6;9VmZAU`4T6u5nqQg2iNbZX{-5p)NpB!GCLhI4<_}?GQ#4 zCi3vCGq4QRtd{;;r@`GTu!93{U^E@!GiiSmS zJN9V1L#4M@(9_LbG_O1GlhVPn!f6$S`Evv6uIx({t@ zIaSb8d-`p_+5?^8iTH!SSVEJ#GaNt>|8HDFc6kzaKR*ky%O%6M9J=}yIzQH=Fg-ds z(r>f`9Osu$3%y-LuWJ$tKRxSRiXOi8Td@)T=zRN|HSYNhs`0PzOU_F5bj?~|_sKv> zA;MnUNp_Qq|B=4FT+8WiF7s9O#u8=O*dny&AKiE75tUaz3%-#CT+Pn@tmN{)xgxO1 zWj3=#cA{%PWKCoY*xf?W&X&z!1EK}I0kbh^5mXTjp5GleyKgI`9!$O+OgvAWz$X6q zR7-dqYm3afiYxzi`vlRnl39*v{5zdHyP+s^v7M6DbF@H1nkiVQ-hbCqTN~OIIDH?Z z#e~AI&yS9N4;#-q-rz_T7xCXEYZ8cO%{`dT6E;#&akk+VQ($k)J+%dUUi#lAw_=!=lQ~@?S=4nVDtY*m9DC&Y}@%vR>_^$ zN}P!340E-~=39EY_fh#(?f)Ny#NDy6vCux_AD<0S`w?GrFC< z$oJ1cMGjzn+0`mBibfMBva|iAAvBa1L&UABtUO(1IPmo3E;J}xyIn|ufu%>3_@!m~7@9x-Q4L1|0$$wj{UaK=(O7+dkn^=3n+&Y9hXR@0QosAT?OHAH(C zSb_Z|z->lWVYmQus<*FpR(}3o_m%&mOe7^f8LWOJ9UKxFX^MTQJ+89mg{YXAVCi9b zIBq=WF2BzdkO^UA(|1UEIKD_(L(@Y@2CIq080e=*z`PR$_Pm_v`LfqZ^0vwOq_p{N zt?b4wPuGQmjv;G{rV-iyY}!4yNCQ#|2VKn1R%Cv^EFJSa<^3{R*t_2PCX`tQ{cBdc|7}3` znLtoOQHu>4R_|*#IbwEr_>#grE_bENRW-YAI&VeJ@#2cC{^e#KL$S*39S+~1{0UF+ zWU$hpJN5B`bj7RtAWfYLsnrIsT@f$U@WJVJgy51&RE^aL7gAar2&3Fpg}#s7_Ew-< z9(ht8md<5t;wWL^Rfu_#^v)udpAjz1+A&UNek5a{aWs|n2OWZI=9V)c>q_0)W%Kxx|6zk?sLlLV zf6&ujXgmd6#bI$HAss0WDbC*UGYqy6Y8@K=+r>$tGI<_OFzr*T-9=)#{+_z;-hgGm zo8PeC7{=VA!Y)V5!Ip#GaJoWw2=!0ZiuQ!fVQFEq%vJV0nOW$)S}fyqRaMHiX7J50 zI3)*hxtKElR@xi~N2`7lS72*rQh2~6^o!6PH8lmi^;v#^eXzwDZ1HTLNMJzP0@9zR zbCbt7irv=1VboK8yHcT1!g&-G`^tNp^C^90s>s46r$!~2bcS*|s~=WyNWSE1xO@%n ziyB9E9-`Ql@xdq-vY693GbX<1WUxVJ(@g9CHc=cROx-J1O#r1nV6?>0pD?w&TpQ5S z5dW`&hMW8OKZB+7wYCZgoKH-aK-lf_sk5oE@$B5(C!ul;*D5&MxL)=?W$`5Pv_Au5 z6y{&KLak#YXk}3fCG1(O0cw;fqU(i*Y3!r3sq>9Mu3#Q6PaakYN@pBA&+@Cm2yt z1ou2iL~+Ar6GP^A8; z_=Sm37~T5$L$^}Dp@eWTh=I?cLH)pC!`PizSOLg_{S1I%02(>&O#e?jhOSU37?H{{ zxF1bLyeI*SGaF6>j7GOY{ODrMS`~13AW|9d9$|PTY)jXj#|~L(PJ)=qOT-<+pp&|L~QR2D(8MHc)pax zl{G~1`f|=j=f|HG_UiYUJacunnW4=g5u6kD8kX-E_U61@{$tHSG5_)d7)8lo%|w4=tdZZ5jxWT1;0rL6htH4tHnQ>y;A*@S1B zQgX5?0$!Y`Nf(UA91R$*|JR?x{;PU8YLUis9#tEgR1Vb$Y1olQhFks`FVFe?q_+`q z{nO$(Fg|5}-kSL~&rGywyOnu|%VVQY#_K^H0`FYOMEQB3oWJ2*#L#eEaUv?LuGH2i z@;hao8jo;v{KTv9L6F(6UyXf$cTw+uQ!e38I3e%=r|`cqJ&n0C*Jtfx3C_Ay%&{U| zm^#`K79;|3U#*;HOut??UslYF{T2^HkAX$Ors8bXnh&TUc+&}_@b*y}+GEW(FKZ{M zEpw+H@2~9Nnr|`}+LOP`&!mQOEDgT)oN`Tj$o{G71WIvhj3uVR(o)**-#bp<9`AW0 zs~#R678VvX7n?e+BsfVy{vgIR3~Y3ap%6jn67Bovn$Wzc%&+H5^SIM>ZOxv#q8f~k z8fqjT8;pqDK+A-=l(!Erl9Li^4eeyZ%;!GdBzNZ_J}oZD^Df=f>5V^$bejbCFUEGA z(+}Gy|C>@b{z0P&0X_`HB_-S2+p^P+32_8uJE1>ybjGhe0S7~4jfM&{kY40|WA_;1 zThp)Se4|3g=a<@v(NxaWHk*p#vbdbRmH4@2*~=Df~S8ohccU)#lt2vzx*H^-1^;Yzi)}X5kQ&F_c7P*kTj6LwIGEo9)gHral$2Fa14En=0_J z3iQsN?zJtEqJKkb%uxTptggE4MK1bRnr}n3tQVHPrP^`e{&g`SI<@8B`mbg}p#!3s zn#kQ^KN$o6%CaP>%hdjbKlVnA?SWX7~tcaLlXGmtq~7!){q@|paC72c&(t6@(d z^{lLFDc{J*#3 ziMY1vB7_~Iuts-I?ZSQO1o#s0F*-P3hL8>X0wcWKdSj?P zaFYSO9t-BoZF?Fyy=R3coeL0-6v#jp3gtUM(X3UzSD6OMhm^HUJb7n6;(`i`Xo^jk zK0yS*O$_l_5$|mkU^B9~&)Q!xw1GV5fHYpmQ`-;syJ_1Swm#js+n+Nd7?f3zoM+Jn zaqXITH|f`AegM4J>kpAoRBo;^vMM!axjd$kQ0JFdp{c_vi>u`-C`h3@duTN*W6nAH zS{PDAq~ORZAO@6>$F;J!_#+4aSlsW9X4^EIiD!Cg3%O^BFTX0H_=E9 zG73rMNS)<+nq075pk5$E%JO3hnmr>bIkbr)oG(WtUX3NUQ4Ru)By9FbiN0Qv)pW$( z$G|jcJP>(c*7uJ2)8CR48GwugOxDBDJ|)wQ+ahAqey_*3ltK8XS%{ zM9;Y{A!j6ho&82>m;gpnhQAr zn#9jVfBzcY9?b$T?+$)0E_{&8v{r-jX?6hoKqTIDvC20Um9#xElYos&htYgc3aNCg ztVoLSPl*e6SfkW%Nk1l?n+Kq5U{Fz+Wg)2i_e01DkW?_UcrhVzG=xx%#$my<@bFFc zgy{*o`Q zt#ufvPGwc(&$6tqMzI*90T99CK z^z-Q95pV_T-wd*v^S?sPVqvURa##GNPWin^Cs;X_AK4Je*o#njba4j5syMZ=Nn^c- zE(-g1LN(0)krKyI$TUB32ORy4H*p0Y& zY~_g$K!%#lGH}#N`06IIvz61@IzUI`*R0^?8QG5*dM(yF;HotUW}Epyi&H$b>{Up{ z=)AJSR^AMO(@3IOQGRC2rioH=w<8);FT9Y0uRNawkGW@we`XDjX>EAWji{gYt|88R)tgXt4+sV! z#5#9*Wa6;6a*q7 zvGCD;I`qTu=v~xfEW563is9jip71!mxA9TM;I>=KWoC+dv2J;Y_#(ZgULA0k_WE#w z#~XheOn@3>Uj#_ol?&u#B_(4&BY=LW@OnMn04{*^@k4dAxRG`_>P`m|L?)N1hZ$MB zS8(b4 zS~vW4Z=`@U8b;h9A(N*F6XU*@+n4FxTtUk>RbF!AjK}J5hPIHbYx?De5aNsEm%d(I zLKs#Sd}CgQWxeXCvrR)8za}(4-xCTkHgS1$r3ACJel`oIP1mu_i~eF&W2&%3OA2m} zxxj(YoD4`VP9&LqxPV0C<;o&L4d+6b3J|{UcU??H^@)m>+-0mc8$Nf^#PG9N8APUS z53AC^2&{Ad++aVNIC_sY%G)8LyVwCOn7DX8e|Zuv~!EwMABM+v+ZPR zSHP%D;;H9ub9R&Dm6%6$4=60^={);gOymiZsUf#bBy{RmT}uutoqym9_#gCWr)iwc zjN!u(Kk~0>*E>vT`EC`1C%JX}LSDErY4I~=Eg`eWYA_T@sompHcojdq7OZ{AJQ zeg=l99Y7g`MU1=5G0>2RPJwZ{$b(@O6O_x z(B5a0M`o*Uf1NOXcYR*1gpz;R*uQ3qKSFu6dJQ+ub9SDfzl!}}!Sm1DU~b^tr}hDS zv`kUzIaz+1WIDB7W*2mbh-&%1(bi#ulmE6Ei!Q<0aClJ_;FQ6cx~9a~{Y}#Q=goPt zT6+`!f|rDPAHm4{(MzTNrCn8-X45Kt*8o<WfY-P$ZzIMZjv+19a>jZmP2z4}ecILC$F7Ndfd`0uZsCNizw^J|{I2}!? zm*lLkhK9}Zc*{*$m4k80T4m&^C2Sp)f0x3~dBRQebWa|B*_pMtnY~hp1bv9WX@BdE zc|Q*@4^}1l{(hb5eQS#}vsl{aV)g$3`BFSR~Nw0RB$_s`tuJ~qD8?3&IV*i}7Ewe}b^=<`eVB;socOE)hEvc#lw zyUfkc%R^WsF-3bAUFlLhRDj9*~FX=77Bb$|#HCH_5&b5E@4P)Ig;9rsc+<<9Pf6jW)lKaH7u-vfU;Im|B zDIUAl6hZRUxqR$3=buc*#wN9Zb{@OKOw9z}Wx$!t#ocTQ>J3-P&@0Jv4cO*aLQBlxQQ>fLR{EVgT%|l9Q9?fgdA6 zlMTm<6%9_B)$qAiI4yjfh<=cmFdM$N7M+vVrQ&t*z3qn67xP+R!a0D_h`{6hZJp$F zm^cMY6*1o69KH^Z|Gr3;g`=Jf6)-D}ThYh! z#CyN*B^+GEL~CNX?MK?tQ0vt;vM2s;!>NY>&L2Kp&E}gT*VxyalgaydbaRc`{8!P(|bBzhBD&$^7>BX@zh6mPFS(&s+h1s z9Bb(7nmI2mYGPORe1or^rfGNdYxS^rtM|GNt>xOFY|JJTEHZgr!e@E5IhXGCnayg= zWl1<4N)2xq4<BLm zbIkBkuFB)U@q}uo{MWCHOF?d{NxC}|QdVM-?ngmTFemS;>P|0_Yji97FpLV0rPMw# zOKuC13TgF=znJ;TpC3$pSez|iN!U~}0Pyl8P=)BHCp8?XlC}hRMm04xeN!v{!(o>* ze~b(lkm-3Psi>$ZCe}|B=IspCpF$lD9Kkbjf4b~4U#<<f2Pz=$|~2jK6xls)+|_f0`VM1kGi_YM6e@dtnlZjS|t57Rq6X=A`bfu z`CaWlaLuLPBI}$m_@@OF4Pojrt!D?#pI!O7E1)FI(8IjVhkdm=(elucFh{zA)rl?g zJSGSSxxfFtpqKlg%m`gQN1qXMV*yf{AM6EJj_d?Wdn~w!ybrD!p?nM#DXTw(+JV`a-CQOZN%fyYVn6h5I>?4(P1dN#vVwhu@FHq z6ff4+g)on1>0pE^P*>m!`WGpko#@Vu zoq2$1^M}{9IpeR)>*UR5)oj`t$8_Vq-r);M7&7baA9gLP8Cty={~gSjPyfbMg!es1 zuxtw)F|&?-wQGF;i*&9Mt6P{+8srDV)xz&_{k=om_=)t@FCi6Lz6gr$Tx52O$kx{O z0iY+&RC>~!4iY!nTYb3m^=vG6r278(qk)x-+}+E&_hDVf=ow^}=0_Mj1DehTr#~n{ zCuZQd`!0wv8Ok;zRN$<-E$GOXUS(-IW%@p?BIB5v*q zdel>y#Fh(XxW%YeJd5uv{a!*>d_6E2!oQ$dc-QeSI$FD(;XDOvkgw{tdrjpHKI4vK)@4bVFwG@@HtM{rv9Xt}#NP z<>Q*eQDx=}u)RsNN|o0|P9B5SowAW{L#8LgDTC0B6rrP3>yt=~{hWpc&+ods80bHt z=H1#7es8eMMneJ1jG~AV&wWDrfUXT~ z4vlF~w!fTpI@Fgo)N5)r;-^@=f*>Rzf&j2DSs;QfEW2pG{wTDXW#n6568jpTWWG%5 zt-z;ZW;{3{o-mEn)o@%w4qaDE7hb6oUMov>l84+}@|8{lCKFlLrw*UH2;H~q_q{xe zedkm9o(1bt1GKd&j6a?>J;nFq2b(;G^qHPU=WhLv?kHhc_|nm8aiIBj3-{w+E@)~Q z^i@kTqj;A)3y}|%J8b&sn}4i)WTh9PV7XeTG5V<~$m!P`Ze~g3pDs(J?-LDU8HRls zH7}SDDO8{W83Ajimb)k?g~sCqeT8MQQI;M}yC{4*P9>Xza1-S+#b1e&isuVF+B+ljNRsMP+&pxCT@37K6= zUq;Zw++99{E0=yR9Sr_;eh3{TO?W@}{q3?M+2?+4`8ywHGgX4Bf2S_<@5$A^s{d#) zJ*2StmVL;C@pgMQB%UreLuaweC@H*KCB3x+)w$l6bBM1lQl!ZR(NE(OPap>A!~iJ2 zaBACw_2uEFz|sQu2MeqSQh}s7^^*R&u^W<~S5Fi%b1s~>eQ-8PKU9SCvp*Qm3i;B? zDkcVVNf2{>He2R@oAO!m^S%|OW|pkuHgPW2YB5qpfeYG}w86UiW-G6tQ@B58GUU-JBRd$xRx z*!*^oijaD$AmAYW@cYYOIq#f_Le`GA<4vLn>4UIYL}##@W)p?SNAozj@S^qi@eQ)m zIz20ubiBoEahWocq2hnVnl0Aye2G}R6t6DdPdVD#{yoSQ%ky_wo8}dLG0Afi;B7a( z2H3yblT?&n61}eG8ywiJWG5i2Rm(Hy`SMg+yHqW@s~WqP57Z?e9CNQCqoT^n%T-lW z;{Ja7QKjInDXNHT2&fMugXC%$;aU%{+hTPl>8{3{aeFNVFd;BL3|*qQKgTvBpqRnbmZ66C1kTPWrzu8s;SMj8=Uc2(PYwkV(qc*Ns#&Ls+GY z%@8;9&J9L$JLCUtb)8wE_5V=uJap(E4^>dLj^*OLwH!DZFSAN`*eDJ0(jVdH1?BF` z6Y&?<%uo}Q!9^11Tp(Z;e(OFKq{qCa=PJ?B?00fo+puIp#bi6ITF z`a;0SyDyu9C#`DDV+{_=4MxqF;yvlDUzi)h(?2DbG8Y@g<+g8}D2Y1ZP5IM+N_ms= z{%d~Dz+a-LCisq36pC3oZ_*?kZ{6iVpa-L$+#UJoi@4EEgnYEsi|aOmummBm!N0(S z!lxra7^Obgn(@d9;<@$TTdC$3PeM{y*wgf({gw{v49-UR@~NF-2mOlQnOQu8E{-GBp+&5L93NiZBvQ}e2}RA zJLrMKQUeJ_2-Z*lvvOkHbt7?AeB5~Az2ec!un?Ie$MtmN82`m=o8%OlHAxJ4de-Xd zZvdeALZ_w$>#-ol7q&aizF7IJXEB_d2e zIo*qYl9u`uiTSyA_%iN)vOZKYuDrnk?Kf;(2{irGM#>&V+yr((L%X zKex2#PO?PC8dn{-nk2^+OZnarEfp`bpAcoqp#)Dq@; zs_Pe1iS7PNz5LO3m-R2*1kzl$lv{j!C5G#I*QT={V}8O~{W@lM3eCquhOMr5e13Rb z81sW>Ma^8X+X*-GUQDDPYFKsS0^q)zgUrxWz^j*jI^UW;k#%UO-bM;Q4$>1WDyq?gdFmZJI}VdQwLH)c`a z8{65G^EI(Clf+RVygo8%mBY6-bkB}hcj8bZ!+3n|Hy3m9@Umh&%S0cyRE16Nj{!ris_U*epZB*|;iMe48=~j*>{E_!DY~U1F282Wef|(B z%v3s41b?RWxK7+RrOj^4Z{L3~4bBVpy1NX!U(;#NX0*+(dapenI~Zg?Itkv2*7_R% zU#839)EB^)sn!GQDPjMyxcI;E%Rcm%hLKfBRp8c9>fX@21PW2Vms%L?S404p;)@mt zU8|(c40VDik4skR*aM1(f{9lD^5M&4Nd5W9YRdxZ0_z-tJUHWizYZG7l_!A*YO5qo? zp1Oy;NMn(i9{bf0#z53&YWq_}&fK<1%n05tA`S^IXOE5N#SqSehrA3iAoQIbIS|Xu z(oPtn0>GOkiwa~CzE<%VVx0;ckbl<(5dFb!y@HE}=O#!wre}0qJWH5BMAj=4Ws#VH z4?|v%8mVV#@zd4a-PG4swg$c5_VVLe)=UC#I&sQlPl|KF5s!7(kASdMxH_l9^1@AK z?wEas8YdM+WMncV5Lj9J)=5@gMUVk2QS1hn^V=d?&TCTnB0{P*IoVR#&-%P(ZYSv| z*yOThUq-m%Z%Xsl+6J*BN@Yo?fx-Fmncinu2W#h02SdxJL5j`rU(I{&=F6}?I;SQl7nuE& zk{S*&-rb!Cp^H*=j2Z{-I6vK(DPm4fPgl@1R9J3u+2H5>!0ZFaz5vXFgN@D9FGOV_ z)WrsHf|(r;{sF;wveWb;fWw?i%LfrC5Cb!2A1n$~p2S5XSV4QPb2sTT&iab1`B8N# z>EtQ4Hf~|j*J6{Y{2SWNuum~XdFF7!{-34jajb{uO&RqfalRDMthMBrt4j2VL@woz zF_7x9ZHo-iH8Fr@0c$5-Dki;M4^C~|ba#^>bZC?IGpDE(S(zD;C$R2qUg z-$G|lM%`63Kx)x~?VL-SlGe5l`Vb0*{aNx8eXm7setA)`bxnRKWvHJ0Vk7jQh=ESp z)xVvRpP(vP`wUv8$s0K>d16JNXOzCL#W{q*7Ut>3Z67tSZ7%Zf|!tXuJZL;qw9WyR(&4M(4%W2ylvd zrvMlix*v@!I|@9Cq@nN2K&D3xMdzxK!7an^%f5}!h>RruTbZjbLqAwXWi9}xTo8~{ z9|9WV!m&tgt;QiD#uU?ScPHkhNIc$wZu1+k3&zk62(! zVe20-rA>U+Px{)|a@pw_3dXQWIVrM&3zpi9-#^a^>(LZ1Cj6*qN-~Gm`o_Pm9U?6u zaWGewMms`Rhwot9_&qyetqB+6f*Ck8&uFr)L>Ij+qS$Id{At}$NP6g-zul@1Lw~** zv;aUGcfUE9OsGp{{2#@oga|pU`cCJ;r3#mwuA}18B=G7~=U6jK8dJmZmQ@W$`*us~ zIL>1IIQVgi>G4zflM1+H?AiLkdc35X-D5@QV6thJ`1p8H#T_@DxWCJ5j3Lh%)~n5R)~iqT&xhPcp#cHHBO9e7=X}|v zHbC%enPx+KE`1fsJv;E^8Fb632)gD1E}X-&B`X0A+Rr8iCcnq3VDa^U?f>XjDC=B= zsH!0z$oo-_02l9%*@RHpDg_|<6DkkXwJeEuRYMx1ODHD>aV`B{qsU({_i;efdC5uZSq@>$7n~fmAl*Gm14zC;F5Uchcz}?W21^D6 zahB6}2=4!{jm8qsytL45R)8>ffxHW${7v9U|G&#>k*Y}x9ohW^+hvDg2b`GgB60kS z>=8I`hEx4u!`7ve_8UNLQz=p9nP_m-1sH~eXK?>XI)12#f>{X^7e1f~X%)_#I%5d^ ztStdpMf9{IOtLk39>C3S*d_~r5&UhkRNfQ??4YEJg|3gpvePhwYC}CSX#P#C@2j$ zz+dq2QK%#*03H>XVUP*fXm@Z{r_#+Osq=Iy?gwKjj_1lej*1GVrlzR##;#R=u@)US z7Z=kl%ZD8I;P&7W1pik9uvskuBeusaI@#Xq5f~oMKsVV8?!*Pa0}mk1r{1bz@V8L_ zG6?pkD+^K?4y$ZQSy@zQ=tFfO86^J$G*t44(|y@1`1c2FkZmX91F+qv1>c`CvdiVL zCj{`4E!4*Kl91YU+M?(Wwh9paUi*v4cUxK z=l6*IXw?axi^ez5E@d|0-{CXr%y)+O0~`KDS2OrOr~Rv&2M~y`Tf9vwG`*q|m`0$Z zg~zNnfKgrhS<O+SGN z6xbn_c-$~xxtt#f)eVsBG)f|u>W!6UIIf(U4{WDb#(?|o=s#d=>2`TNd6}@wI6S6C zk-;xd5sdt4B8!KgYqi`y-+%CX4coByIr;YV&m24$*ev>W#L1mbAZosFo*ee`J}c;C|)KEcSew zS&FC9Db?uUTHi&dZaYzAHqlHn0~~l4?}GmIBuwp%q*!U<@HeA238_-eXK-8^j5S*f zeN3^^r70=VXQ`z&NtjGa!Z(=7pE0>8)1pkt$HKy@GL^qC^B`Nbbt&95Jp;c)Z(s_OIb+`R7v3<4T)VKo?U|xq)9)ok)HXpA&_*OJ?@{ls) zv8_|tgah}FV(1rLm!yUv)9h;sj--TSfx@Htv_`OkDS9GlkvL$otxpP*N%Ha^+S2WV zYwK0U#C*{y14(-7wfIvhJ|78SN5}-$;`DYIi_GD8cA1L*F%uP^Aa|p9v z3%^gI*YpU!@%gn^T4cB_)6)&hm}U>H+kTguXG6a-YjbAETo4tF>NVxR<=7~z#nr*5 z{5%?aAMJ_`G~ zY-h97XA?${h((%3x&0+({j?kOLk&}f6+Ncnf?glVHAY?_$rvD>;};|7>Y7I~%3U>q znN@bZ6P}#16WrU*WYfmqw{QsV&Sn|!ZqL#MB)mjKddJgQCwOX324l^m5#8J1xt#yV zm~B*V_a(PY0n2uvahF}?W;|S&@zYH|Nbw%wqEq93W2Ht9?uSqXIeHx@rY`o)yh%Ru zR33Qb=eN4M_^Ckgegjhad$7MZyb#rAPMgY?hC!P+CP>KxS8b;{g^mkRWEJS1pGC)eC0`9TB-NB>|FDoG?k@7RH7S@+sdjU_ zModPFAPkV_6b1Z5gv|a`)M|GaCNt4(?+4R;wj2@5QK)yB4EyPF?2YV(zC-p_N_zNt2#9<@J zDz6J$l%E36QCRskFSfWzPAOhZ#v#lO2KJY0URcId4H-NN#{{=8CpWJoz$7wL*w=f0j`g*)kAcw1V8Bob#w+29$&Yu~v%I3$;3U$q*JsPh97 zSYR`MRi*0CLFZ-rppO4}`)oBWoCLB+TsS*rDoAAgr$jza$H7}A`q@4G@$8$AC%wh* zhI}!^q7Hzi=gdy%Zn_9RayQ@K*Zc5wqV@ghipk;VzMxn2WVOCzcm$c~kJ8nmG)a5w zi&lmB^KX;3dxD{|!H+3pjS^b*N4r?>3!Y2MoP3urhj|qh)TYWAgalDef*V~w07nN8 z5s^x{6@5j7s;9}ZeShk*cWN3mOITu`&6=j4{K6QYpB zJ?cS1jxZ^PD-Z4@$WcMSOaK|jWnur71|hdt*xv1A!y1F$_>3yw83#kT4-i$MV1Q-R ztc+uYd@in`tTL6%;7V(g>qG{5Poewc?yQ|%+xVX$Ui-fZ+o9_u_ig90u6wDj6>5{W zd#(e(SZ5uQEVG!P)oHKhG8tAjL+AR`y)qx$YN9L_<~muNV-B%4OrH77#ac+-RlLw) z#U5dgOtd1Eg#6%qmdm+PqQ#&}Dw3ivyd`Y*a*lHD(_nj3W93@1k@@@sf7Zfv`)w>_ z;-eW1JUfduNBuf87azrW`iGJjwK_bO%9^L*G`=uVSs)*1$l|mCZ-Nf(F&G1wg)(J~Hp+rWTo28M+=BZ)+t!p7r;n zE-Hyf_UQdtLsz3JRyu9Rk-SG9-+d|?2(sKazjt()wAs54SFDoX^)|#<^E~v!`@Agt zc}(lzVWw3rw_(^MFvo9V)qtw2!YwCyUv8!tXc>DMc~2vNQK-8}=2veK<70oDM}Owy z&2wKuA%-Z_yBYfJNgpng(YBTw&F^cf(yfE1|E?)2Pg7XVW_h}%-1UW=)4KaOLGin; zMB*{$2c@sux7Rw}`co$@eFRo<@RQg`v!u4RO5MSC#$aXz8E1< z)+#TP+|t-(3n4WH6W#}1MP-rr$aeSD$5j35Nwa=1hHrNbW`L(tmmiF1lFIdbhj*{t zCtsjwe|y{?;CplZGeBXn^hME=-Xkybefj>$^&qw{0u^h2{@7~?_jz5jawmXQm;BKz z-oE<-emUTnz0r2hp0WERmv-CPdui2UE)RV48Z>ghbplW9{%h@KE--oJPp_ZD%kt*h z_EtMn_h5Y|cX4uu7WGd@XpkJ;S28Tc9=MV7j&{AnBVqMV3u{D~-xnLpk;En1Q36OF z7i}`rc>KSwtGGTLyxW|QZs!(O^Ln3^0=t{4#o!%nusnK^708(-KV>7?`AdW1Ery?q>qvzNR=Tkl?5eu+i>r8NDPa3wmT zx2pja%hME}N9trpGBu#)=hqlKe_V{x)r%MZ=Xb|K%T1VTG4a}ZSEt@PDK*#)E~^g- znQn(tRvI<#%l$-rsyu(Y4mJgZ|US z>+`bzq*Ab;UX)$@HmKNpDS*IQMK`5ZW1x0xQa5q%X}`#rq}->b-?7fp&1Je@>IVa# z>^sn&i@bu%u^K_gN^4sbMq~YHTD=XRTZa+nU)S`C8zW(4QhcnD`kX@_()9|{Mi^E!PKI(xUvN(Y{bh0$#N}*sygdJ z+!SfQIczs1&dEADl4jW1h)Vg0xSMF3HWil7KqBYX5m6HKklBW}L(_E)YWiyyqhR#9pB}(EEz6GmulCT-&&{enc`ZmwqF+3 zWHIO(W#=Y4!$h2O<2e2rRe&Mgk7h}rtMIb-VhEAUoP%s1UkbNQtg>qIM3scf>_MxIyyp4Fym*+_*TU%|BfT zhQdPY0(GKcry+@fTF0yn?HWDwWRQGw-cWWHZingN?RR!ShVpj1D0AfMAO)aWp%ep! zHb4o8*gI=W_o_L86z)|-fGiOE&%V|lBJWYaU)cKZ6i4_i>Ipes-eQ}qP*o#lXAN$9 zPEpk=iuW`tooTacPpKj@)6|rlIdx${;19`YB%iheU|p4yg*z4O$srXnXcSJf1c|&B zlNJ@DQY#Nr$36@H09HW$nsjCl(0c+q|IW}g%GsS^_-YhgMq(ORTDA%Q{{oK>aPT`~ zcSKQw5k-W86gVwQ4FDXa)Kq0j3AB1TI@*bWFromAsdpTzuc|(JxZS7fUOgnKGNJuM zHi2p&%ZUM+8fvXPO-sbsGB=SRxiqa{sIDo17|fcA6kWRD_S+uLuKUBnC;#*1XSdK+ zy#;mDp6o2pXKG%LDcn*lw%B}sT=PdC-y#k!M{P~5i4k(5T{>7D$eKK1>YQP2pG=FLGe|mY|l%M_iy{f*0i-r%(kCy!H?TviFoFZFb%d^v;n6Y>lNg6h{kZ)-Z zXtHVmfT09}eidUlwtVyEJ*B;7owFHjSD`eVxCEGB{o3X8Mh z{Ya1ai`3>Z+3aZ>UUpz-|LDIiQYStq+h4403oWWC~siW zk)3sG)_P;Ixylt!Py5~eP};bO8OR%`Rm_$M0d(&fZ$k^qGk_4oAZqeXcU)WyW~pfs zMzHzzF5h}2F#PAQ{_Z!EZW*BJjNOKj4u~KO3`1C|l8Ee7yi;o3;=v-wRldd@JnsN% zQs4zRzUw14rSESyA6&U)u4C6*@2uH>r0V@`M|`E71sWVj0Qb+eBcbw*N&(q(^8k3p zE~Ql5vf!mRm%cIYrm+d_1{Z~?*`uQ{!*XYFagp1%pINx@g6xy3H|z*SCMCtR|Kl%_4ptamf*<*RK1=JA2NEkX|ur{!4EzedCcIjE!$MI5&gA+E8QBGr34W z8ev2UI0oHRw&1@9L=7A01Vhs!Q}4ZJwp&=e`b(Jj%jq*FiMGZeV@4N=eGm{calpKp zvy)-<-}lr*>bOax?@EhmA2ep9;>yn(lsL_7U%tMs@VNQlpF-n{tFRj#6+&&hQess^zc zMVX(hd3(WQa>m}uO}0L_?CC3?q8IO-q(+AmrA$bCY6Y4bwyjg`2dj#g|MOdho^N&)rs;R1|2b z_PB@49iNy{kg@6Qw--F7T+Z^UtjInu?K5h8&Z(uVp1AMt5glrsduYWI;~(hX_^UrZ zGiI+F+G>-_=DjpHO$tgnZ}ec>S=_iV_uhMNW@ctMK^g!cgn&>!uK$q1gY#oqVgkm* zq>hZ-(#g}NO&Xe=E)bKr`lRh9Al$cb^%o|=YWlBXwd2x0$n8eX1BQ-sNBh=k* zK>4&8Q%ZBw9mF&tG9|rFT9Sj9R7^?ln;vf?IJzJ+qacPOgyQh!F;IddbBeQyvm(GG zh$V#BV*3p$pFVBcsN#$m-ZV`yu_!&gAkI!qgE}*EOQ+44T9TbA5MuWD4S>CoaHjR@ z>S~5z3JVLnE5Rv3NK;9Ek#k_yfITa5Jyh8v72_DsW`^`H-Gfgoc3$pK|zv( zYK9RRlPW2=;l$zWk|~1*PcF!b%F2!v9LZ^YQqmlVaPHL9xcJPxA{VlIj_nij#||Df zb9hcnW`xZkMu?5e88sj=la=aQ&4}EJnNxmPk{;#u2DrwPhqH&CEigOFjctN0py&3@ z&Ko{JH2sD%ZAhlzQcu^^N0mKr=N)$pa|Z+6H0@wnwymwLy1F_oEiErEuY2l|5F(0> zPd;gW{PDF_Ri|vWXif9GTq#dIHQw!3cI-IAaW;emNvijHg&{)dv?X#2j|VqkF~k|DtG!ymS^2*X$kI>v_XAcqS91zw8=4O_Ux&5-jp4m zl`Ocm#)D@~R~!{oQFK~Ha$-#CuD^Uga@pFZ7PDfAEmO*3QT zY&$=d<}G?{Px@<)+uP&~F@sM&y5#vsuNX3NK@njIOS|JdRaR98j9I(DVWKOl2|y?# zI7A!hvaDQhY*AN6-whi!96WgNuDkB4sHg~qLS0)VD5WgR`g}e`QMxe`Y&Y1dQ&ENzcnT7$pLtqV6mQ@`v0uwQU-EQYu)6@~7 zB&5qaw(|_tWyxSg2ZyMk1VhN-5Cj_slmZQ@s-~(2jf{+Vchx(q)@|&Ynkw=pHr4w2 z`f<0-m~+>x5T!gJIzp7_hN6T_ia3YJGiFGVbzl%NC?^OG!G^;crK-_A(~fXe9zJ|{ z*|KFf-+c4bsZ+bB9u32YiHTXXXyXend?1R6lH_%}lOKI_;!l5?dEmgAg$v)?v!{V! zklUU7$RoEt@IYcrOy}xIXDyxk!vMM>%c^QJyyI&vFKdJq?7}(S5k)l_n_xd{I#pIA zgL69AYgA>ux_vAxx&=ZA$8l9vRqNKRyL`4!gc$aMR)-jd?P`bgqV#wddaN*=fA58*i9zHzg_19-U^w7%M zT7AKS$@AwYI-OEDyIvO;{9you8H^d`S}czlY<6~qa8Fso%u~IEV>UX z`MAZuMpw%9?kbSuP)oB?S}wk5{OGT5+TJHL-`M|O_NB!`@tkQCq$0J{ZFVYFZm%PxS02dylaCZFxF&C+g zU3xaUc#aWPfWP28#4y2NaN@*?GiJ;<|6Em7C0XWew#(cky6AI{-qm{n7$Z%ix7;Gv z*0vowR5Wec?F$wpIvi3cB($~pOG;v1cwyYZgU0OHNun4~l*_I+T+Ys4Yk7c6O@HBe z7tbUuEP4`_J7RIIgk2Du&1SdTyU`s10J|Lm0Wxf4@~E*Hs;UJ7W@Mz;GxgjFA$Gf+ zW!bOwNV?r`3&U~9j4{UeJMrz{nmx;J zQjdW^aP;Wp{QPkT4isf)CyHWFmKlaIIga&sydy^@j~$Ere!rq%jJv&sbQj;D=WTIg zV!0z0*F^YLy5H}wsi|?fT;Iksln`ndlx2JL1TGwS)zQ&$rSL|CQ26+_;kSjc*?8=eUy%b6*&-03+a2$8(V>-X4Gj*5 zqsPvDEyA9q-|rU$;j-UeXFXC%!-e$HvHa@xv9RcNEO*4>nm`C`*|H@nDvDv48`&2L zLn-Ar&gb)~s(SwMaFlpWP0iZ1Yg<}cEL-((D5ZiRG&D367Z;y@+^WYgSF(?V#kYaw zj#zvR;jsULf`W%1e%OAgto4lwLP%BBqM{<`G*K72uBWA?6&4m+OCvoWgwUWtgRFYI z5?8U0g+=#bxg!=|1476&&D`AF{QUgy`d)e*gb?#Q(FlyOX_|$Fg#!i*_-=2^;$On5 z$E$OZeJm`#4bE;5xRKp|i)(=pa$~MnUmb+drBlXm(7<sUaFi7rZe$$CSqpI%78Vv378d_D!tO|T!#~^9)O6_3Aw^Lvcf{iR!DXkp;pT0v z0$NyDSo9pi5)!Sgtvt`O2M!#ltE;otBrU!lAcPFV=<+|Hv$+R@5(OIZ?I@;LSXfwC gd^5t?krYMwf8-(D4yfqD0RR9107*qoM6N<$f+t&nWdHyG literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.lcn/doc/ir.png b/bundles/org.openhab.binding.lcn/doc/ir.png new file mode 100644 index 0000000000000000000000000000000000000000..c287edb0eee96f4fb5902f7a3e393a8dc2ff0501 GIT binary patch literal 110156 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 #0R0L8;j;mzP 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.lcn/pom.xml b/bundles/org.openhab.binding.lcn/pom.xml new file mode 100644 index 0000000000000..eccef96b93fe3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.6-SNAPSHOT + + + org.openhab.binding.lcn + + openHAB Add-ons :: Bundles :: LCN Binding + + diff --git a/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml b/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml new file mode 100644 index 0000000000000..4b803e077eff1 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.lcn/${project.version} + + diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java new file mode 100644 index 0000000000000..5c7feb50d2c78 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java @@ -0,0 +1,119 @@ +/** + * 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.lcn.internal; + +import java.math.BigDecimal; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.profiles.ProfileCallback; +import org.eclipse.smarthome.core.thing.profiles.ProfileContext; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID; +import org.eclipse.smarthome.core.thing.profiles.StateProfile; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A profile to control multiple dimmer outputs simultaneously with ramp. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class DimmerOutputProfile implements StateProfile { + private final Logger logger = LoggerFactory.getLogger(DimmerOutputProfile.class); + /** The Profile's UID */ + static final ProfileTypeUID UID = new ProfileTypeUID(LcnBindingConstants.BINDING_ID, "output"); + private final ProfileCallback callback; + private int rampMs; + private boolean controlAllOutputs; + private boolean controlOutputs12; + + public DimmerOutputProfile(ProfileCallback callback, ProfileContext profileContext) { + this.callback = callback; + + Optional ramp = getConfig(profileContext, "ramp"); + Optional allOutputs = getConfig(profileContext, "controlAllOutputs"); + Optional outputs12 = getConfig(profileContext, "controlOutputs12"); + + ramp.ifPresent(b -> { + if (b instanceof BigDecimal) { + rampMs = (int) (((BigDecimal) b).doubleValue() * 1000); + } else { + logger.warn("Could not parse 'ramp', unexpected type, should be float: {}", ramp); + } + }); + + allOutputs.ifPresent(b -> { + if (b instanceof Boolean) { + controlAllOutputs = true; + } else { + logger.warn("Could not parse 'controlAllOutputs', unexpected type, should be true/false: {}", b); + } + }); + + outputs12.ifPresent(b -> { + if (b instanceof Boolean) { + controlOutputs12 = true; + } else { + logger.warn("Could not parse 'controlOutputs12', unexpected type, should be true/false: {}", b); + } + }); + } + + private Optional getConfig(ProfileContext profileContext, String key) { + return Optional.ofNullable(profileContext.getConfiguration().get(key)); + } + + @Override + public void onCommandFromItem(Command command) { + if (rampMs != 0 && rampMs != LcnDefs.FIXED_RAMP_MS && controlOutputs12) { + logger.warn("Unsupported 'ramp' setting. Will be forced to 250ms: {}", rampMs); + } + BigDecimal value; + if (command instanceof DecimalType) { + value = ((DecimalType) command).toBigDecimal(); + } else if (command instanceof OnOffType) { + value = ((OnOffType) command) == OnOffType.ON ? BigDecimal.valueOf(100) : BigDecimal.ZERO; + } else { + logger.warn("Unsupported type: {}", command.toFullString()); + return; + } + callback.handleCommand(new DimmerOutputCommand(value, controlAllOutputs, controlOutputs12, rampMs)); + } + + @Override + public void onStateUpdateFromHandler(State state) { + callback.sendUpdate(state); + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return UID; + } + + @Override + public void onCommandFromHandler(Command command) { + // nothing + } + + @Override + public void onStateUpdateFromItem(State state) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java new file mode 100644 index 0000000000000..a6f2871e44321 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java @@ -0,0 +1,31 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ILcnModuleActions} defines the interface for all thing actions supported by the binding. + * These methods, parameters, and return types are explained in {@link LcnModuleActions}. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public interface ILcnModuleActions { + void hitKey(@Nullable String table, int key, @Nullable String action); + + void flickerOutput(int output, int depth, int ramp, int count); + + void sendDynamicText(int row, @Nullable String textInput); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java new file mode 100644 index 0000000000000..39c7d2b4ba62a --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java @@ -0,0 +1,43 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link LcnBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnBindingConstants { + /** The scope name of this binding */ + public static final String BINDING_ID = "lcn"; + /** + * Firmware version of the measurement processing since 2013. It has more variables and thresholds and event-based + * variable updates. + */ + public static final int FIRMWARE_2013 = 0x170206; + /** Firmware version which supports controlling all 4 outputs simultaneously */ + public static final int FIRMWARE_2014 = 0x180501; + /** List of all Thing Type UIDs */ + public static final ThingTypeUID THING_TYPE_PCK_GATEWAY = new ThingTypeUID(BINDING_ID, "pckGateway"); + public static final ThingTypeUID THING_TYPE_MODULE = new ThingTypeUID(BINDING_ID, "module"); + public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group"); + /** Regex for address in PCK protocol */ + public static final String ADDRESS_REGEX = "[:=%]M(?\\d{3})(?\\d{3})"; + /** LCN coding for ACK */ + public static final int CODE_ACK = -1; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java new file mode 100644 index 0000000000000..6c3713cbcfe21 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java @@ -0,0 +1,26 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LcnChannelVariableConfiguration} class contains configuration field mapping for Channels of type + * 'variable'. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnChannelVariableConfiguration { + public String unit = "native"; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java new file mode 100644 index 0000000000000..165966cdc85a0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java @@ -0,0 +1,25 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LcnModuleConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnGroupConfiguration extends LcnModuleConfiguration { + public int groupId; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java new file mode 100644 index 0000000000000..5ce3f6bdfeac3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java @@ -0,0 +1,55 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnAddrGrp; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * The {@link LcnGroupHandler} is responsible for handling commands, which are + * addressed to an LCN group. + * + * The module in the field moduleAddress is used for state updates of the group as representative for all modules in + * the group. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnGroupHandler extends LcnModuleHandler { + private @Nullable LcnAddrGrp groupAddress; + + public LcnGroupHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + LcnGroupConfiguration localConfig = getConfigAs(LcnGroupConfiguration.class); + groupAddress = new LcnAddrGrp(localConfig.segmentId, localConfig.groupId); + + super.initialize(); + } + + @Override + protected LcnAddr getCommandAddress() throws LcnException { + LcnAddrGrp localAddress = groupAddress; + if (localAddress == null) { + throw new LcnException("LCN group address not set"); + } + return localAddress; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java new file mode 100644 index 0000000000000..b66e61f9d7ddd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * 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.lcn.internal; + +import static org.openhab.binding.lcn.internal.LcnBindingConstants.*; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link LcnHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.lcn", service = ThingHandlerFactory.class) +public class LcnHandlerFactory extends BaseThingHandlerFactory { + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( + Stream.of(THING_TYPE_PCK_GATEWAY, THING_TYPE_MODULE, THING_TYPE_GROUP).collect(Collectors.toSet())); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_GROUP.equals(thingTypeUID)) { + return new LcnGroupHandler(thing); + } + + if (THING_TYPE_MODULE.equals(thingTypeUID)) { + return new LcnModuleHandler(thing); + } + + if (THING_TYPE_PCK_GATEWAY.equals(thingTypeUID)) { + return new PckGatewayHandler((Bridge) thing); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java new file mode 100644 index 0000000000000..2dd7524a44f8f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java @@ -0,0 +1,202 @@ +/** + * 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.lcn.internal; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.KeyTable; +import org.openhab.binding.lcn.internal.common.LcnDefs.SendKeyCommand; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles actions requested to be sent to an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@ThingActionsScope(name = "lcn") +@NonNullByDefault +public class LcnModuleActions implements ThingActions, ILcnModuleActions { + private final Logger logger = LoggerFactory.getLogger(LcnModuleActions.class); + private static final int DYN_TEXT_CHUNK_COUNT = 5; + private static final int DYN_TEXT_HEADER_LENGTH = 6; + private static final int DYN_TEXT_CHUNK_LENGTH = 12; + private @Nullable LcnModuleHandler moduleHandler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + this.moduleHandler = (LcnModuleHandler) handler; + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return moduleHandler; + } + + @Override + @RuleAction(label = "LCN Hit Key", description = "Sends a \"hit key\" command to an LCN module") + public void hitKey( + @ActionInput(name = "table", required = true, type = "java.lang.String", label = "Table", description = "The key table (A-D)") @Nullable String table, + @ActionInput(name = "key", required = true, type = "java.lang.Integer", label = "Key", description = "The key number (1-8)") int key, + @ActionInput(name = "action", required = true, type = "java.lang.String", label = "Action", description = "The action (HIT, MAKE, BREAK)") @Nullable String action) { + try { + if (table == null) { + throw new LcnException("Table is not set"); + } + + if (action == null) { + throw new LcnException("Action is not set"); + } + + KeyTable keyTable; + try { + keyTable = LcnDefs.KeyTable.valueOf(table.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new LcnException("Unknown key table: " + table); + } + + SendKeyCommand sendKeyCommand; + try { + sendKeyCommand = SendKeyCommand.valueOf(action.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new LcnException("Unknown action: " + action); + } + + if (!LcnChannelGroup.KEYLOCKTABLEA.isValidId(key - 1)) { + throw new LcnException("Key number is out of range: " + key); + } + + SendKeyCommand[] cmds = new SendKeyCommand[LcnDefs.KEY_TABLE_COUNT]; + Arrays.fill(cmds, SendKeyCommand.DONTSEND); + boolean[] keys = new boolean[LcnChannelGroup.KEYLOCKTABLEA.getCount()]; + + int keyTableNumber = keyTable.name().charAt(0) - LcnDefs.KeyTable.A.name().charAt(0); + cmds[keyTableNumber] = sendKeyCommand; + keys[key - 1] = true; + + getHandler().sendPck(PckGenerator.sendKeys(cmds, keys)); + } catch (LcnException e) { + logger.warn("Could not execute hit key command: {}", e.getMessage()); + } + } + + @Override + @RuleAction(label = "LCN Flicker Output", description = "Let a dimmer output flicker for a given count of flashes") + public void flickerOutput( + @ActionInput(name = "output", type = "java.lang.Integer", required = true, label = "Output", description = "The output number (1-4)") int output, + @ActionInput(name = "depth", type = "java.lang.Integer", label = "Depth", description = "0=25% 1=50% 2=100%") int depth, + @ActionInput(name = "ramp", type = "java.lang.Integer", label = "Ramp", description = "0=2sec 1=1sec 2=0.5sec") int ramp, + @ActionInput(name = "count", type = "java.lang.Integer", label = "Count", description = "Number of flashes (1-15)") int count) { + try { + getHandler().sendPck(PckGenerator.flickerOutput(output - 1, depth, ramp, count)); + } catch (LcnException e) { + logger.warn("Could not send output flicker command: {}", e.getMessage()); + } + } + + @Override + @RuleAction(label = "LCN Dynamic Text", description = "Send custom text to an LCN-GTxD display") + public void sendDynamicText( + @ActionInput(name = "row", type = "java.lang.Integer", required = true, label = "Row", description = "Display the text on the LCN-GTxD in the given row number (1-4)") int row, + @ActionInput(name = "text", type = "java.lang.String", label = "Text", description = "The text to display (max. 60 chars/bytes)") @Nullable String textInput) { + try { + String text = textInput; + + if (text == null) { + text = new String(); + } + + // convert String to bytes to split the data every 12 bytes, because a unicode character can take more than + // one byte + ByteBuffer bb = ByteBuffer.wrap(text.getBytes(LcnDefs.LCN_ENCODING)); + + if (bb.capacity() > DYN_TEXT_CHUNK_LENGTH * DYN_TEXT_CHUNK_COUNT) { + logger.warn("Dynamic text truncated. Has {} bytes: '{}'", bb.capacity(), text); + } + + bb.limit(Math.min(DYN_TEXT_CHUNK_LENGTH * DYN_TEXT_CHUNK_COUNT, bb.capacity())); + + int part = 0; + while (bb.hasRemaining()) { + byte[] chunk = new byte[DYN_TEXT_CHUNK_LENGTH]; + bb.get(chunk, 0, Math.min(bb.remaining(), DYN_TEXT_CHUNK_LENGTH)); + + ByteBuffer command = ByteBuffer.allocate(DYN_TEXT_HEADER_LENGTH + DYN_TEXT_CHUNK_LENGTH); + command.put(PckGenerator.dynTextHeader(row - 1, part++).getBytes(LcnDefs.LCN_ENCODING)); + command.put(chunk); + + getHandler().sendPck(command.array()); + } + } catch (IllegalArgumentException | LcnException e) { + logger.warn("Could not send dynamic text: {}", e.getMessage()); + } + } + + private static ILcnModuleActions invokeMethodOf(@Nullable ThingActions actions) { + if (actions == null) { + throw new IllegalArgumentException("actions cannot be null"); + } + if (actions.getClass().getName().equals(LcnModuleActions.class.getName())) { + if (actions instanceof LcnModuleActions) { + return (ILcnModuleActions) actions; + } else { + return (ILcnModuleActions) Proxy.newProxyInstance(ILcnModuleActions.class.getClassLoader(), + new Class[] { ILcnModuleActions.class }, (Object proxy, Method method, Object[] args) -> { + Method m = actions.getClass().getDeclaredMethod(method.getName(), + method.getParameterTypes()); + return m.invoke(actions, args); + }); + } + } + throw new IllegalArgumentException("Actions is not an instance of EcobeeActions"); + } + + /** Static alias to support the old DSL rules engine and make the action available there. */ + public static void hitKey(@Nullable ThingActions actions, @Nullable String table, int key, + @Nullable String action) { + invokeMethodOf(actions).hitKey(table, key, action); + } + + /** Static alias to support the old DSL rules engine and make the action available there. */ + public static void flickerOutput(@Nullable ThingActions actions, int output, int depth, int ramp, int count) { + invokeMethodOf(actions).flickerOutput(output, depth, ramp, count); + } + + /** Static alias to support the old DSL rules engine and make the action available there. */ + public static void sendDynamicText(@Nullable ThingActions actions, int row, @Nullable String text) { + invokeMethodOf(actions).sendDynamicText(row, text); + } + + private LcnModuleHandler getHandler() throws LcnException { + LcnModuleHandler localModuleHandler = moduleHandler; + if (localModuleHandler != null) { + return localModuleHandler; + } else { + throw new LcnException("Handler not set"); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java new file mode 100644 index 0000000000000..aa845f77027ab --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java @@ -0,0 +1,26 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LcnModuleConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleConfiguration { + public int segmentId; + public int moduleId; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java new file mode 100644 index 0000000000000..8ab89ce32297b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java @@ -0,0 +1,264 @@ +/** + * 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.lcn.internal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.lcn.internal.common.LcnAddrMod; +import org.openhab.binding.lcn.internal.connection.Connection; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Scans all LCN segments for LCN modules. + * + * Scan approach: + * 1. Send "Leerkomando" to the broadcast address with request for Ack set + * 2. For every received Ack, send the following requests to the module: + * - serial number request (SN) + * - module's name first part request (NM1) + * - module's name second part request (NM2) + * 3. When all three messages have been received, fire thingDiscovered() + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class LcnModuleDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(LcnModuleDiscoveryService.class); + private static final Pattern NAME_PATTERN = Pattern + .compile("=M(?\\d{3})(?\\d{3}).N(?[1-2]{1})(?.*)"); + private static final String SEGMENT_ID = "segmentId"; + private static final String MODULE_ID = "moduleId"; + private static final String SERIAL_NUMBER = "serialNumber"; + private static final int MODULE_NAME_PART_COUNT = 2; + private static final int DISCOVERY_TIMEOUT_SEC = 90; + private static final int ACK_TIMEOUT_MS = 1000; + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_MODULE).collect(Collectors.toSet())); + private @Nullable PckGatewayHandler bridgeHandler; + private final Map> moduleNames = new HashMap<>(); + private final Map discoveryResultBuilders = new ConcurrentHashMap<>(); + private final List successfullyDiscovered = new LinkedList<>(); + private final Queue<@Nullable LcnAddrMod> serialNumberRequestQueue = new ConcurrentLinkedQueue<>(); + private final Queue<@Nullable LcnAddrMod> moduleNameRequestQueue = new ConcurrentLinkedQueue<>(); + private @Nullable volatile ScheduledFuture queueProcessor; + private @Nullable ScheduledFuture builderTask; + + public LcnModuleDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC, false); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof PckGatewayHandler) { + this.bridgeHandler = (PckGatewayHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + public void deactivate() { + stopScan(); + super.deactivate(); + } + + @Override + protected void startScan() { + synchronized (this) { + PckGatewayHandler localBridgeHandler = bridgeHandler; + if (localBridgeHandler == null) { + logger.warn("Bridge handler not set"); + return; + } + + ScheduledFuture localBuilderTask = builderTask; + if (localBridgeHandler.getConnection() == null && localBuilderTask != null) { + localBuilderTask.cancel(true); + } + + localBridgeHandler.registerPckListener(data -> { + Matcher matcher; + + if ((matcher = LcnModuleMetaAckSubHandler.PATTERN_POS.matcher(data)).matches() + || (matcher = LcnModuleMetaFirmwareSubHandler.PATTERN.matcher(data)).matches() + || (matcher = NAME_PATTERN.matcher(data)).matches()) { + synchronized (LcnModuleDiscoveryService.this) { + Connection connection = localBridgeHandler.getConnection(); + + if (connection == null) { + return; + } + + LcnAddrMod addr = new LcnAddrMod( + localBridgeHandler.toLogicalSegmentId(Integer.parseInt(matcher.group("segId"))), + Integer.parseInt(matcher.group("modId"))); + + if (matcher.pattern() == LcnModuleMetaAckSubHandler.PATTERN_POS) { + // Received an ACK frame + + // The module could send an Ack with a response to another command. So, ignore the Ack, when + // we received our data already. + if (!discoveryResultBuilders.containsKey(addr)) { + serialNumberRequestQueue.add(addr); + rescheduleQueueProcessor(); // delay request of serial until all modules finished ACKing + } + + Map localNameParts = moduleNames.get(addr); + if (localNameParts == null || localNameParts.size() != MODULE_NAME_PART_COUNT) { + moduleNameRequestQueue.add(addr); + rescheduleQueueProcessor(); // delay request of names until all modules finished ACKing + } + } else if (matcher.pattern() == LcnModuleMetaFirmwareSubHandler.PATTERN) { + // Received a firmware version info frame + + ThingUID bridgeUid = localBridgeHandler.getThing().getUID(); + String serialNumber = matcher.group("sn"); + ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_MODULE, bridgeUid, + serialNumber); + + Map properties = new HashMap<>(3); + properties.put(SEGMENT_ID, addr.getSegmentId()); + properties.put(MODULE_ID, addr.getModuleId()); + properties.put(SERIAL_NUMBER, serialNumber); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties).withRepresentationProperty(SERIAL_NUMBER) + .withBridge(bridgeUid); + + discoveryResultBuilders.put(addr, discoveryResult); + } else if (matcher.pattern() == NAME_PATTERN) { + // Received part of a module's name frame + + final int part = Integer.parseInt(matcher.group("part")) - 1; + final String name = matcher.group("name"); + + moduleNames.compute(addr, (partNumber, namePart) -> { + Map namePartMapping = namePart; + if (namePartMapping == null) { + namePartMapping = new HashMap<>(); + } + + namePartMapping.put(part, name); + + return namePartMapping; + }); + } + } + } + }); + + builderTask = scheduler.scheduleWithFixedDelay(() -> { + synchronized (LcnModuleDiscoveryService.this) { + discoveryResultBuilders.entrySet().stream().filter(e -> { + Map localNameParts = moduleNames.get(e.getKey()); + return localNameParts != null && localNameParts.size() == MODULE_NAME_PART_COUNT; + }).filter(e -> !successfullyDiscovered.contains(e.getKey())).forEach(e -> { + StringBuilder thingName = new StringBuilder(); + if (e.getKey().getSegmentId() != 0) { + thingName.append("Segment " + e.getKey().getSegmentId() + " "); + } + + thingName.append("Module " + e.getKey().getModuleId() + ": "); + Map localNameParts = moduleNames.get(e.getKey()); + if (localNameParts != null) { + thingName.append(localNameParts.get(0)); + thingName.append(localNameParts.get(1)); + + thingDiscovered(e.getValue().withLabel(thingName.toString()).build()); + successfullyDiscovered.add(e.getKey()); + } + }); + } + }, 500, 500, TimeUnit.MILLISECONDS); + + localBridgeHandler.sendModuleDiscoveryCommand(); + } + } + + private synchronized void rescheduleQueueProcessor() { + // delay serial number and module name requests to not clog the bus + ScheduledFuture localQueueProcessor = queueProcessor; + if (localQueueProcessor != null) { + localQueueProcessor.cancel(true); + } + queueProcessor = scheduler.scheduleWithFixedDelay(() -> { + PckGatewayHandler localBridgeHandler = bridgeHandler; + if (localBridgeHandler != null) { + LcnAddrMod serial = serialNumberRequestQueue.poll(); + if (serial != null) { + localBridgeHandler.sendSerialNumberRequest(serial); + } + + LcnAddrMod name = moduleNameRequestQueue.poll(); + if (name != null) { + localBridgeHandler.sendModuleNameRequest(name); + } + + // stop scan when all LCN modules have been requested + if (serial == null && name == null) { + scheduler.schedule(this::stopScan, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + } + }, ACK_TIMEOUT_MS, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + @Override + public synchronized void stopScan() { + ScheduledFuture localBuilderTask = builderTask; + if (localBuilderTask != null) { + localBuilderTask.cancel(true); + } + ScheduledFuture localQueueProcessor = queueProcessor; + if (localQueueProcessor != null) { + localQueueProcessor.cancel(true); + } + PckGatewayHandler localBridgeHandler = bridgeHandler; + if (localBridgeHandler != null) { + localBridgeHandler.removeAllPckListeners(); + } + successfullyDiscovered.clear(); + moduleNames.clear(); + + super.stopScan(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java new file mode 100644 index 0000000000000..32ea98c7e35b3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java @@ -0,0 +1,363 @@ +/** + * 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.lcn.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnAddrMod; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.connection.Connection; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.openhab.binding.lcn.internal.converter.Converter; +import org.openhab.binding.lcn.internal.converter.Converters; +import org.openhab.binding.lcn.internal.converter.S0Converter; +import org.openhab.binding.lcn.internal.subhandler.AbstractLcnModuleSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LcnModuleHandler} is responsible for handling commands, which are + * sent to or received from one of the channels. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleHandler.class); + private static final Map CONVERTERS = new HashMap<>(); + private @Nullable LcnAddrMod moduleAddress; + private final Map subHandlers = new HashMap<>(); + private final List metadataSubHandlers = new ArrayList<>(); + private final Map converters = new HashMap<>(); + + static { + CONVERTERS.put("temperature", Converters.TEMPERATURE); + CONVERTERS.put("light", Converters.LIGHT); + CONVERTERS.put("co2", Converters.CO2); + CONVERTERS.put("current", Converters.CURRENT); + CONVERTERS.put("voltage", Converters.VOLTAGE); + CONVERTERS.put("angle", Converters.ANGLE); + CONVERTERS.put("windspeed", Converters.WINDSPEED); + } + + public LcnModuleHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + LcnModuleConfiguration localConfig = getConfigAs(LcnModuleConfiguration.class); + LcnAddrMod localModuleAddress = moduleAddress = new LcnAddrMod(localConfig.segmentId, localConfig.moduleId); + + try { + // create sub handlers + ModInfo info = getPckGatewayHandler().getModInfo(localModuleAddress); + for (LcnChannelGroup type : LcnChannelGroup.values()) { + subHandlers.put(type, type.createSubHandler(this, info)); + } + + // meta sub handlers, which are not assigned to a channel group + metadataSubHandlers.add(new LcnModuleMetaAckSubHandler(this, info)); + metadataSubHandlers.add(new LcnModuleMetaFirmwareSubHandler(this, info)); + + // initialize variable value converters + for (Channel channel : thing.getChannels()) { + Object unitObject = channel.getConfiguration().get("unit"); + Object parameterObject = channel.getConfiguration().get("parameter"); + + if (unitObject instanceof String) { + switch ((String) unitObject) { + case "power": + case "energy": + converters.put(channel.getUID(), new S0Converter(parameterObject)); + break; + default: + if (CONVERTERS.containsKey(unitObject)) { + converters.put(channel.getUID(), CONVERTERS.get(unitObject)); + } + break; + } + } + } + + // module is assumed as online, when the corresponding Bridge (PckGatewayHandler) is online. + updateStatus(ThingStatus.ONLINE); + } catch (LcnException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + @Override + public void handleCommand(ChannelUID channelUid, Command command) { + try { + String groupId = channelUid.getGroupId(); + + if (!channelUid.isInGroup()) { + return; + } + + if (groupId == null) { + throw new LcnException("Group ID is null"); + } + + LcnChannelGroup channelGroup = LcnChannelGroup.valueOf(groupId.toUpperCase()); + AbstractLcnModuleSubHandler subHandler = subHandlers.get(channelGroup); + + if (subHandler == null) { + throw new LcnException("Sub Handler not found for: " + channelGroup); + } + + Optional number = channelUidToChannelNumber(channelUid, channelGroup); + + if (command instanceof RefreshType) { + number.ifPresent(n -> subHandler.handleRefresh(channelGroup, n)); + subHandler.handleRefresh(channelUid.getIdWithoutGroup()); + } else if (command instanceof OnOffType) { + subHandler.handleCommandOnOff((OnOffType) command, channelGroup, number.get()); + } else if (command instanceof DimmerOutputCommand) { + subHandler.handleCommandDimmerOutput((DimmerOutputCommand) command, number.get()); + } else if (command instanceof PercentType && number.isPresent()) { + subHandler.handleCommandPercent((PercentType) command, channelGroup, number.get()); + } else if (command instanceof HSBType) { + subHandler.handleCommandHsb((HSBType) command, channelUid.getIdWithoutGroup()); + } else if (command instanceof PercentType) { + subHandler.handleCommandPercent((PercentType) command, channelGroup, channelUid.getIdWithoutGroup()); + } else if (command instanceof StringType) { + subHandler.handleCommandString((StringType) command, number.get()); + } else if (command instanceof DecimalType) { + DecimalType decimalType = (DecimalType) command; + DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(decimalType.doubleValue()); + subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get()); + } else if (command instanceof QuantityType) { + QuantityType quantityType = (QuantityType) command; + DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(quantityType); + subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get()); + } else if (command instanceof UpDownType) { + subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get()); + } else if (command instanceof StopMoveType) { + subHandler.handleCommandStopMove((StopMoveType) command, channelGroup, number.get()); + } else { + throw new LcnException("Unsupported command type"); + } + } catch (IllegalArgumentException | NoSuchElementException | LcnException e) { + logger.warn("{}: Failed to handle command {}: {}", channelUid, command.getClass().getSimpleName(), + e.getMessage()); + } + } + + @NonNullByDefault({}) // getOrDefault() + private Converter getConverter(ChannelUID channelUid) { + return converters.getOrDefault(channelUid, Converters.IDENTITY); + } + + /** + * Invoked when a PCK messages arrives from the PCK gateway + * + * @param pck the message without line termination + */ + @SuppressWarnings("null") + public void handleStatusMessage(String pck) { + for (AbstractLcnModuleSubHandler handler : subHandlers.values()) { + if (handler.tryParse(pck)) { + break; + } + } + + metadataSubHandlers.forEach(h -> h.tryParse(pck)); + } + + private Optional channelUidToChannelNumber(ChannelUID channelUid, LcnChannelGroup channelGroup) + throws LcnException { + try { + int number = Integer.parseInt(channelUid.getIdWithoutGroup()) - 1; + + if (!channelGroup.isValidId(number)) { + throw new LcnException("Out of range: " + number); + } + return Optional.of(number); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + private PckGatewayHandler getPckGatewayHandler() throws LcnException { + Bridge bridge = getBridge(); + if (bridge == null) { + throw new LcnException("No LCN-PCK gateway configured for this module"); + } + + PckGatewayHandler handler = (PckGatewayHandler) bridge.getHandler(); + if (handler == null) { + throw new LcnException("Could not get PckGatewayHandler"); + } + return handler; + } + + /** + * Queues a PCK string for sending. + * + * @param command without the address part + * @throws LcnException when the module address is unknown + */ + public void sendPck(String command) throws LcnException { + getPckGatewayHandler().queue(getCommandAddress(), true, command); + } + + /** + * Queues a PCK byte buffer for sending. + * + * @param command without the address part + * @throws LcnException when the module address is unknown + */ + public void sendPck(byte[] command) throws LcnException { + getPckGatewayHandler().queue(getCommandAddress(), true, command); + } + + /** + * Gets the address, which shall be used when sending commands into the LCN bus. This can also be a group address. + * + * @return the address to send to + * @throws LcnException when the address is unknown + */ + protected LcnAddr getCommandAddress() throws LcnException, LcnException { + LcnAddr localAddress = moduleAddress; + if (localAddress == null) { + throw new LcnException("Module address not set"); + } + return localAddress; + } + + /** + * Invoked when an update for this LCN module should be fired to openHAB. + * + * @param channelGroup the Channel to update + * @param channelId the ID within the Channel to update + * @param state the new state + */ + public void updateChannel(LcnChannelGroup channelGroup, String channelId, State state) { + ChannelUID channelUid = createChannelUid(channelGroup, channelId); + Converter converter = converters.get(channelUid); + + State convertedState = state; + if (converter != null) { + convertedState = converter.onStateUpdateFromHandler(state); + } + updateState(channelUid, convertedState); + } + + /** + * Invoked when an trigger for this LCN module should be fired to openHAB. + * + * @param channelGroup the Channel to update + * @param channelId the ID within the Channel to update + * @param event the event used to trigger + */ + public void triggerChannel(LcnChannelGroup channelGroup, String channelId, String event) { + triggerChannel(createChannelUid(channelGroup, channelId), event); + } + + private ChannelUID createChannelUid(LcnChannelGroup channelGroup, String channelId) { + return new ChannelUID(thing.getUID(), channelGroup.name().toLowerCase() + "#" + channelId); + } + + /** + * Checks the LCN module address against the own. + * + * @param physicalSegmentId which is 0 if it is the local segment + * @param moduleId + * @return true, if the given address matches the own address + */ + public boolean isMyAddress(String physicalSegmentId, String moduleId) { + try { + return new LcnAddrMod(getPckGatewayHandler().toLogicalSegmentId(Integer.parseInt(physicalSegmentId)), + Integer.parseInt(moduleId)).equals(getStatusMessageAddress()); + } catch (LcnException e) { + return false; + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(LcnModuleActions.class); + } + + /** + * Invoked when an Ack from this module has been received. + */ + public void onAckRceived() { + try { + Connection connection = getPckGatewayHandler().getConnection(); + LcnAddrMod localModuleAddress = moduleAddress; + if (connection != null && localModuleAddress != null) { + getPckGatewayHandler().getModInfo(localModuleAddress).onAck(LcnBindingConstants.CODE_ACK, connection, + getPckGatewayHandler().getTimeoutMs(), System.nanoTime()); + } + } catch (LcnException e) { + logger.warn("Connection or module address not set"); + } + } + + /** + * Gets the address the handler shall react to, when a status message from this address is processed. + * + * @return the address for status messages + */ + public LcnAddrMod getStatusMessageAddress() { + LcnAddrMod localmoduleAddress = moduleAddress; + if (localmoduleAddress != null) { + return localmoduleAddress; + } else { + return new LcnAddrMod(0, 0); + } + } + + @Override + public void dispose() { + metadataSubHandlers.clear(); + subHandlers.clear(); + converters.clear(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java new file mode 100644 index 0000000000000..a63c73e0c74cf --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java @@ -0,0 +1,71 @@ +/** + * 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.lcn.internal; + +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.CoreItemFactory; +import org.eclipse.smarthome.core.thing.profiles.Profile; +import org.eclipse.smarthome.core.thing.profiles.ProfileCallback; +import org.eclipse.smarthome.core.thing.profiles.ProfileContext; +import org.eclipse.smarthome.core.thing.profiles.ProfileFactory; +import org.eclipse.smarthome.core.thing.profiles.ProfileType; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeBuilder; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory to create Profile instances. Also provides the available ProfileTypes and gives advise which profile to use + * by a given link. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +@Component(service = { ProfileFactory.class, ProfileTypeProvider.class }) +public class LcnProfileFactory implements ProfileFactory, ProfileTypeProvider { + private final Logger logger = LoggerFactory.getLogger(LcnProfileFactory.class); + + @Override + public Collection getSupportedProfileTypeUIDs() { + return Collections.singleton(DimmerOutputProfile.UID); + } + + @Override + public Collection getProfileTypes(@Nullable Locale locale) { + return Collections.singleton(ProfileTypeBuilder.newState(DimmerOutputProfile.UID, "Dimmer Output (%)") + .withSupportedItemTypes(CoreItemFactory.DIMMER, CoreItemFactory.COLOR) + .withSupportedChannelTypeUIDs( + new ChannelTypeUID(LcnBindingConstants.BINDING_ID, LcnChannelGroup.OUTPUT.name().toLowerCase())) + .build()); + } + + @Override + public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, + ProfileContext profileContext) { + if (profileTypeUID.equals(DimmerOutputProfile.UID)) { + return new DimmerOutputProfile(callback, profileContext); + } else { + logger.warn("Could not create {}", profileTypeUID); + return null; + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java new file mode 100644 index 0000000000000..593396a463c2a --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java @@ -0,0 +1,54 @@ +/** + * 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.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PckGatewayConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class PckGatewayConfiguration { + private @NonNullByDefault({}) String hostname; + private int port; + private @NonNullByDefault({}) String username; + private @NonNullByDefault({}) String password; + private @NonNullByDefault({}) String mode; + private @NonNullByDefault({}) int timeoutMs; + + public String getHostname() { + return hostname; + } + + public int getPort() { + return port; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getMode() { + return mode; + } + + public int getTimeoutMs() { + return timeoutMs; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java new file mode 100644 index 0000000000000..9d239c5936a02 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java @@ -0,0 +1,303 @@ +/** + * 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.lcn.internal; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnAddrMod; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.OutputPortDimMode; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.connection.Connection; +import org.openhab.binding.lcn.internal.connection.ConnectionCallback; +import org.openhab.binding.lcn.internal.connection.ConnectionSettings; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PckGatewayHandler} is responsible for the communication via a PCK gateway. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class PckGatewayHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(PckGatewayHandler.class); + private @Nullable Connection connection; + private Optional> pckListener = Optional.empty(); + private @Nullable PckGatewayConfiguration config; + + public PckGatewayHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // nothing + } + + @Override + public synchronized void initialize() { + PckGatewayConfiguration localConfig = config = getConfigAs(PckGatewayConfiguration.class); + + String errorMessage = "Could not connect to LCN-PCHK/PKE: " + localConfig.getHostname() + ": "; + + try { + OutputPortDimMode dimMode; + String mode = localConfig.getMode(); + if (LcnDefs.OutputPortDimMode.NATIVE50.name().equalsIgnoreCase(mode)) { + dimMode = LcnDefs.OutputPortDimMode.NATIVE50; + } else if (LcnDefs.OutputPortDimMode.NATIVE200.name().equalsIgnoreCase(mode)) { + dimMode = LcnDefs.OutputPortDimMode.NATIVE200; + } else { + throw new LcnException("DimMode " + mode + " is not supported"); + } + + ConnectionSettings settings = new ConnectionSettings("0", localConfig.getHostname(), localConfig.getPort(), + localConfig.getUsername(), localConfig.getPassword(), dimMode, LcnDefs.OutputPortStatusMode.PERCENT, + localConfig.getTimeoutMs()); + + connection = new Connection(settings, scheduler, new ConnectionCallback() { + @Override + public void onOnline() { + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void onOffline(@Nullable String errorMessage) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage + "."); + } + + @Override + public void onPckMessageReceived(String message) { + pckListener.ifPresent(l -> l.accept(message)); + getThing().getThings().stream().filter(t -> t.getStatus() == ThingStatus.ONLINE).map(t -> { + LcnModuleHandler handler = (LcnModuleHandler) t.getHandler(); + if (handler == null) { + logger.warn("Failed to process PCK message: Handler not set"); + } + return handler; + }).filter(h -> h != null).forEach(h -> h.handleStatusMessage(message)); + } + }); + + updateStatus(ThingStatus.UNKNOWN); + } catch (LcnException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage + e.getMessage()); + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(LcnModuleDiscoveryService.class); + } + + @Override + public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { + if (childThing.getThingTypeUID().equals(LcnBindingConstants.THING_TYPE_MODULE) + || childThing.getThingTypeUID().equals(LcnBindingConstants.THING_TYPE_GROUP)) { + try { + LcnAddr addr = getLcnAddrFromThing(childThing); + Connection localConnection = connection; + if (localConnection != null) { + localConnection.removeLcnModule(addr); + } + } catch (LcnException e) { + logger.warn("Failed to read configuration: {}", e.getMessage()); + } + } + } + + private LcnAddr getLcnAddrFromThing(Thing childThing) throws LcnException { + LcnModuleHandler lcnModuleHandler = (LcnModuleHandler) childThing.getHandler(); + if (lcnModuleHandler != null) { + return lcnModuleHandler.getCommandAddress(); + } else { + throw new LcnException("Could not get module handler"); + } + } + + /** + * Enqueues a PCK (String) command to be sent to an LCN module. + * + * @param addr the modules address + * @param wantsAck true, if the module shall send an ACK upon successful processing + * @param pck the command to send + */ + public void queue(LcnAddr addr, boolean wantsAck, String pck) { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.queue(addr, wantsAck, pck); + } else { + logger.warn("Dropped PCK command: {}", pck); + } + } + + /** + * Enqueues a PCK (ByteBuffer) command to be sent to an LCN module. + * + * @param addr the modules address + * @param wantsAck true, if the module shall send an ACK upon successful processing + * @param pck the command to send + */ + public void queue(LcnAddr addr, boolean wantsAck, byte[] pck) { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.queue(addr, wantsAck, pck); + } else { + logger.warn("Dropped PCK command of length: {}", pck.length); + } + } + + /** + * Sends a broadcast message to all LCN modules: All LCN modules are requested to answer with an Ack. + */ + void sendModuleDiscoveryCommand() { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.sendModuleDiscoveryCommand(); + } + } + + /** + * Send a request to an LCN module to respond with its serial number and firmware version. + * + * @param addr the module's address + */ + void sendSerialNumberRequest(LcnAddrMod addr) { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.sendSerialNumberRequest(addr); + } + } + + /** + * Send a request to an LCN module to respond with its configured name. + * + * @param addr the module's address + */ + void sendModuleNameRequest(LcnAddrMod addr) { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.sendModuleNameRequest(addr); + } + } + + /** + * Returns the ModInfo to a given module. Will be created if it doesn't exist,yet. + * + * @param addr the module's address + * @return the ModInfo + * @throws LcnException when this handler is not initialized, yet + */ + ModInfo getModInfo(LcnAddrMod addr) throws LcnException { + Connection localConnection = connection; + if (localConnection != null) { + return localConnection.updateModuleData(addr); + } else { + throw new LcnException("Connection is null"); + } + } + + /** + * Registers a listener to receive all PCK messages from this PCK gateway. + * + * @param listener the listener to add + */ + void registerPckListener(Consumer listener) { + this.pckListener = Optional.of(listener); + } + + /** + * Removes all listeners for PCK messages from this PCK gateway. + */ + void removeAllPckListeners() { + this.pckListener = Optional.empty(); + } + + /** + * Gets the Connection for this handler. + * + * @return the Connection + */ + @Nullable + public Connection getConnection() { + return connection; + } + + /** + * Gets the local segment ID. When no segments are used, the value is 0. + * + * @return the local segment ID + */ + public int getLocalSegmentId() { + Connection localConnection = connection; + if (localConnection != null) { + return localConnection.getLocalSegId(); + } else { + return 0; + } + } + + /** + * Translates the given physical segment ID (0 or 4 if local segment) to the logical segment ID (local segment ID). + * + * @param physicalSegmentId the segment ID to convert + * @return the converted segment ID + */ + public int toLogicalSegmentId(int physicalSegmentId) { + int localSegmentId = getLocalSegmentId(); + if ((physicalSegmentId == 0 || physicalSegmentId == 4) && localSegmentId != -1) { + // PCK message came from local segment + // physicalSegmentId == 0 => Module is programmed to send status messages to local segment only + // physicalSegmentId == 4 => Module is programmed to send status messages globally (to all segments) + // or segment coupler scan did not finish, yet (-1). Assume local segment, then. + return localSegmentId; + } else { + return physicalSegmentId; + } + } + + @Override + public void dispose() { + Connection localConnection = connection; + if (localConnection != null) { + localConnection.shutdown(); + } + } + + /** + * Gets the configured connection timeout for the PCK gateway. + * + * @return the timeout in ms + */ + public long getTimeoutMs() { + PckGatewayConfiguration localConfig = config; + return localConfig != null ? localConfig.getTimeoutMs() : 3500; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java new file mode 100644 index 0000000000000..b54dd12367647 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java @@ -0,0 +1,65 @@ +/** + * 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.lcn.internal.common; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.PercentType; + +/** + * Holds the information to control dimmer outputs of an LCN module. Used when the user configured an "output" profile. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class DimmerOutputCommand extends PercentType { + private static final long serialVersionUID = 8147502412107723798L; + private final boolean controlAllOutputs; + private final boolean controlOutputs12; + private final int rampMs; + + public DimmerOutputCommand(BigDecimal value, boolean controlAllOutputs, boolean controlOutputs12, int rampMs) { + super(value); + this.controlAllOutputs = controlAllOutputs; + this.controlOutputs12 = controlOutputs12; + this.rampMs = rampMs; + } + + /** + * Gets the ramp. + * + * @return ramp in milliseconds + */ + public int getRampMs() { + return rampMs; + } + + /** + * Returns if all dimmer outputs shall be controlled. + * + * @return true, if all dimmer outputs shall be controlled + */ + public boolean isControlAllOutputs() { + return controlAllOutputs; + } + + /** + * Returns if dimmer outputs 1+2 shall be controlled. + * + * @return true, if dimmer outputs 1+2 shall be controlled + */ + public boolean isControlOutputs12() { + return controlOutputs12; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java new file mode 100644 index 0000000000000..c5630ecc69d9c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java @@ -0,0 +1,79 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Represents an LCN address (module or group). + * + * @author Tobias Jüttner - Initial Contribution + */ +@NonNullByDefault +public abstract class LcnAddr { + /** + * The logical segment ID. When no segments are used, the ID is always 0. When segments are used and the module is + * in the local segment, the ID is the local's segment ID. + */ + protected final int segmentId; + + /** + * Constructs an address with a (logical) segment id. + * + * @param segId the segment id + */ + public LcnAddr(int segId) { + this.segmentId = segId; + } + + /** + * Gets the (logical) segment id. + * + * @return the segment id + */ + public int getSegmentId() { + return this.segmentId; + } + + /** + * Gets the physical segment id ("local" segment replaced with 0). + * Can be used to send data into the LCN bus. + * + * @param localSegegmentId the segment id of the local segment (managed by {@link Connection}) + * @return the physical segment id + */ + public int getPhysicalSegmentId(int localSegegmentId) { + return this.segmentId == localSegegmentId ? 0 : this.segmentId; + } + + /** + * Checks the address against the LCN specification for valid addresses. + * + * @return true if address is valid + */ + public abstract boolean isValid(); + + /** + * Queries the concrete address type. + * + * @return true if address is a group address (module address otherwise) + */ + public abstract boolean isGroup(); + + /** + * Gets the address' module or group id (discarding the concrete type). + * + * @return the module or group id + */ + public abstract int getId(); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java new file mode 100644 index 0000000000000..0873dfd0edd85 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java @@ -0,0 +1,102 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents an LCN group address. + * Can be used as a key in maps. + * Hash codes are guaranteed to be unique as long as {@link #isValid()} is true. + * + * @author Tobias Jüttner - Initial Contribution + */ +@NonNullByDefault +public class LcnAddrGrp extends LcnAddr implements Comparable { + private final Logger logger = LoggerFactory.getLogger(LcnAddrGrp.class); + private final int groupId; + + /** + * Constructs a group address with (logical) segment id and group id. + * + * @param segId the segment id + * @param grpId the group id + */ + public LcnAddrGrp(int segId, int grpId) { + super(segId); + this.groupId = grpId; + } + + /** + * Gets the group id. + * + * @return the group id + */ + public int getGroupId() { + return this.groupId; + } + + @Override + public boolean isValid() { + // segId: + // 0 = Local, 1..2 = Not allowed (but "seen in the wild") + // 3 = Broadcast, 4 = Status messages, 5..127, 128 = Segment-bus disabled (valid value) + // grpId: + // 3 = Broadcast, 4 = Status messages, 5..254 + return this.segmentId >= 0 && this.segmentId <= 128 && this.groupId >= 3 && this.groupId <= 254; + } + + @Override + public boolean isGroup() { + return true; + } + + @Override + public int getId() { + return this.groupId; + } + + @Override + public int hashCode() { + // Reversing the bits helps to generate better balanced trees as ids tend to be "user-sorted" + try { + if (this.isValid()) { + return ReverseNumber.reverseUInt8(this.groupId) << 8 + ReverseNumber.reverseUInt8(this.segmentId); + } + } catch (LcnException ex) { + logger.warn("Could not calculate hash code"); + } + return -1; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof LcnAddrGrp)) { + return false; + } + return this.segmentId == ((LcnAddrGrp) obj).segmentId && this.groupId == ((LcnAddrGrp) obj).groupId; + } + + @Override + public int compareTo(LcnAddrMod other) { + return this.hashCode() - other.hashCode(); + } + + @Override + public String toString() { + return this.isValid() ? String.format("S%03dG%03d", this.segmentId, this.groupId) : "Invalid"; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java new file mode 100644 index 0000000000000..81fe842230c38 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java @@ -0,0 +1,102 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents an LCN module address. + * Can be used as a key in maps. + * Hash codes are guaranteed to be unique as long as {@link #isValid()} is true. + * + * @author Tobias Jüttner - Initial Contribution + */ +@NonNullByDefault +public class LcnAddrMod extends LcnAddr implements Comparable { + private final Logger logger = LoggerFactory.getLogger(LcnAddrMod.class); + private final int moduleId; + + /** + * Constructs a module address with (logical) segment id and module id. + * + * @param segId the segment id + * @param modId the module id + */ + public LcnAddrMod(int segId, int modId) { + super(segId); + this.moduleId = modId; + } + + /** + * Gets the module id. + * + * @return the module id + */ + public int getModuleId() { + return this.moduleId; + } + + @Override + public boolean isValid() { + // segId: + // 0 = Local, 1..2 = Not allowed (but "seen in the wild") + // 3 = Broadcast, 4 = Status messages, 5..127, 128 = Segment-bus disabled (valid value) + // modId: + // 1 = LCN-PRO, 2 = LCN-GVS/LCN-W, 4 = PCHK, 5..254, 255 = Unprog. (valid, but irrelevant here) + return this.segmentId >= 0 && this.segmentId <= 128 && this.moduleId >= 1 && this.moduleId <= 254; + } + + @Override + public boolean isGroup() { + return false; + } + + @Override + public int getId() { + return this.moduleId; + } + + @Override + public int hashCode() { + // Reversing the bits helps to generate better balanced trees as ids tend to be "user-sorted" + try { + if (this.isValid()) { + return ReverseNumber.reverseUInt8(this.moduleId) << 8 + ReverseNumber.reverseUInt8(this.segmentId); + } + } catch (LcnException ex) { + logger.warn("Could not calculate hash code"); + } + return -1; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof LcnAddrMod)) { + return false; + } + return this.segmentId == ((LcnAddrMod) obj).segmentId && this.moduleId == ((LcnAddrMod) obj).moduleId; + } + + @Override + public int compareTo(LcnAddrMod other) { + return this.hashCode() - other.hashCode(); + } + + @Override + public String toString() { + return this.isValid() ? String.format("S%03dM%03d", this.segmentId, this.moduleId) : "Invalid"; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java new file mode 100644 index 0000000000000..d9d6a6da4f38c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java @@ -0,0 +1,122 @@ +/** + * 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.lcn.internal.common; + +import java.util.function.BiFunction; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.openhab.binding.lcn.internal.subhandler.AbstractLcnModuleSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleBinarySensorSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleCodeSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleKeyLockTableSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleLedSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleLogicSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleOutputSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleRelaySubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleRollershutterOutputSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleRollershutterRelaySubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleRvarLockSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleRvarSetpointSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleS0CounterSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleThresholdSubHandler; +import org.openhab.binding.lcn.internal.subhandler.LcnModuleVariableSubHandler; + +/** + * Defines the supported channels of an LCN module handler. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public enum LcnChannelGroup { + OUTPUT(4, LcnModuleOutputSubHandler::new), + ROLLERSHUTTEROUTPUT(1, LcnModuleRollershutterOutputSubHandler::new), + RELAY(8, LcnModuleRelaySubHandler::new), + ROLLERSHUTTERRELAY(4, LcnModuleRollershutterRelaySubHandler::new), + LED(12, LcnModuleLedSubHandler::new), + LOGIC(4, LcnModuleLogicSubHandler::new), + BINARYSENSOR(8, LcnModuleBinarySensorSubHandler::new), + VARIABLE(12, LcnModuleVariableSubHandler::new), + RVARSETPOINT(2, LcnModuleRvarSetpointSubHandler::new), + RVARLOCK(2, LcnModuleRvarLockSubHandler::new), + THRESHOLDREGISTER1(5, LcnModuleThresholdSubHandler::new), + THRESHOLDREGISTER2(4, LcnModuleThresholdSubHandler::new), + THRESHOLDREGISTER3(4, LcnModuleThresholdSubHandler::new), + THRESHOLDREGISTER4(4, LcnModuleThresholdSubHandler::new), + S0INPUT(4, LcnModuleS0CounterSubHandler::new), + KEYLOCKTABLEA(8, LcnModuleKeyLockTableSubHandler::new), + KEYLOCKTABLEB(8, LcnModuleKeyLockTableSubHandler::new), + KEYLOCKTABLEC(8, LcnModuleKeyLockTableSubHandler::new), + KEYLOCKTABLED(8, LcnModuleKeyLockTableSubHandler::new), + CODE(0, LcnModuleCodeSubHandler::new); + + private int count; + private BiFunction handlerFactory; + + private LcnChannelGroup(int count, + BiFunction handlerFactory) { + this.count = count; + this.handlerFactory = handlerFactory; + } + + /** + * Gets the number of Channels within the channel group. + * + * @return the Channel count + */ + public int getCount() { + return count; + } + + /** + * Checks the given Channel id against the max. Channel count in this Channel group. + * + * @param number the number to check + * @return true, if the number is in the range + */ + public boolean isValidId(int number) { + return number >= 0 && number < count; + } + + /** + * Gets the sub handler class to handle this Channel group. + * + * @return the sub handler class + */ + public AbstractLcnModuleSubHandler createSubHandler(LcnModuleHandler handler, ModInfo info) { + return handlerFactory.apply(handler, info); + } + + /** + * Converts a given table ID into the corresponding Channel group. + * + * @param tableId to convert + * @return the channel group + * @throws LcnException when the ID is out of range + */ + public static LcnChannelGroup fromTableId(int tableId) throws LcnException { + switch (tableId) { + case 0: + return KEYLOCKTABLEA; + case 1: + return KEYLOCKTABLEB; + case 2: + return KEYLOCKTABLEC; + case 3: + return KEYLOCKTABLED; + default: + throw new LcnException("Unknown key table ID: " + tableId); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java new file mode 100644 index 0000000000000..6ad123bc8affe --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java @@ -0,0 +1,156 @@ +/** + * 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.lcn.internal.common; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Common definitions and helpers for the PCK protocol. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public final class LcnDefs { + /** Text encoding used by LCN-PCHK. */ + public static final Charset LCN_ENCODING = StandardCharsets.UTF_8; + /** Number of thresholds registers of an LCN module */ + public static final int THRESHOLD_REGISTER_COUNT = 4; + /** Number of key tables of an LCN module. */ + public static final int KEY_TABLE_COUNT = 4; + /** Number of thresholds before LCN module firmware version 2013 */ + public static final int THRESHOLD_COUNT_BEFORE_2013 = 5; + /** + * Default dimmer output ramp when used with roller shutters. Results in a switching delay of 600ms. Value copied + * from the LCN-PRO motor/shutter command dialog. + */ + public static final int ROLLER_SHUTTER_RAMP_MS = 4000; + /** Max. value of a variable, threshold or regulator setpoint */ + public static final int MAX_VARIABLE_VALUE = 32768; + /** The fixed ramp when output 1+2 are controlled */ + public static final int FIXED_RAMP_MS = 250; + /** Authentication at LCN-PCHK: Request user name. */ + public static final String AUTH_USERNAME = "Username:"; + /** Authentication at LCN-PCHK: Request password. */ + public static final String AUTH_PASSWORD = "Password:"; + /** LCN-PK/PKU is connected. */ + public static final String LCNCONNSTATE_CONNECTED = "$io:#LCN:connected"; + /** LCN-PK/PKU is disconnected. */ + public static final String LCNCONNSTATE_DISCONNECTED = "$io:#LCN:disconnected"; + /** LCN-PCHK/PKE has not enough licenses to handle this connection. */ + public static final String INSUFFICIENT_LICENSES = "$err:(license?)"; + + /** + * LCN dimming mode. + * If solely modules with firmware 170206 or newer are present, LCN-PRO automatically programs {@link #NATIVE200}. + * Otherwise the default is {@link #NATIVE50}. + * Since LCN-PCHK doesn't know the current mode, it must explicitly be set. + */ + public enum OutputPortDimMode { + NATIVE50, // 0..50 dimming steps (all LCN module generations) + NATIVE200 // 0..200 dimming steps (since 170206) + } + + /** + * Tells LCN-PCHK how to format output-port status-messages. + * {@link #NATIVE} allows to show the status in half-percent steps (e.g. "10.5"). + * {@link #NATIVE} is completely backward compatible and there are no restrictions + * concerning the LCN module generations. It requires LCN-PCHK 2.3 or higher though. + */ + public enum OutputPortStatusMode { + PERCENT, // Default (compatible with all versions of LCN-PCHK) + NATIVE // 0..200 steps (since LCN-PCHK 2.3) + } + + /** Possible states for LCN LEDs. */ + public enum LedStatus { + OFF, + ON, + BLINK, + FLICKER; + } + + /** Possible states for LCN logic-operations. */ + public enum LogicOpStatus { + NOT, + OR, // Note: Actually not correct since AND won't be OR also + AND; + } + + /** Time units used for several LCN commands. */ + public enum TimeUnit { + SECONDS, + MINUTES, + HOURS, + DAYS; + } + + /** Relay-state modifiers used in LCN commands. */ + public enum RelayStateModifier { + ON, + OFF, + TOGGLE, + NOCHANGE + } + + /** Value-reference for relative LCN variable commands. */ + public enum RelVarRef { + CURRENT, + PROG // Programmed value (LCN-PRO). Relevant for set-points and thresholds. + } + + /** Command types used when sending LCN keys. */ + public enum SendKeyCommand { + HIT, + MAKE, + BREAK, + DONTSEND + } + + /** Key-lock modifiers used in LCN commands. */ + public enum KeyLockStateModifier { + ON, + OFF, + TOGGLE, + NOCHANGE + } + + /** List of key tables of an LCN module */ + public enum KeyTable { + A, + B, + C, + D + } + + /** + * Generates an array of booleans from an input integer (actually a byte). + * + * @param input the input byte (0..255) + * @return the array of 8 booleans + * @throws IllegalArgumentException if input is out of range (not a byte) + */ + public static boolean[] getBooleanValue(int inputByte) throws IllegalArgumentException { + if (inputByte < 0 || inputByte > 255) { + throw new IllegalArgumentException(); + } + boolean[] result = new boolean[8]; + for (int i = 0; i < 8; ++i) { + result[i] = (inputByte & (1 << i)) != 0; + } + return result; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java new file mode 100644 index 0000000000000..3731bf000b6a3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java @@ -0,0 +1,37 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Default checked exception. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnException extends Exception { + private static final long serialVersionUID = -4341882774124288028L; + + public LcnException() { + super(); + } + + public LcnException(String message) { + super(message); + } + + public LcnException(Exception e) { + super(e); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java new file mode 100644 index 0000000000000..5c76e562639e3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java @@ -0,0 +1,780 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helpers to generate LCN-PCK commands. + *

    + * LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public final class PckGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger(PckGenerator.class); + /** Termination character after a PCK message */ + public static final String TERMINATION = "\n"; + + /** + * Generates a keep-alive. + * LCN-PCHK will close the connection if it does not receive any commands from + * an open {@link Connection} for a specific period (10 minutes by default). + * + * @param counter the current ping's id (optional, but "best practice"). Should start with 1 + * @return the PCK command as text + */ + public static String ping(int counter) { + return String.format("^ping%d", counter); + } + + /** + * Generates a PCK command that will set the LCN-PCHK connection's operation mode. + * This influences how output-port commands and status are interpreted and must be + * in sync with the LCN bus. + * + * @param dimMode see {@link LcnDefs.OutputPortDimMode} + * @param statusMode see {@link LcnDefs.OutputPortStatusMode} + * @return the PCK command as text + */ + public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) { + return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.NATIVE200 ? "1" : "0") + + (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N"); + } + + /** + * Generates a PCK address header. + * Used for commands to LCN modules and groups. + * + * @param addr the target's address (module or group) + * @param localSegId the local segment id where the physical bus connection is located + * @param wantsAck true to claim an acknowledge / receipt from the target + * @return the PCK address header as text + */ + public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) { + return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegmentId(localSegId), + addr.getId(), wantsAck ? "!" : "."); + } + + /** + * Generates a scan-command for LCN segment-couplers. + * Used to detect the local segment (where the physical bus connection is located). + * + * @return the PCK command (without address header) as text + */ + public static String segmentCouplerScan() { + return "SK"; + } + + /** + * Generates a firmware/serial-number request. + * + * @return the PCK command (without address header) as text + */ + public static String requestSn() { + return "SN"; + } + + /** + * Generates a command to request a part of a name of a module. + * + * @param partNumber 0..1 + * @return the PCK command (without address header) as text + */ + public static String requestModuleName(int partNumber) { + return "NMN" + (partNumber + 1); + } + + /** + * Generates an output-port status request. + * + * @param outputId 0..3 + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String requestOutputStatus(int outputId) throws LcnException { + if (outputId < 0 || outputId > 3) { + throw new LcnException(); + } + return String.format("SMA%d", outputId + 1); + } + + /** + * Generates a dim command for a single output-port. + * + * @param outputId 0..3 + * @param percent 0..100 + * @param rampMs ramp in milliseconds + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String dimOutput(int outputId, double percent, int rampMs) throws LcnException { + if (outputId < 0 || outputId > 3) { + throw new LcnException(); + } + int rampNative = PckGenerator.timeToRampValue(rampMs); + int n = (int) Math.round(percent * 2); + if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions) + return String.format("A%dDI%03d%03d", outputId + 1, n / 2, rampNative); + } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3) + return String.format("O%dDI%03d%03d", outputId + 1, n, rampNative); + } + } + + /** + * Generates a dim command for all output-ports. + * + * Attention: This command is supported since module firmware version 180501 AND LCN-PCHK 2.61 + * + * @param firstPercent dimmer value of the first output 0..100 + * @param secondPercent dimmer value of the first output 0..100 + * @param thirdPercent dimmer value of the first output 0..100 + * @param fourthPercent dimmer value of the first output 0..100 + * @param rampMs ramp in milliseconds + * @return the PCK command (without address header) as text + */ + public static String dimAllOutputs(double firstPercent, double secondPercent, double thirdPercent, + double fourthPercent, int rampMs) { + long n1 = Math.round(firstPercent * 2); + long n2 = Math.round(secondPercent * 2); + long n3 = Math.round(thirdPercent * 2); + long n4 = Math.round(fourthPercent * 2); + + return String.format("OY%03d%03d%03d%03d%03d", n1, n2, n3, n4, timeToRampValue(rampMs)); + } + + /** + * Generates a control command for switching all outputs ON or OFF with a fixed ramp of 0.5s. + * + * @param percent 0..100 + * @returnthe PCK command (without address header) as text + */ + public static String controlAllOutputs(double percent) { + return String.format("AH%03d", Math.round(percent)); + } + + /** + * Generates a control command for switching dimmer output 1 and 2 both ON or OFF with a fixed ramp of 0.5s or + * without ramp. + * + * @param on true, if outputs shall be switched on + * @param ramp true, if the ramp shall be 0.5s, else 0s + * @return the PCK command (without address header) as text + */ + public static String controlOutputs12(boolean on, boolean ramp) { + int commandByte; + if (on) { + commandByte = ramp ? 0xC8 : 0xFD; + } else { + commandByte = ramp ? 0x00 : 0xFC; + } + return String.format("X2%03d%03d%03d", 1, commandByte, commandByte); + } + + /** + * Generates a dim command for setting the brightness of dimmer output 1 and 2 with a fixed ramp of 0.5s. + * + * @param percent brightness of both outputs 0..100 + * @return the PCK command (without address header) as text + */ + public static String dimOutputs12(double percent) { + long localPercent = Math.round(percent); + return String.format("AY%03d%03d", localPercent, localPercent); + } + + /** + * Let an output flicker. + * + * @param outputId output id 0..3 + * @param depth flicker depth, the higher the deeper 0..2 + * @param ramp the flicker speed 0..2 + * @param count number of flashes 1..15 + * @return the PCK command (without address header) as text + * @throws LcnException when the input values are out of range + */ + public static String flickerOutput(int outputId, int depth, int ramp, int count) throws LcnException { + if (outputId < 0 || outputId > 3) { + throw new LcnException("Output number out of range"); + } + if (count < 1 || count > 15) { + throw new LcnException("Number of flashes out of range"); + } + String depthString; + switch (depth) { + case 0: + depthString = "G"; + break; + case 1: + depthString = "M"; + break; + case 2: + depthString = "S"; + break; + default: + throw new LcnException("Depth out of range"); + } + String rampString; + switch (ramp) { + case 0: + rampString = "L"; + break; + case 1: + rampString = "M"; + break; + case 2: + rampString = "S"; + break; + default: + throw new LcnException("Ramp out of range"); + } + return String.format("A%dFL%s%s%02d", outputId + 1, depthString, rampString, count); + } + + /** + * Generates a command to change the value of an output-port. + * + * @param outputId 0..3 + * @param percent -100..100 + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String relOutput(int outputId, double percent) throws LcnException { + if (outputId < 0 || outputId > 3) { + throw new LcnException(); + } + int n = (int) Math.round(percent * 2); + if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions) + return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2)); + } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3) + return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n)); + } + } + + /** + * Generates a command that toggles a single output-port (on->off, off->on). + * + * @param outputId 0..3 + * @param ramp see {@link PckGenerator#timeToRampValue(int)} + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String toggleOutput(int outputId, int ramp) throws LcnException { + if (outputId < 0 || outputId > 3) { + throw new LcnException(); + } + return String.format("A%dTA%03d", outputId + 1, ramp); + } + + /** + * Generates a command that toggles all output-ports (on->off, off->on). + * + * @param ramp see {@link PckGenerator#timeToRampValue(int)} + * @return the PCK command (without address header) as text + */ + public static String toggleAllOutputs(int ramp) { + return String.format("AU%03d", ramp); + } + + /** + * Generates a relays-status request. + * + * @return the PCK command (without address header) as text + */ + public static String requestRelaysStatus() { + return "SMR"; + } + + /** + * Generates a command to control relays. + * + * @param states the 8 modifiers for the relay states + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws LcnException { + if (states.length != 8) { + throw new LcnException(); + } + StringBuilder ret = new StringBuilder("R8"); + for (int i = 0; i < 8; ++i) { + switch (states[i]) { + case ON: + ret.append("1"); + break; + case OFF: + ret.append("0"); + break; + case TOGGLE: + ret.append("U"); + break; + case NOCHANGE: + ret.append("-"); + break; + default: + throw new LcnException(); + } + } + return ret.toString(); + } + + /** + * Generates a binary-sensors status request. + * + * @return the PCK command (without address header) as text + */ + public static String requestBinSensorsStatus() { + return "SMB"; + } + + /** + * Generates a command that sets a variable absolute. + * + * @param number regulator number 0..1 + * @param value the absolute value to set + * @return the PCK command (without address header) as text + * @throws LcnException + */ + public static String setSetpointAbsolute(int number, int value) { + int internalValue = value; + // Set absolute (not in PCK yet) + int b1 = number << 6; // 01000000 + b1 |= 0x20; // xx10xxxx (set absolute) + if (value < 1000) { + internalValue = 1000 - internalValue; + b1 |= 8; + } else { + internalValue -= 1000; + } + b1 |= (internalValue >> 8) & 0x0f; // xxxx1111 + int b2 = internalValue & 0xff; + return String.format("X2%03d%03d%03d", 30, b1, b2); + } + + /** + * Generates a command to change the value of a variable. + * + * @param variable the target variable to change + * @param type the reference-point + * @param value the native LCN value to add/subtract (can be negative) + * @return the PCK command (without address header) as text + * @throws LcnException if command is not supported + */ + public static String setVariableRelative(Variable variable, LcnDefs.RelVarRef type, int value) { + if (variable.getNumber() == 0) { + // Old command for variable 1 / T-var (compatible with all modules) + return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value)); + } else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8) + return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", variable.getNumber() + 1, Math.abs(value)); + } + } + + /** + * Generates a command the change the value of a regulator setpoint relative. + * + * @param number 0..1 + * @param type relative to the current or to the programmed value + * @param value the relative value -4000..+4000 + * @return the PCK command (without address header) as text + */ + public static String setSetpointRelative(int number, LcnDefs.RelVarRef type, int value) { + return String.format("RE%sS%s%s%d", number == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P", + value >= 0 ? "+" : "-", Math.abs(value)); + } + + /** + * Generates a command the change the value of a threshold relative. + * + * @param variable the threshold to change + * @param type relative to the current or to the programmed value + * @param value the relative value -4000..+4000 + * @param is2013 true, if the LCN module's firmware is equal to or newer than 2013 + * @return the PCK command (without address header) as text + */ + public static String setThresholdRelative(Variable variable, LcnDefs.RelVarRef type, int value, boolean is2013) + throws LcnException { + if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8) + return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value), + value >= 0 ? "A" : "S", variable.getNumber() + 1, variable.getThresholdNumber().get() + 1); + } else if (variable.getNumber() == 0) { // Old command for register 1 (before 170206) + return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value), + value >= 0 ? "A" : "S", variable.getThresholdNumber().get() == 0 ? "1" : "0", + variable.getThresholdNumber().get() == 1 ? "1" : "0", + variable.getThresholdNumber().get() == 2 ? "1" : "0", + variable.getThresholdNumber().get() == 3 ? "1" : "0", + variable.getThresholdNumber().get() == 4 ? "1" : "0"); + } else { + throw new LcnException( + "Module does not have threshold register " + (variable.getThresholdNumber().get() + 1)); + } + } + + /** + * Generates a variable value request. + * + * @param variable the variable to request + * @param firmwareVersion the target module's firmware version + * @return the PCK command (without address header) as text + * @throws LcnException if command is not supported + */ + public static String requestVarStatus(Variable variable, int firmwareVersion) throws LcnException { + if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013) { + int id = variable.getNumber(); + switch (variable.getType()) { + case UNKNOWN: + throw new LcnException("Variable unknown"); + case VARIABLE: + return String.format("MWT%03d", id + 1); + case REGULATOR: + return String.format("MWS%03d", id + 1); + case THRESHOLD: + return String.format("SE%03d", id + 1); // Whole register + case S0INPUT: + return String.format("MWC%03d", id + 1); + } + throw new LcnException("Unsupported variable type: " + variable); + } else { + switch (variable) { + case VARIABLE1: + return "MWV"; + case VARIABLE2: + return "MWTA"; + case VARIABLE3: + return "MWTB"; + case RVARSETPOINT1: + return "MWSA"; + case RVARSETPOINT2: + return "MWSB"; + case THRESHOLDREGISTER11: + case THRESHOLDREGISTER12: + case THRESHOLDREGISTER13: + case THRESHOLDREGISTER14: + case THRESHOLDREGISTER15: + return "SL1"; // Whole register + default: + throw new LcnException("Unsupported variable type: " + variable); + } + } + } + + /** + * Generates a request for LED and logic-operations states. + * + * @return the PCK command (without address header) as text + */ + public static String requestLedsAndLogicOpsStatus() { + return "SMT"; + } + + /** + * Generates a command to the set the state of a single LED. + * + * @param ledId 0..11 + * @param state the state to set + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String controlLed(int ledId, LcnDefs.LedStatus state) throws LcnException { + if (ledId < 0 || ledId > 11) { + throw new LcnException(); + } + return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A" + : state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F"); + } + + /** + * Generates a command to send LCN keys. + * + * @param cmds the 4 concrete commands to send for the tables (A-D) + * @param keys the tables' 8 key-states (true means "send") + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws LcnException { + if (cmds.length != 4 || keys.length != 8) { + throw new LcnException(); + } + StringBuilder ret = new StringBuilder("TS"); + for (int i = 0; i < 4; ++i) { + switch (cmds[i]) { + case HIT: + ret.append("K"); + break; + case MAKE: + ret.append("L"); + break; + case BREAK: + ret.append("O"); + break; + case DONTSEND: + // By skipping table D (if it is not used), we use the old command + // for table A-C which is compatible with older LCN modules + if (i < 3) { + ret.append("-"); + } + break; + default: + throw new LcnException(); + } + } + for (int i = 0; i < 8; ++i) { + ret.append(keys[i] ? "1" : "0"); + } + return ret.toString(); + } + + /** + * Generates a command to send LCN keys deferred / delayed. + * + * @param tableId 0(A)..3(D) + * @param time the delay time + * @param timeUnit the time unit + * @param keys the key-states (true means "send") + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) + throws LcnException { + if (tableId < 0 || tableId > 3 || keys.length != 8) { + throw new LcnException(); + } + StringBuilder ret = new StringBuilder("TV"); + switch (tableId) { + case 0: + ret.append("A"); + break; + case 1: + ret.append("B"); + break; + case 2: + ret.append("C"); + break; + case 3: + ret.append("D"); + break; + default: + throw new LcnException(); + } + ret.append(String.format("%03d", time)); + switch (timeUnit) { + case SECONDS: + if (time < 1 || time > 60) { + throw new LcnException(); + } + ret.append("S"); + break; + case MINUTES: + if (time < 1 || time > 90) { + throw new LcnException(); + } + ret.append("M"); + break; + case HOURS: + if (time < 1 || time > 50) { + throw new LcnException(); + } + ret.append("H"); + break; + case DAYS: + if (time < 1 || time > 45) { + throw new LcnException(); + } + ret.append("D"); + break; + default: + throw new LcnException(); + } + for (int i = 0; i < 8; ++i) { + ret.append(keys[i] ? "1" : "0"); + } + return ret.toString(); + } + + /** + * Generates a request for key-lock states. + * Always requests table A-D. Supported since LCN-PCHK 2.8. + * + * @return the PCK command (without address header) as text + */ + public static String requestKeyLocksStatus() { + return "STX"; + } + + /** + * Generates a command to lock keys. + * + * @param tableId 0(A)..3(D) + * @param states the 8 key-lock modifiers + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws LcnException { + if (tableId < 0 || tableId > 3 || states.length != 8) { + throw new LcnException(); + } + StringBuilder ret = new StringBuilder( + String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D")); + for (int i = 0; i < 8; ++i) { + switch (states[i]) { + case ON: + ret.append("1"); + break; + case OFF: + ret.append("0"); + break; + case TOGGLE: + ret.append("U"); + break; + case NOCHANGE: + ret.append("-"); + break; + default: + throw new LcnException(); + } + } + return ret.toString(); + } + + /** + * Generates a command to lock keys for table A temporary. + * There is no hardware-support for locking tables B-D. + * + * @param time the lock time + * @param timeUnit the time unit + * @param keys the 8 key-lock states (true means lock) + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) throws LcnException { + if (keys.length != 8) { + throw new LcnException(); + } + StringBuilder ret = new StringBuilder(String.format("TXZA%03d", time)); + switch (timeUnit) { + case SECONDS: + if (time < 1 || time > 60) { + throw new LcnException(); + } + ret.append("S"); + break; + case MINUTES: + if (time < 1 || time > 90) { + throw new LcnException(); + } + ret.append("M"); + break; + case HOURS: + if (time < 1 || time > 50) { + throw new LcnException(); + } + ret.append("H"); + break; + case DAYS: + if (time < 1 || time > 45) { + throw new LcnException(); + } + ret.append("D"); + break; + default: + throw new LcnException(); + } + for (int i = 0; i < 8; ++i) { + ret.append(keys[i] ? "1" : "0"); + } + return ret.toString(); + } + + /** + * Generates the command header / start for sending dynamic texts. + * Used by LCN-GTxD periphery (supports 4 text rows). + * To complete the command, the text to send must be appended (UTF-8 encoding). + * Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each. + * + * @param row 0..3 + * @param part 0..4 + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String dynTextHeader(int row, int part) throws LcnException { + if (row < 0 || row > 3 || part < 0 || part > 4) { + throw new LcnException("Row number is out of range: " + (row + 1)); + } + return String.format("GTDT%d%d", row + 1, part + 1); + } + + /** + * Generates a command to lock a regulator. + * + * @param regId 0..1 + * @param state the lock state + * @return the PCK command (without address header) as text + * @throws LcnException if out of range + */ + public static String lockRegulator(int regId, boolean state) throws LcnException { + if (regId < 0 || regId > 1) { + throw new LcnException(); + } + return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A"); + } + + /** + * Generates a null command, used for broadcast messages. + * + * @return the PCK command (without address header) as text + */ + public static String nullCommand() { + return "LEER"; + } + + /** + * Converts the given time into an LCN ramp value. + * + * @param timeMSec the time in milliseconds + * @return the (LCN-internal) ramp value (0..250) + */ + private static int timeToRampValue(int timeMSec) { + int ret; + if (timeMSec < 250) { + ret = 0; + } else if (timeMSec < 500) { + ret = 1; + } else if (timeMSec < 660) { + ret = 2; + } else if (timeMSec < 1000) { + ret = 3; + } else if (timeMSec < 1400) { + ret = 4; + } else if (timeMSec < 2000) { + ret = 5; + } else if (timeMSec < 3000) { + ret = 6; + } else if (timeMSec < 4000) { + ret = 7; + } else if (timeMSec < 5000) { + ret = 8; + } else if (timeMSec < 6000) { + ret = 9; + } else { + ret = (timeMSec / 1000 - 6) / 2 + 10; + if (ret > 250) { + ret = 250; + LOGGER.warn("Ramp value is too high. Limiting value to 486s."); + } + } + return ret; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java new file mode 100644 index 0000000000000..70a6c1fc82bd8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java @@ -0,0 +1,53 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Helper to bitwise reverse numbers. + * + * @author Tobias Jüttner - Initial Contribution + */ +@NonNullByDefault +final class ReverseNumber { + /** Cache with all reversed 8 bit values. */ + private static final int[] REVERSED_UINT8 = new int[256]; + + /** Initializes static data once this class is first used. */ + static { + for (int i = 0; i < 256; ++i) { + int reversed = 0; + for (int j = 0; j < 8; ++j) { + if ((i & (1 << j)) != 0) { + reversed |= (0x80 >> j); + } + } + REVERSED_UINT8[i] = reversed; + } + } + + /** + * Reverses the given 8 bit value bitwise. + * + * @param value the value to reverse bitwise (treated as unsigned 8 bit value) + * @return the reversed value + * @throws LcnException if value is out of range (not unsigned 8 bit) + */ + static int reverseUInt8(int value) throws LcnException { + if (value < 0 || value > 255) { + throw new LcnException(); + } + return REVERSED_UINT8[value]; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java new file mode 100644 index 0000000000000..c32573988d5ee --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java @@ -0,0 +1,278 @@ +/** + * 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.lcn.internal.common; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnBindingConstants; + +/** + * LCN variable types. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public enum Variable { + UNKNOWN(0, Type.UNKNOWN, LcnChannelGroup.VARIABLE), // Used if the real type is not known (yet) + VARIABLE1(0, Type.VARIABLE, LcnChannelGroup.VARIABLE), // or TVar + VARIABLE2(1, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE3(2, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE4(3, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE5(4, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE6(5, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE7(6, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE8(7, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE9(8, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE10(9, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE11(10, Type.VARIABLE, LcnChannelGroup.VARIABLE), + VARIABLE12(11, Type.VARIABLE, LcnChannelGroup.VARIABLE), // Since 170206 + RVARSETPOINT1(0, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT), + RVARSETPOINT2(1, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT), // Set-points for regulators + THRESHOLDREGISTER11(0, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1), + THRESHOLDREGISTER12(0, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1), + THRESHOLDREGISTER13(0, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1), + THRESHOLDREGISTER14(0, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1), + // Register 1 (THRESHOLDREGISTER15 only before 170206) + THRESHOLDREGISTER15(0, 4, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1), + THRESHOLDREGISTER21(1, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), + THRESHOLDREGISTER22(1, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), + THRESHOLDREGISTER23(1, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), + THRESHOLDREGISTER24(1, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), // Register 2 (since 2012) + THRESHOLDREGISTER31(2, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), + THRESHOLDREGISTER32(2, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), + THRESHOLDREGISTER33(2, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), + THRESHOLDREGISTER34(2, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), // Register 3 (since 2012) + THRESHOLDREGISTER41(3, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), + THRESHOLDREGISTER42(3, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), + THRESHOLDREGISTER43(3, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), + THRESHOLDREGISTER44(3, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), // Register 4 (since 2012) + S0INPUT1(0, Type.S0INPUT, LcnChannelGroup.S0INPUT), + S0INPUT2(1, Type.S0INPUT, LcnChannelGroup.S0INPUT), + S0INPUT3(2, Type.S0INPUT, LcnChannelGroup.S0INPUT), + S0INPUT4(3, Type.S0INPUT, LcnChannelGroup.S0INPUT); // LCN-BU4L + + private final int number; + private final Optional thresholdNumber; + private final Type type; + private final LcnChannelGroup channelGroup; + + /** + * Defines the origin of an LCN variable. + */ + public enum Type { + UNKNOWN, + VARIABLE, + REGULATOR, + THRESHOLD, + S0INPUT + } + + Variable(int number, Type type, LcnChannelGroup channelGroup) { + this(number, Optional.empty(), type, channelGroup); + } + + Variable(int number, int thresholdNumber, Type type, LcnChannelGroup channelGroup) { + this(number, Optional.of(thresholdNumber), type, channelGroup); + } + + Variable(int number, Optional thresholdNumber, Type type, LcnChannelGroup channelGroup) { + this.number = number; + this.type = type; + this.channelGroup = channelGroup; + this.thresholdNumber = thresholdNumber; + } + + /** + * Gets the type of the variable's origin. + * + * @return the type + */ + public Type getType() { + return type; + } + + /** + * Gets the channel type of the variable. + * + * @return the channel type + */ + public LcnChannelGroup getChannelType() { + return channelGroup; + } + + /** + * Gets the threshold number within a threshold register. + * + * @return the threshold number + */ + public Optional getThresholdNumber() { + return thresholdNumber; + } + + /** + * Gets the threshold register number. + * + * @return the threshold register number + */ + public int getNumber() { + return number; + } + + /** + * Translates a given id into a variable type. + * + * @param number 0..11 + * @return the translated {@link Variable} + * @throws LcnException if out of range + */ + public static Variable varIdToVar(int number) throws LcnException { + if (number < 0 || number >= LcnChannelGroup.VARIABLE.getCount()) { + throw new LcnException("Invalid variable number: " + (number + 1)); + } + return getVariableFromNumberAndType(number, Type.VARIABLE, v -> true); + } + + /** + * Translates a given id into a LCN set-point variable type. + * + * @param number 0..1 + * @return the translated {@link Variable} + * @throws LcnException if out of range + */ + public static Variable setPointIdToVar(int number) throws LcnException { + if (number < 0 || number >= LcnChannelGroup.RVARSETPOINT.getCount()) { + throw new LcnException(); + } + + return getVariableFromNumberAndType(number, Type.REGULATOR, v -> true); + } + + /** + * Translates given ids into a LCN threshold variable type. + * + * @param registerNumber 0..3 + * @param thresholdNumber 0..4 for register 0, 0..3 for registers 1..3 + * @return the translated {@link Variable} + * @throws LcnException if out of range + */ + public static Variable thrsIdToVar(int registerNumber, int thresholdNumber) throws LcnException { + if (registerNumber < 0 || registerNumber >= LcnDefs.THRESHOLD_REGISTER_COUNT) { + throw new LcnException("Threshold register number out of range: " + (registerNumber + 1)); + } + if (thresholdNumber < 0 || thresholdNumber >= (registerNumber == 0 ? 5 : 4)) { + throw new LcnException("Threshold number out of range: " + (thresholdNumber + 1)); + } + return getVariableFromNumberAndType(registerNumber, Type.THRESHOLD, + v -> v.thresholdNumber.get() == thresholdNumber); + } + + /** + * Translates a given id into a LCN S0-input variable type. + * + * @param number 0..3 + * @return the translated {@link Variable} + * @throws LcnException if out of range + */ + public static Variable s0IdToVar(int number) throws LcnException { + if (number < 0 || number >= LcnChannelGroup.S0INPUT.getCount()) { + throw new LcnException(); + } + return getVariableFromNumberAndType(number, Type.S0INPUT, v -> true); + } + + private static Variable getVariableFromNumberAndType(int varId, Type type, Predicate filter) + throws LcnException { + return Stream.of(values()).filter(v -> v.type == type).filter(v -> v.number == varId).filter(filter).findAny() + .orElseThrow(LcnException::new); + } + + /** + * Checks if this variable type uses special values. + * Examples for special values: "No value yet", "sensor defective" etc. + * + * @return true if special values are in use + */ + public boolean useLcnSpecialValues() { + return type != Type.S0INPUT; + } + + /** + * Module-generation check. + * Checks if the given variable type would receive a typed response if + * its status was requested. + * + * @param firmwareVersion the target LCN-modules firmware version + * @return true if a response would contain the variable's type + */ + public boolean hasTypeInResponse(int firmwareVersion) { + return (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013 + || (type != Type.VARIABLE && type != Type.REGULATOR)); + } + + /** + * Module-generation check. + * Checks if the given variable type automatically sends status-updates on + * value-change. It must be polled otherwise. + * + * @param firmwareVersion the target LCN-module's firmware version + * @return true if the LCN module supports automatic status-messages for this {@link Variable} + */ + public boolean isEventBased(int firmwareVersion) { + return type == Type.REGULATOR || type == Type.S0INPUT || firmwareVersion >= LcnBindingConstants.FIRMWARE_2013; + } + + /** + * Module-generation check. + * Checks if the target LCN module would automatically send status-updates if + * the given variable type was changed by command. + * + * @param variable the variable type to check + * @param is2013 the target module's-generation + * @return true if a poll is required to get the new status-value + */ + public boolean shouldPollStatusAfterCommand(int firmwareVersion) { + // Regulator set-points will send status-messages on every change (all firmware versions) + if (type == Type.REGULATOR) { + return false; + } + // Thresholds since 170206 will send status-messages on every change + if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013 && type == Type.THRESHOLD) { + return false; + } + // Others: + // - Variables before 170206 will never send any status-messages + // - Variables since 170206 only send status-messages on "big" changes + // - Thresholds before 170206 will never send any status-messages + // - S0-inputs only send status-messages on "big" changes + // (all "big changes" cases force us to poll the status to get faster updates) + return true; + } + + /** + * Module-generation check. + * Checks if the target LCN module would automatically send status-updates if + * the given regulator's lock-state was changed by command. + * + * @param firmwareVersion the target LCN-module's firmware version + * @param lockState the lock-state sent via command + * @return true if a poll is required to get the new status-value + */ + public boolean shouldPollStatusAfterRegulatorLock(int firmwareVersion, boolean lockState) { + // LCN modules before 170206 will send an automatic status-message for "lock", but not for "unlock" + return !lockState && firmwareVersion < LcnBindingConstants.FIRMWARE_2013; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java new file mode 100644 index 0000000000000..90c6bf83c8cb6 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java @@ -0,0 +1,99 @@ +/** + * 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.lcn.internal.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.types.State; + +/** + * A value of an LCN variable. + *

    + * It internally stores the native LCN value and allows to convert from/into other units. + * Some conversions allow to specify whether the source value is absolute or relative. + * Relative values are used to create {@link VariableValue}s that can be added/subtracted from + * other (absolute) {@link VariableValue}s. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public class VariableValue { + private static final String SENSOR_DEFECTIVE_STATE = "DEFECTIVE"; + + /** The absolute, native LCN value. */ + private final long nativeValue; + + /** + * Constructor with native LCN value. + * + * @param nativeValue the native value + */ + public VariableValue(long nativeValue) { + this.nativeValue = nativeValue; + } + + /** + * Converts to native value. Mask locked bit. + * + * @return the converted value + */ + public long toNative(boolean useSpecialValues) { + if (useSpecialValues) { + return nativeValue & 0x7fff; + } else { + return nativeValue; + } + } + + /** + * Returns the lock state if value comes from a regulator set-point. + * If the variable type is not a regulator, the result is undefined. + * + * @return true if the regulator is locked + */ + public boolean isRegulatorLocked() { + return (this.nativeValue & 0x8000) != 0; + } + + /** + * Returns the defective state of the originating sensor for this variable. + * + * @return true if the sensor is defective + */ + public boolean isSensorDefective() { + return nativeValue == 0x7f00; + } + + /** + * Returns the configuration state of the variable. + * + * @return true if the variable is configured via LCN-PRO + */ + public boolean isConfigured() { + return this.nativeValue != 0xFFFF; + } + + public State getState(Variable variable) { + State stateValue; + if (variable.useLcnSpecialValues() && isSensorDefective()) { + stateValue = new StringType(SENSOR_DEFECTIVE_STATE); + } else if (variable.useLcnSpecialValues() && !isConfigured()) { + stateValue = new StringType("Not configured in LCN-PRO"); + } else { + stateValue = new DecimalType(toNative(variable.useLcnSpecialValues())); + } + return stateValue; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java new file mode 100644 index 0000000000000..13d001868090d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java @@ -0,0 +1,100 @@ +/** + * 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.lcn.internal.connection; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.util.concurrent.ScheduledExecutorService; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnDefs; + +/** + * Base class representing LCN-PCK gateway connection states. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractConnectionState extends AbstractState { + /** The PCK gateway's Connection */ + protected final Connection connection; + + public AbstractConnectionState(ConnectionStateMachine context) { + super(context); + this.connection = context.getConnection(); + } + + /** + * Callback method when a PCK message has been received. + * + * @param data the received PCK message without line termination character + */ + public abstract void onPckMessageReceived(String data); + + /** + * Gets the framework's scheduler. + * + * @return the scheduler + */ + public ScheduledExecutorService getScheduler() { + return context.getScheduler(); + } + + /** + * Enqueues a PCK message to be sent. When the connection is offline, the message will be buffered and sent when the + * connection is established. When the enqueued PCK message is too old, it will be discarded before a new connection + * is established. + * + * @param addr the module's address to which is message shall be sent + * @param wantsAck true, if the module shall respond with an Ack upon successful processing + * @param data the PCK message to be sent + */ + public void queue(LcnAddr addr, boolean wantsAck, byte[] data) { + connection.queueOffline(addr, wantsAck, data); + } + + /** + * Shuts the Connection down finally. A shut-down connection cannot re-used. + */ + public void shutdownFinally() { + nextState(ConnectionStateShutdown::new); + } + + /** + * Checks if the given PCK message is an LCN bus disconnect message. If so, openHAB will be informed and the + * Connection's State Machine waits for a re-connect. + * + * @param pck the PCK message to check + */ + protected void parseLcnBusDiconnectMessage(String pck) { + if (pck.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) { + connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE"); + nextState(ConnectionStateWaitForLcnBusConnectedAfterDisconnected::new); + } + } + + /** + * Closes the Connection SocketChannel. + */ + protected void closeSocketChannel() { + try { + Channel socketChannel = connection.getSocketChannel(); + if (socketChannel != null) { + socketChannel.close(); + } + } catch (IOException e) { + // ignore + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java new file mode 100644 index 0000000000000..4037bf47e29ca --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java @@ -0,0 +1,48 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Base class for sending username or password. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractConnectionStateSendCredentials extends AbstractConnectionState { + private static final int AUTH_TIMEOUT_SEC = 10; + + public AbstractConnectionStateSendCredentials(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), AUTH_TIMEOUT_SEC, + TimeUnit.SECONDS)); + } + + /** + * Starts a timeout when the PCK gateway does not answer to the credentials. + */ + protected void startTimeoutTimer() { + addTimer(getScheduler().schedule( + () -> context.handleConnectionFailed( + new LcnException("Network timeout in state " + getClass().getSimpleName())), + connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java new file mode 100644 index 0000000000000..0b8bbd5a5205b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java @@ -0,0 +1,76 @@ +/** + * 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.lcn.internal.connection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Base class for all states used with {@link AbstractStateMachine}. + * + * @param type of the state machine implementation + * @param type of the state implementation + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractState, U extends AbstractState> { + private final List> usedTimers = Collections.synchronizedList(new ArrayList<>()); + protected final T context; + + public AbstractState(T context) { + this.context = context; + } + + /** + * Invoked when the State shall start its operation. + */ + protected abstract void startWorking(); + + /** + * Stops all timers, the State has been started. + */ + protected void cancelAllTimers() { + synchronized (usedTimers) { + usedTimers.forEach(t -> t.cancel(true)); + } + } + + /** + * When a state starts a timer, its ScheduledFuture must be registered by this method. All timers added by this + * method, are canceled when the StateMachine leaves this State. + * + * @param timer the new timer + */ + protected void addTimer(ScheduledFuture timer) { + usedTimers.add(timer); + } + + /** + * Sets a new State. The current state is torn down gracefully. + * + * @param newStateFactory the lambda returning the new State + */ + protected void nextState(Function newStateFactory) { + synchronized (context) { + if (context.isStateActive(this)) { + context.setState(newStateFactory); + } + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java new file mode 100644 index 0000000000000..fbf44830c440e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java @@ -0,0 +1,64 @@ +/** + * 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.lcn.internal.connection; + +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for state machines. + * + * @param type of the state machine implementation + * @param type of the state implementation + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractStateMachine, U extends AbstractState> { + private final Logger logger = LoggerFactory.getLogger(AbstractStateMachine.class); + /** The StateMachine's current state */ + protected @Nullable volatile U state; + + /** + * Sets the current state. + * + * @param newStateFactory the new state's factory + */ + protected synchronized void setState(Function newStateFactory) { + @Nullable + U localState = state; + if (localState != null) { + localState.cancelAllTimers(); + } + + @SuppressWarnings("unchecked") + U newState = newStateFactory.apply((T) this); + + if (localState != null) { + logger.debug("Changing state {} -> {}", localState.getClass().getSimpleName(), + newState.getClass().getSimpleName()); + } + + state = newState; + + state.startWorking(); + } + + protected boolean isStateActive(AbstractState otherState) { + return state == otherState; // compare by identity + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java new file mode 100644 index 0000000000000..f89a7051c1e8c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java @@ -0,0 +1,474 @@ +/** + * 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.lcn.internal.connection; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.Channel; +import java.nio.channels.CompletionHandler; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnAddrGrp; +import org.openhab.binding.lcn.internal.common.LcnAddrMod; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents a configured connection to one LCN-PCHK. + * It uses a {@link AsynchronousSocketChannel} to connect to LCN-PCHK. + * Included logic: + *

      + *
    • Reconnection on connection loss + *
    • Segment scan (to detect the local segment ID) + *
    • Acknowledge handling + *
    • Periodic value requests + *
    • Caching of runtime data about the underlying LCN bus + *
    + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class Connection { + private final Logger logger = LoggerFactory.getLogger(Connection.class); + private static final int BROADCAST_MODULE_ID = 3; + private static final int BROADCAST_SEGMENT_ID = 3; + private final ConnectionSettings settings; + private final ConnectionCallback callback; + @Nullable + private AsynchronousSocketChannel channel; + /** The local segment id. -1 means "unknown". */ + private int localSegId; + private final ByteBuffer readBuffer = ByteBuffer.allocate(1024); + private final ByteArrayOutputStream sendBuffer = new ByteArrayOutputStream(); + private final Queue<@Nullable SendData> sendQueue = new LinkedBlockingQueue<>(); + private final BlockingQueue offlineSendQueue = new LinkedBlockingQueue<>(); + private final Map modData = Collections.synchronizedMap(new HashMap<>()); + private volatile boolean writeInProgress; + private final ScheduledExecutorService scheduler; + private final ConnectionStateMachine connectionStateMachine; + + /** + * Constructs a clean (disconnected) connection with the given settings. + * This does not start the actual connection process. + * + * @param sets the settings to use for the new connection + * @param callback the callback to the owner + * @throws IOException + */ + public Connection(ConnectionSettings sets, ScheduledExecutorService scheduler, ConnectionCallback callback) { + this.settings = sets; + this.callback = callback; + this.scheduler = scheduler; + this.clearRuntimeData(); + + connectionStateMachine = new ConnectionStateMachine(this, scheduler); + } + + /** Clears all runtime data. */ + void clearRuntimeData() { + this.channel = null; + this.localSegId = -1; + this.readBuffer.clear(); + this.sendQueue.clear(); + this.sendBuffer.reset(); + } + + /** + * Retrieves the settings for this connection (never changed). + * + * @return the settings + */ + public ConnectionSettings getSettings() { + return this.settings; + } + + private boolean isSocketConnected() { + try { + AsynchronousSocketChannel localChannel = channel; + return localChannel != null && localChannel.getRemoteAddress() != null; + } catch (IOException e) { + return false; + } + } + + /** + * Sets the local segment id. + * + * @param localSegId the new local segment id + */ + public void setLocalSegId(int localSegId) { + this.localSegId = localSegId; + } + + /** + * Called whenever an acknowledge is received. + * + * @param addr the source LCN module + * @param code the LCN internal code (-1 = "positive") + */ + public void onAck(LcnAddrMod addr, int code) { + synchronized (modData) { + if (modData.containsKey(addr)) { + modData.get(addr).onAck(code, this, this.settings.getTimeout(), System.nanoTime()); + } + } + } + + /** + * Creates and/or returns cached data for the given LCN module. + * + * @param addr the module's address + * @return the data + */ + public ModInfo updateModuleData(LcnAddrMod addr) { + return modData.computeIfAbsent(addr, ModInfo::new); + } + + /** + * Reads and processes input from the underlying channel. + * Fragmented input is kept in {@link #readBuffer} and will be processed with the next call. + * + * @throws IOException if connection was closed or a generic channel error occurred + */ + void readAndProcess() { + AsynchronousSocketChannel localChannel = channel; + if (localChannel != null && isSocketConnected()) { + localChannel.read(readBuffer, null, new CompletionHandler<@Nullable Integer, @Nullable Void>() { + @Override + public void completed(@Nullable Integer transmittedByteCount, @Nullable Void attachment) { + synchronized (Connection.this) { + if (transmittedByteCount == null || transmittedByteCount == -1) { + String msg = "Connection was closed by foreign host."; + connectionStateMachine.handleConnectionFailed(new LcnException(msg)); + } else { + // read data chunks from socket and separate frames + readBuffer.flip(); + int aPos = readBuffer.position(); // 0 + String s = new String(readBuffer.array(), aPos, transmittedByteCount, LcnDefs.LCN_ENCODING); + int pos1 = 0, pos2 = s.indexOf(PckGenerator.TERMINATION, pos1); + while (pos2 != -1) { + String data = s.substring(pos1, pos2); + if (logger.isTraceEnabled()) { + logger.trace("Received: '{}'", data); + } + scheduler.submit(() -> { + connectionStateMachine.onInputReceived(data); + callback.onPckMessageReceived(data); + }); + // Seek position in input array + aPos += s.substring(pos1, pos2 + 1).getBytes(LcnDefs.LCN_ENCODING).length; + // Next input + pos1 = pos2 + 1; + pos2 = s.indexOf(PckGenerator.TERMINATION, pos1); + } + readBuffer.limit(readBuffer.capacity()); + readBuffer.position(transmittedByteCount - aPos); // Keeps fragments for the next call + + if (isSocketConnected()) { + readAndProcess(); + } + } + } + } + + @Override + public void failed(@Nullable Throwable e, @Nullable Void attachment) { + logger.debug("Lost connection"); + connectionStateMachine.handleConnectionFailed(e); + } + }); + } else { + connectionStateMachine.handleConnectionFailed(new LcnException("Socket not open")); + } + } + + /** + * Writes all queued data. + * Will try to write all data at once to reduce overhead. + */ + public synchronized void triggerWriteToSocket() { + AsynchronousSocketChannel localChannel = channel; + if (localChannel == null || !isSocketConnected() || writeInProgress) { + return; + } + sendBuffer.reset(); + SendData item = sendQueue.poll(); + + if (item != null) { + try { + if (!item.write(sendBuffer, localSegId)) { + logger.warn("Data loss: Could not write packet into send buffer"); + } + + writeInProgress = true; + byte[] data = sendBuffer.toByteArray(); + localChannel.write(ByteBuffer.wrap(data), null, + new CompletionHandler<@Nullable Integer, @Nullable Void>() { + @Override + public void completed(@Nullable Integer result, @Nullable Void attachment) { + synchronized (Connection.this) { + if (result != data.length) { + logger.warn("Data loss while writing to channel: {}", settings.getAddress()); + } else { + if (logger.isTraceEnabled()) { + logger.trace("Sent: {}", new String(data, 0, data.length)); + } + } + + writeInProgress = false; + + if (sendQueue.size() > 0) { + /** + * This could lead to stack overflows, since the CompletionHandler may run in + * the same Thread as triggerWriteToSocket() is invoked (see + * {@link AsynchronousChannelGroup}/Threading), but we do not expect as much + * data in one chunk here, that the stack can be filled in a critical way. + */ + triggerWriteToSocket(); + } + } + } + + @Override + public void failed(@Nullable Throwable exc, @Nullable Void attachment) { + synchronized (Connection.this) { + if (exc != null) { + logger.warn("Writing to channel \"{}\" failed: {}", settings.getAddress(), + exc.getMessage()); + } + writeInProgress = false; + connectionStateMachine.handleConnectionFailed(new LcnException("write() failed")); + } + } + }); + } catch (BufferOverflowException | IOException e) { + logger.warn("Sending failed: {}: {}: {}", item, e.getClass().getSimpleName(), e.getMessage()); + } + } + } + + /** + * Queues plain text to be sent to LCN-PCHK. + * Sending will be done the next time {@link #triggerWriteToSocket()} is called. + * + * @param plainText the text + */ + public void queueDirectlyPlainText(String plainText) { + this.queueAndSend(new SendDataPlainText(plainText)); + } + + /** + * Queues a PCK command to be sent. + * + * @param addr the target LCN address + * @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses) + * @param pck the pure PCK command (without address header) + */ + void queueDirectly(LcnAddr addr, boolean wantsAck, String pck) { + this.queueDirectly(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING)); + } + + /** + * Queues a PCK command for immediate sending, regardless of the Connection state. The PCK command is automatically + * re-sent if the destination is not a group, an Ack is requested and the module did not answer within the expected + * time. + * + * @param addr the target LCN address + * @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses) + * @param data the pure PCK command (without address header) + */ + void queueDirectly(LcnAddr addr, boolean wantsAck, byte[] data) { + if (!addr.isGroup() && wantsAck) { + this.updateModuleData((LcnAddrMod) addr).queuePckCommandWithAck(data, this, this.settings.getTimeout(), + System.nanoTime()); + } else { + this.queueAndSend(new SendDataPck(addr, false, data)); + } + } + + /** + * Enqueues a raw PCK command and triggers the socket to start sending, if it does not already. Does not take care + * of any Acks. + * + * @param data raw PCK command + */ + synchronized void queueAndSend(SendData data) { + this.sendQueue.add(data); + + triggerWriteToSocket(); + } + + /** + * Enqueues a PCK command to the offline queue. Data will be sent when the Connection state will enter + * {@link ConnectionStateConnected}. + * + * @param addr LCN module address + * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing + * @param data the pure PCK command (without address header) + */ + void queueOffline(LcnAddr addr, boolean wantsAck, byte[] data) { + offlineSendQueue.add(new PckQueueItem(addr, wantsAck, data)); + } + + /** + * Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific + * time if the Connection is not ready. If an Ack is requested, the PCK command is automatically + * re-sent, if the module did not answer in the expected time. + * + * @param addr LCN module address + * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing + * @param pck the pure PCK command (without address header) + */ + public void queue(LcnAddr addr, boolean wantsAck, String pck) { + this.queue(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING)); + } + + /** + * Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific + * time if the Connection is not ready. If an Ack is requested, the PCK command is automatically + * re-sent, if the module did not answer in the expected time. + * + * @param addr LCN module address + * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing + * @param pck the pure PCK command (without address header) + */ + public void queue(LcnAddr addr, boolean wantsAck, byte[] pck) { + connectionStateMachine.queue(addr, wantsAck, pck); + } + + /** + * Process the offline PCK command queue. Does only send recently enqueued PCK commands, the rest is discarded. + */ + void sendOfflineQueue() { + List allItems = new ArrayList<>(offlineSendQueue.size()); + offlineSendQueue.drainTo(allItems); + + allItems.forEach(item -> { + // only send messages that were enqueued recently, discard older messages + long timeout = settings.getTimeout(); + if (item.getEnqueued().isAfter(Instant.now().minus(timeout * 4, ChronoUnit.MILLIS))) { + queueDirectly(item.getAddr(), item.isWantsAck(), item.getData()); + } + }); + } + + /** + * Gets the Connection's callback. + * + * @return the callback + */ + public ConnectionCallback getCallback() { + return callback; + } + + /** + * Sets the SocketChannel of this Connection + * + * @param channel the new Channel + */ + public void setSocketChannel(AsynchronousSocketChannel channel) { + this.channel = channel; + } + + /** + * Gets the SocketChannel of the Connection. + * + * @returnthe socket channel + */ + @Nullable + public Channel getSocketChannel() { + return channel; + } + + /** + * Gets the local segment ID. When no segments are used, the local segment ID is 0. + * + * @return the local segment ID + */ + public int getLocalSegId() { + return localSegId; + } + + /** + * Runs the periodic updates on all ModInfos. + */ + public void updateModInfos() { + synchronized (modData) { + modData.values().forEach(i -> i.update(this, settings.getTimeout(), System.nanoTime())); + } + } + + /** + * Removes an LCN module from the ModData list. + * + * @param addr the module's address to be removed + */ + public void removeLcnModule(LcnAddr addr) { + modData.remove(addr); + } + + /** + * Invoked when this Connection shall be shut-down finally. + */ + public void shutdown() { + connectionStateMachine.shutdownFinally(); + } + + /** + * Sends a broadcast to all LCN modules with a reuqest to respond with an Ack. + */ + public void sendModuleDiscoveryCommand() { + queueAndSend(new SendDataPck(new LcnAddrGrp(BROADCAST_SEGMENT_ID, BROADCAST_MODULE_ID), true, + PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING))); + queueAndSend(new SendDataPck(new LcnAddrGrp(0, BROADCAST_MODULE_ID), true, + PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING))); + } + + /** + * Requests the serial number and the firmware version of the given LCN module. + * + * @param addr module's address + */ + public void sendSerialNumberRequest(LcnAddrMod addr) { + queueDirectly(addr, false, PckGenerator.requestSn()); + } + + /** + * Requests theprogrammed name of the given LCN module. + * + * @param addr module's address + */ + public void sendModuleNameRequest(LcnAddrMod addr) { + queueDirectly(addr, false, PckGenerator.requestModuleName(0)); + queueDirectly(addr, false, PckGenerator.requestModuleName(1)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java new file mode 100644 index 0000000000000..0a2b116250453 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java @@ -0,0 +1,44 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Handles events from the connection to the LCN-PCK gateway. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public interface ConnectionCallback { + /** + * Invoked when the Connection to the PCK gateway is established and the LCN bus is connected to the PCK gateway. + */ + void onOnline(); + + /** + * Invoked when the Connection to the PCK gateway has been closed or when the LCN bus is disconnected from the PCK + * gateway. + * + * @param errorMessage the reason + */ + void onOffline(String errorMessage); + + /** + * Invoked when a PCK message has been reived from the PCK gateway. + * + * @param message the received message + */ + void onPckMessageReceived(String message); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java new file mode 100644 index 0000000000000..dae04ef589fe6 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java @@ -0,0 +1,158 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.common.LcnDefs; + +/** + * Settings for a connection to LCN-PCHK. + * + * @author Tobias Jüttner - Initial Contribution + */ +@NonNullByDefault +public class ConnectionSettings { + + /** Unique identifier for this connection. */ + private final String id; + + /** The user name for authentication. */ + private final String username; + + /** The password for authentication. */ + private final String password; + + /** The TCP/IP address or IP of the connection. */ + private final String address; + + /** The TCP/IP port of the connection. */ + private final int port; + + /** The dimming mode to use. */ + private final LcnDefs.OutputPortDimMode dimMode; + + /** The status-messages mode to use. */ + private final LcnDefs.OutputPortStatusMode statusMode; + + /** Timeout for requests. */ + private final long timeoutMSec; + + /** + * Constructor. + * + * @param id the connnection's unique identifier + * @param address the connection's TCP/IP address or IP + * @param port the connection's TCP/IP port + * @param username the user name for authentication + * @param password the password for authentication + * @param dimMode the dimming mode + * @param statusMode the status-messages mode + * @param timeout the request timeout + */ + public ConnectionSettings(String id, String address, int port, String username, String password, + LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode, int timeout) { + this.id = id; + this.address = address; + this.port = port; + this.username = username; + this.password = password; + this.dimMode = dimMode; + this.statusMode = statusMode; + this.timeoutMSec = timeout; + } + + /** + * Gets the unique identifier for the connection. + * + * @return the unique identifier + */ + public String getId() { + return this.id; + } + + /** + * Gets the user name used for authentication. + * + * @return the user name + */ + public String getUsername() { + return this.username; + } + + /** + * Gets the password used for authentication. + * + * @return the password + */ + public String getPassword() { + return this.password; + } + + /** + * Gets the TCP/IP address or IP of the connection. + * + * @return the address or IP + */ + public String getAddress() { + return this.address; + } + + /** + * Gets the TCP/IP port of the connection. + * + * @return the port + */ + public int getPort() { + return this.port; + } + + /** + * Gets the dimming mode to use for the connection. + * + * @return the dimming mode + */ + public LcnDefs.OutputPortDimMode getDimMode() { + return this.dimMode; + } + + /** + * Gets the status-messages mode to use for the connection. + * + * @return the status-messages mode + */ + public LcnDefs.OutputPortStatusMode getStatusMode() { + return this.statusMode; + } + + /** + * Gets the request timeout. + * + * @return the timeout in milliseconds + */ + public long getTimeout() { + return this.timeoutMSec; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof ConnectionSettings)) { + return false; + } + ConnectionSettings other = (ConnectionSettings) o; + return this.id.equals(other.id) && this.address.equals(other.address) && this.port == other.port + && this.username.equals(other.username) && this.password.equals(other.password) + && this.dimMode == other.dimMode && this.statusMode == other.statusMode + && this.timeoutMSec == other.timeoutMSec; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java new file mode 100644 index 0000000000000..55acca37974b0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java @@ -0,0 +1,58 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.PckGenerator; + +/** + * This state is active when the connection to the LCN bus has been established successfully and data can be sent and + * retrieved. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateConnected extends AbstractConnectionState { + private static final int PING_INTERVAL_SEC = 60; + private int pingCounter; + + public ConnectionStateConnected(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + // send periodic keep-alives to keep the connection open + addTimer(getScheduler().scheduleWithFixedDelay( + () -> connection.queueDirectlyPlainText(PckGenerator.ping(++pingCounter)), PING_INTERVAL_SEC, + PING_INTERVAL_SEC, TimeUnit.SECONDS)); + + // run ModInfo.update() for every LCN module + addTimer(getScheduler().scheduleWithFixedDelay(connection::updateModInfos, 0, 1, TimeUnit.SECONDS)); + + connection.sendOfflineQueue(); + } + + @Override + public void queue(LcnAddr addr, boolean wantsAck, byte[] data) { + connection.queueDirectly(addr, wantsAck, data); + } + + @Override + public void onPckMessageReceived(String data) { + parseLcnBusDiconnectMessage(data); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java new file mode 100644 index 0000000000000..35f54d8b22795 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java @@ -0,0 +1,97 @@ +/** + * 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.lcn.internal.connection; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.StandardSocketOptions; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This state is active during the socket creation, host name resolving and waiting for the TCP connection to become + * established. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateConnecting extends AbstractConnectionState { + private final Logger logger = LoggerFactory.getLogger(ConnectionStateConnecting.class); + + public ConnectionStateConnecting(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + connection.clearRuntimeData(); + + logger.debug("Connecting to {}:{} ...", connection.getSettings().getAddress(), + connection.getSettings().getPort()); + + try { + // Open Channel by using the system-wide default AynchronousChannelGroup. + // So, Threads are used or re-used on demand by the JVM. + AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(); + // Do not wait until some buffer is filled, send PCK commands immediately + channel.setOption(StandardSocketOptions.TCP_NODELAY, true); + connection.setSocketChannel(channel); + + InetSocketAddress address = new InetSocketAddress(connection.getSettings().getAddress(), + connection.getSettings().getPort()); + + if (address.isUnresolved()) { + throw new LcnException("Could not resolve hostname"); + } + + channel.connect(address, null, new CompletionHandler<@Nullable Void, @Nullable Void>() { + @Override + public void completed(@Nullable Void result, @Nullable Void attachment) { + connection.readAndProcess(); + nextState(ConnectionStateSendUsername::new); + } + + @Override + public void failed(@Nullable Throwable e, @Nullable Void attachment) { + handleConnectionFailure(e); + } + }); + } catch (IOException | LcnException e) { + handleConnectionFailure(e); + } + } + + private void handleConnectionFailure(@Nullable Throwable e) { + String message; + if (e != null) { + logger.warn("Could not connect to {}:{}: {}", connection.getSettings().getAddress(), + connection.getSettings().getPort(), e.getMessage()); + message = e.getMessage(); + } else { + message = ""; + } + connection.getCallback().onOffline(message); + context.handleConnectionFailed(e); + } + + @Override + public void onPckMessageReceived(String data) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java new file mode 100644 index 0000000000000..e6d05cba3ff49 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java @@ -0,0 +1,45 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This state is active when the connection failed. A grace period is enforced to prevent fast cycling through the + * states. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateGracePeriodBeforeReconnect extends AbstractConnectionState { + private static final int RECONNECT_GRACE_PERIOD_SEC = 5; + + public ConnectionStateGracePeriodBeforeReconnect(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + closeSocketChannel(); + + addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), RECONNECT_GRACE_PERIOD_SEC, + TimeUnit.SECONDS)); + } + + @Override + public void onPckMessageReceived(String data) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java new file mode 100644 index 0000000000000..a4c3790ee96a2 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java @@ -0,0 +1,37 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This is the initial state of the {@link ConnectionStateMachine}. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateInit extends AbstractConnectionState { + public ConnectionStateInit(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + nextState(ConnectionStateConnecting::new); + } + + @Override + public void onPckMessageReceived(String data) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java new file mode 100644 index 0000000000000..9e83f7ffeaa3b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java @@ -0,0 +1,107 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.ScheduledExecutorService; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.common.LcnAddr; + +/** + * Implements a state machine for managing the connection to the LCN-PCK gateway. Setting states is thread-safe. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateMachine extends AbstractStateMachine { + private final Connection connection; + final ScheduledExecutorService scheduler; + + public ConnectionStateMachine(Connection connection, ScheduledExecutorService scheduler) { + this.connection = connection; + this.scheduler = scheduler; + + setState(ConnectionStateInit::new); + } + + /** + * Gets the framework's scheduler. + * + * @return the scheduler + */ + protected ScheduledExecutorService getScheduler() { + return scheduler; + } + + /** + * Gets the PCHK Connection object. + * + * @return the connection + */ + public Connection getConnection() { + return connection; + } + + /** + * Enqueues a PCK command. Implementation is state dependent. + * + * @param addr the destination address + * @param wantsAck true, if the module shall respond with an Ack + * @param data the data + */ + public void queue(LcnAddr addr, boolean wantsAck, byte[] data) { + AbstractConnectionState localState = state; + if (localState != null) { + localState.queue(addr, wantsAck, data); + } + } + + /** + * Invoked by any state, if the connection fails. + * + * @param e the cause + */ + public void handleConnectionFailed(@Nullable Throwable e) { + if (!(state instanceof ConnectionStateShutdown)) { + if (e != null) { + connection.getCallback().onOffline(e.getMessage()); + } else { + connection.getCallback().onOffline(""); + } + setState(ConnectionStateGracePeriodBeforeReconnect::new); + } + } + + /** + * Processes a received PCK message by passing it to the current State. + * + * @param data the PCK message + */ + public void onInputReceived(String data) { + AbstractConnectionState localState = state; + if (localState != null) { + localState.onPckMessageReceived(data); + } + } + + /** + * Shuts the StateMachine down finally. A shut-down StateMachine cannot be re-used. + */ + public void shutdownFinally() { + AbstractConnectionState localState = state; + if (localState != null) { + localState.shutdownFinally(); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java new file mode 100644 index 0000000000000..0942f777d85b9 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java @@ -0,0 +1,82 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddrGrp; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This state discovers the LCN segment couplers. + * + * After the authorization against the LCN-PCK gateway was successful, the LCN segment couplers are discovery, to + * retrieve the segment ID of the local segment. When no segment couplers were found, a timeout sets the local segment + * ID to 0. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateSegmentScan extends AbstractConnectionState { + private final Logger logger = LoggerFactory.getLogger(ConnectionStateSegmentScan.class); + public static final Pattern PATTERN_SK_RESPONSE = Pattern + .compile("=M(?\\d{3})(?\\d{3})\\.SK(?\\d+)"); + private final RequestStatus statusSegmentScan = new RequestStatus(-1, 3, "Segment Scan"); + + public ConnectionStateSegmentScan(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + statusSegmentScan.refresh(); + addTimer(getScheduler().scheduleWithFixedDelay(this::update, 0, 500, TimeUnit.MILLISECONDS)); + } + + private void update() { + long currTime = System.nanoTime(); + try { + if (statusSegmentScan.shouldSendNextRequest(connection.getSettings().getTimeout(), currTime)) { + connection.queueDirectly(new LcnAddrGrp(3, 3), false, PckGenerator.segmentCouplerScan()); + statusSegmentScan.onRequestSent(currTime); + } + } catch (LcnException e) { + // Give up. Probably no segments available. + connection.setLocalSegId(0); + logger.debug("No segment couplers detected"); + nextState(ConnectionStateConnected::new); + } + } + + @Override + public void onPckMessageReceived(String data) { + Matcher matcher = PATTERN_SK_RESPONSE.matcher(data); + + if (matcher.matches()) { + // any segment coupler answered + if (Integer.parseInt(matcher.group("segId")) == 0) { + // local segment coupler answered + connection.setLocalSegId(Integer.parseInt(matcher.group("id"))); + logger.debug("Local segment ID is {}", connection.getLocalSegId()); + nextState(ConnectionStateConnected::new); + } + } + parseLcnBusDiconnectMessage(data); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java new file mode 100644 index 0000000000000..bfcf89f37a766 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java @@ -0,0 +1,41 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.PckGenerator; + +/** + * Sets the dimming mode range (0-50 or 0-200) in the LCN-PCK for this connection, as configured by the user. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateSendDimMode extends AbstractConnectionState { + public ConnectionStateSendDimMode(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + connection.queueDirectlyPlainText(PckGenerator.setOperationMode(connection.getSettings().getDimMode(), + connection.getSettings().getStatusMode())); + + nextState(ConnectionStateSegmentScan::new); + } + + @Override + public void onPckMessageReceived(String data) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java new file mode 100644 index 0000000000000..01ae13bd3fe72 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java @@ -0,0 +1,41 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnDefs; + +/** + * This state sends the password during the authentication with the LCN-PCK gateway. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateSendPassword extends AbstractConnectionStateSendCredentials { + public ConnectionStateSendPassword(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + startTimeoutTimer(); + } + + @Override + public void onPckMessageReceived(String data) { + if (data.equals(LcnDefs.AUTH_PASSWORD)) { + connection.queueDirectlyPlainText(connection.getSettings().getPassword()); + nextState(ConnectionStateWaitForLcnBusConnected::new); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java new file mode 100644 index 0000000000000..a04f1d341a3c3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java @@ -0,0 +1,41 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnDefs; + +/** + * This state sends the username during the authentication with the LCN-PCK gateway. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateSendUsername extends AbstractConnectionStateSendCredentials { + public ConnectionStateSendUsername(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + startTimeoutTimer(); + } + + @Override + public void onPckMessageReceived(String data) { + if (data.equals(LcnDefs.AUTH_USERNAME)) { + connection.queueDirectlyPlainText(connection.getSettings().getUsername()); + nextState(ConnectionStateSendPassword::new); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java new file mode 100644 index 0000000000000..67c7ff2100e25 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java @@ -0,0 +1,45 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddr; + +/** + * This state is entered when the connection shall be shut-down finally. This happens when Thing.dispose() is called. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateShutdown extends AbstractConnectionState { + public ConnectionStateShutdown(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + closeSocketChannel(); + + // end state + } + + @Override + public void queue(LcnAddr addr, boolean wantsAck, byte[] data) { + // nothing + } + + @Override + public void onPckMessageReceived(String data) { + // nothing + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java new file mode 100644 index 0000000000000..8882042cebed4 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java @@ -0,0 +1,68 @@ +/** + * 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.lcn.internal.connection; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * This state waits for the status answer of the LCN-PCK gateway after connection establishment, rather the LCN bus is + * connected. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateWaitForLcnBusConnected extends AbstractConnectionState { + private @Nullable ScheduledFuture legacyTimer; + + public ConnectionStateWaitForLcnBusConnected(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + // Legacy support for LCN-PCHK 2.2 and earlier: + // There was no explicit "LCN connected" notification after successful authentication. + // Only "LCN disconnected" would be reported immediately. That means "LCN connected" used to be the default. + ScheduledFuture localLegacyTimer = legacyTimer = getScheduler().schedule(() -> { + connection.getCallback().onOnline(); + nextState(ConnectionStateSendDimMode::new); + }, connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS); + addTimer(localLegacyTimer); + } + + @Override + public void onPckMessageReceived(String data) { + ScheduledFuture localLegacyTimer = legacyTimer; + if (data.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) { + if (localLegacyTimer != null) { + localLegacyTimer.cancel(true); + } + connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE"); + } else if (data.equals(LcnDefs.LCNCONNSTATE_CONNECTED)) { + if (localLegacyTimer != null) { + localLegacyTimer.cancel(true); + } + connection.getCallback().onOnline(); + nextState(ConnectionStateSendDimMode::new); + } else if (data.equals(LcnDefs.INSUFFICIENT_LICENSES)) { + context.handleConnectionFailed( + new LcnException("LCN-PCHK/PKE has not enough licenses to handle this connection")); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java new file mode 100644 index 0000000000000..8174ada6eecb7 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java @@ -0,0 +1,33 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This state is entered when the LCN-PCK gateway sent a message, that the connection to the LCN bus was lost. This can + * happen if the user plugs the USB cable to the PC coupler. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class ConnectionStateWaitForLcnBusConnectedAfterDisconnected extends ConnectionStateWaitForLcnBusConnected { + public ConnectionStateWaitForLcnBusConnectedAfterDisconnected(ConnectionStateMachine context) { + super(context); + } + + @Override + public void startWorking() { + // nothing, don't start legacy timer + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java new file mode 100644 index 0000000000000..a7799240c2e3f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java @@ -0,0 +1,500 @@ +/** + * 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.lcn.internal.connection; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.common.VariableValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Holds data of an LCN module. + *
      + *
    • Stores the module's firmware version (if requested) + *
    • Manages the scheduling of status-requests + *
    • Manages the scheduling of acknowledged commands + *
    + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public class ModInfo { + private final Logger logger = LoggerFactory.getLogger(ModInfo.class); + /** Total number of request to sent before going into failed-state. */ + private static final int NUM_TRIES = 3; + + /** Poll interval for status values that automatically send their values on change. */ + private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000; + + /** Poll interval for status values that do not send their values on change (always polled). */ + private static final int MAX_STATUS_POLLED_VALUEAGE_MSEC = 30000; + + /** Status request delay after a command has been send which potentially changed that status. */ + private static final int STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC = 2000; + + /** The LCN module's address. */ + private final LcnAddr addr; + + /** Firmware date of the LCN module. -1 means "unknown". */ + private int firmwareVersion = -1; + + /** Firmware version request status. */ + private final RequestStatus requestFirmwareVersion = new RequestStatus(-1, NUM_TRIES, "Firmware Version"); + + /** Output-port request status (0..3). */ + private final RequestStatus[] requestStatusOutputs = new RequestStatus[LcnChannelGroup.OUTPUT.getCount()]; + + /** Relays request status (all 8). */ + private final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES, + "Relays"); + + /** Binary-sensors request status (all 8). */ + private final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, + NUM_TRIES, "Binary Sensors"); + + /** + * Variables request status. + * Lazy initialization: Will be filled once the firmware version is known. + */ + private final Map requestStatusVars = new HashMap<>(); + + /** + * Caches the values of the variables, needed for changing the values. + */ + private final Map variableValue = new HashMap<>(); + + /** LEDs and logic-operations request status (all 12+4). */ + private final RequestStatus requestStatusLedsAndLogicOps = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, + NUM_TRIES, "LEDs and Logic"); + + /** Key lock-states request status (all tables, A-D). */ + private final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES, + "Key Locks"); + + /** + * Holds the last LCN variable requested whose response will not contain the variable's type. + * {@link Variable#UNKNOWN} means there is currently no such request. + */ + private Variable lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN; + + /** + * List of queued PCK commands to be acknowledged by the LCN module. + * Commands are always without address header. + * Note that the first one might currently be "in progress". + */ + private final Queue pckCommandsWithAck = new ConcurrentLinkedQueue<>(); + + /** Status data for the currently processed {@link PckCommandWithAck}. */ + private final RequestStatus requestCurrentPckCommandWithAck = new RequestStatus(-1, NUM_TRIES, "Commands with Ack"); + + /** + * Constructor. + * + * @param addr the module's address + */ + public ModInfo(LcnAddr addr) { + this.addr = addr; + for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) { + requestStatusOutputs[i] = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES, + "Output " + (i + 1)); + } + + for (Variable var : Variable.values()) { + if (var != Variable.UNKNOWN) { + this.requestStatusVars.put(var, new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES, + var.getType() + " " + (var.getNumber() + 1))); + } + } + } + + /** + * Gets the last requested variable whose response will not contain the variables type. + * + * @return the "typeless" variable + */ + public Variable getLastRequestedVarWithoutTypeInResponse() { + return this.lastRequestedVarWithoutTypeInResponse; + } + + /** + * Sets the last requested variable whose response will not contain the variables type. + * + * @param var the "typeless" variable + */ + public void setLastRequestedVarWithoutTypeInResponse(Variable var) { + this.lastRequestedVarWithoutTypeInResponse = var; + } + + /** + * Queues a PCK command to be sent. + * It will request an acknowledge from the LCN module on receipt. + * If there is no response within the request timeout, the command is retried. + * + * @param data the PCK command to send (without address header) + * @param timeoutMSec the time to wait for a response before retrying a request + * @param currTime the current time stamp + */ + public void queuePckCommandWithAck(byte[] data, Connection conn, long timeoutMSec, long currTime) { + this.pckCommandsWithAck.add(data); + // Try to process the new acknowledged command. Will do nothing if another one is still in progress. + this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); + } + + /** + * Called whenever an acknowledge is received from the LCN module. + * + * @param code the LCN internal code. -1 means "positive" acknowledge + * @param timeoutMSec the time to wait for a response before retrying a request + * @param currTime the current time stamp + */ + public void onAck(int code, Connection conn, long timeoutMSec, long currTime) { + if (this.requestCurrentPckCommandWithAck.isActive()) { // Check if we wait for an ack. + this.pckCommandsWithAck.poll(); + this.requestCurrentPckCommandWithAck.reset(); + // Try to process next acknowledged command + this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); + } + } + + /** + * Sends the next acknowledged command from the queue. + * + * @param conn the {@link Connection} belonging to this {@link ModInfo} + * @param timeoutMSec the time to wait for a response before retrying a request + * @param currTime the current time stamp + * @return true if a new command was sent + * @throws LcnException when a command response timed out + */ + private boolean tryProcessNextCommandWithAck(Connection conn, long timeoutMSec, long currTime) { + // Use the chance to remove a failed command first + if (this.requestCurrentPckCommandWithAck.isFailed(timeoutMSec, currTime)) { + byte[] failedCommand = this.pckCommandsWithAck.poll(); + this.requestCurrentPckCommandWithAck.reset(); + + if (failedCommand != null) { + logger.warn("{}: Module did not respond to command: {}", addr, + new String(failedCommand, LcnDefs.LCN_ENCODING)); + } + } + // Peek new command + if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrentPckCommandWithAck.isActive()) { + this.requestCurrentPckCommandWithAck.nextRequestIn(0, currTime); + } + byte[] command = this.pckCommandsWithAck.peek(); + if (command == null) { + return false; + } + try { + if (requestCurrentPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime)) { + conn.queueAndSend(new SendDataPck(addr, true, command)); + this.requestCurrentPckCommandWithAck.onRequestSent(currTime); + } + } catch (LcnException e) { + logger.warn("{}: Could not send command: {}: {}", addr, new String(command, LcnDefs.LCN_ENCODING), + e.getMessage()); + } + return true; + } + + /** + * Triggers a request to retrieve the firmware version of the LCN module, if it is not known, yet. + */ + public void requestFirmwareVersion() { + if (firmwareVersion == -1) { + requestFirmwareVersion.refresh(); + } + } + + /** + * Used to check if the module has the measurement processing firmware (since Feb. 2013). + * + * @return if the module has at least 4 threshold registers and 12 variables + */ + public boolean hasExtendedMeasurementProcessing() { + if (firmwareVersion == -1) { + logger.warn("LCN module firmware version unknown"); + return false; + } + return firmwareVersion >= LcnBindingConstants.FIRMWARE_2013; + } + + private boolean update(Connection conn, long timeoutMSec, long currTime, RequestStatus requestStatus, String pck) + throws LcnException { + if (requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) { + conn.queue(this.addr, false, pck); + requestStatus.onRequestSent(currTime); + return true; + } + return false; + } + + /** + * Keeps the request logic active. + * Must be called periodically. + * + * @param conn the {@link Connection} belonging to this {@link ModInfo} + * @param timeoutMSec the time to wait for a response before retrying a request + * @param currTime the current time stamp + */ + void update(Connection conn, long timeoutMSec, long currTime) { + try { + if (update(conn, timeoutMSec, currTime, requestFirmwareVersion, PckGenerator.requestSn())) { + return; + } + + for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) { + if (update(conn, timeoutMSec, currTime, requestStatusOutputs[i], PckGenerator.requestOutputStatus(i))) { + return; + } + } + + if (update(conn, timeoutMSec, currTime, requestStatusRelays, PckGenerator.requestRelaysStatus())) { + return; + } + if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) { + return; + } + + // Variable requests + if (this.firmwareVersion != -1) { // Firmware version is required + // Use the chance to remove a failed "typeless variable" request + if (lastRequestedVarWithoutTypeInResponse != Variable.UNKNOWN) { + RequestStatus requestStatus = requestStatusVars.get(lastRequestedVarWithoutTypeInResponse); + if (requestStatus != null && requestStatus.isTimeout(timeoutMSec, currTime)) { + lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN; + } + } + // Variables + for (Map.Entry kv : this.requestStatusVars.entrySet()) { + RequestStatus requestStatus = kv.getValue(); + if (requestStatus != null && requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) { + // Detect if we can send immediately or if we have to wait for a "typeless" request first + boolean hasTypeInResponse = kv.getKey().hasTypeInResponse(this.firmwareVersion); + if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == Variable.UNKNOWN) { + try { + conn.queue(this.addr, false, + PckGenerator.requestVarStatus(kv.getKey(), this.firmwareVersion)); + requestStatus.onRequestSent(currTime); + if (!hasTypeInResponse) { + this.lastRequestedVarWithoutTypeInResponse = kv.getKey(); + } + return; + } catch (LcnException ex) { + requestStatus.reset(); + } + } + } + } + } + + if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps, + PckGenerator.requestLedsAndLogicOpsStatus())) { + return; + } + + if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) { + return; + } + + // Try to send next acknowledged command. Will also detect failed ones. + this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); + } catch (LcnException e) { + logger.warn("{}: Failed to receive status message: {}", addr, e.getMessage()); + } + } + + /** + * Gets the LCN module's firmware date. + * + * @return the date + */ + public int getFirmwareVersion() { + return this.firmwareVersion; + } + + /** + * Sets the LCN module's firmware date. + * + * @param firmwareVersion the date + */ + public void setFirmwareVersion(int firmwareVersion) { + this.firmwareVersion = firmwareVersion; + + requestFirmwareVersion.onResponseReceived(); + + // increase poll interval, if the LCN module sends status updates of a variable event-based + requestStatusVars.entrySet().stream().filter(e -> e.getKey().isEventBased(firmwareVersion)).forEach(e -> { + RequestStatus value = e.getValue(); + if (value != null) { + value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC); + } + }); + } + + /** + * Updates the variable value cache. + * + * @param variable the variable to update + * @param value the new value + */ + public void updateVariableValue(Variable variable, VariableValue value) { + variableValue.put(variable, value); + } + + /** + * Gets the current value of a variable from the cache. + * + * @param variable the variable to retrieve the value for + * @return the value of the variable + * @throws LcnException when the variable is not in the cache + */ + public long getVariableValue(Variable variable) throws LcnException { + return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues())) + .orElseThrow(() -> new LcnException("Current variable value unknown")); + } + + /** + * Requests the current value of all dimmer outputs. + */ + public void refreshAllOutputs() { + Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh); + } + + /** + * Requests the current value of the given dimmer output. + * + * @param number 0..3 + */ + public void refreshOutput(int number) { + requestStatusOutputs[number].refresh(); + } + + /** + * Requests the current value of all relays. + */ + public void refreshRelays() { + requestStatusRelays.refresh(); + } + + /** + * Requests the current value of all binary sensor. + */ + public void refreshBinarySensors() { + requestStatusBinSensors.refresh(); + } + + /** + * Requests the current value of the given variable. + * + * @param variable the variable to request + */ + public void refreshVariable(Variable variable) { + RequestStatus requestStatus = requestStatusVars.get(variable); + if (requestStatus != null) { + requestStatus.refresh(); + } + } + + /** + * Requests the current value of all LEDs and logic operations. + */ + public void refreshLedsAndLogic() { + requestStatusLedsAndLogicOps.refresh(); + } + + /** + * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB. + */ + public void refreshStatusLedsAnLogicAfterChange() { + requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime()); + } + + /** + * Requests the current locking states of all keys. + */ + public void refreshStatusLockedKeys() { + requestStatusLockedKeys.refresh(); + } + + /** + * Requests the current locking states of all keys, after a lock state has been changed by openHAB. + */ + public void refreshStatusStatusLockedKeysAfterChange() { + requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime()); + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output + * + * @param outputId 0..3 + */ + public void onOutputResponseReceived(int outputId) { + requestStatusOutputs[outputId].onResponseReceived(); + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: Relay + */ + public void onRelayResponseReceived() { + requestStatusRelays.onResponseReceived(); + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor + */ + public void onBinarySensorsResponseReceived() { + requestStatusBinSensors.onResponseReceived(); + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: Variable + * + * @param variable the received variable type + */ + public void onVariableResponseReceived(Variable variable) { + RequestStatus requestStatus = requestStatusVars.get(variable); + if (requestStatus != null) { + requestStatus.onResponseReceived(); + } + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic + */ + public void onLedsAndLogicResponseReceived() { + requestStatusLedsAndLogicOps.onResponseReceived(); + } + + /** + * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state + */ + public void onLockedKeysResponseReceived() { + requestStatusLockedKeys.onResponseReceived(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java new file mode 100644 index 0000000000000..f65ac8fa9ce9e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java @@ -0,0 +1,74 @@ +/** + * 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.lcn.internal.connection; + +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddr; + +/** + * Holds data of one PCK command with the target address and the date when the item has been enqueued. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PckQueueItem { + private final Instant enqueued; + private final LcnAddr addr; + private final boolean wantsAck; + private final byte[] data; + + public PckQueueItem(LcnAddr addr, boolean wantsAck, byte[] data) { + this.enqueued = Instant.now(); + this.addr = addr; + this.wantsAck = wantsAck; + this.data = data; + } + + /** + * Gets the time when this message has been enqueued. + * + * @return the Instant + */ + public Instant getEnqueued() { + return enqueued; + } + + /** + * Gets the address of the destination LCN module. + * + * @return the address + */ + public LcnAddr getAddr() { + return addr; + } + + /** + * Checks whether an Ack is requested. + * + * @return true, if an Ack is requested + */ + public boolean isWantsAck() { + return wantsAck; + } + + /** + * Gets the raw PCK message to be sent. + * + * @return message as ByteBuffer + */ + public byte[] getData() { + return data; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java new file mode 100644 index 0000000000000..e5b95e876bda8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java @@ -0,0 +1,195 @@ +/** + * 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.lcn.internal.connection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages timeout and retry logic for an LCN request. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public class RequestStatus { + private final Logger logger = LoggerFactory.getLogger(RequestStatus.class); + /** Interval for forced updates. -1 if not used. */ + private volatile long maxAgeMSec; + + /** Tells how often a request will be sent if no response was received. */ + private final int numTries; + + /** true if request logic is activated. */ + private volatile boolean isActive; + + /** The time the current request was sent out or 0. */ + private volatile long currRequestTimeStamp; + + /** The time stamp of the next scheduled request or 0. */ + private volatile long nextRequestTimeStamp; + + /** Number of retries left until the request is marked as failed. */ + private volatile int numRetriesLeft; + private final String label; + + /** + * Constructor. + * + * @param maxAgeMSec the forced-updates interval (-1 if not used) + * @param numTries the maximum number of tries until the request is marked as failed + */ + RequestStatus(long maxAgeMSec, int numTries, String label) { + this.maxAgeMSec = maxAgeMSec; + this.numTries = numTries; + this.label = label; + this.reset(); + } + + /** Resets the runtime data to the initial states. */ + public synchronized void reset() { + this.isActive = false; + this.currRequestTimeStamp = 0; + this.nextRequestTimeStamp = 0; + this.numRetriesLeft = 0; + } + + /** + * Checks whether the request logic is active. + * + * @return true if active + */ + public boolean isActive() { + return this.isActive; + } + + /** + * Checks whether a request is waiting for a response. + * + * @return true if waiting for a response + */ + boolean isPending() { + return this.currRequestTimeStamp != 0; + } + + /** + * Checks whether the request is active and ran into timeout while waiting for a response. + * + * @param timeoutMSec the timeout in milliseconds + * @param currTime the current time stamp + * @return true if request timed out + */ + synchronized boolean isTimeout(long timeoutMSec, long currTime) { + return this.isPending() && currTime - this.currRequestTimeStamp >= timeoutMSec * 1000000L; + } + + /** + * Checks for failed requests (active and out of retries). + * + * @param timeoutMSec the timeout in milliseconds + * @param currTime the current time stamp + * @return true if no response was received and no retries are left + */ + synchronized boolean isFailed(long timeoutMSec, long currTime) { + return this.isTimeout(timeoutMSec, currTime) && this.numRetriesLeft == 0; + } + + /** + * Schedules the next request. + * + * @param delayMSec the delay in milliseconds + * @param currTime the current time stamp + */ + public synchronized void nextRequestIn(long delayMSec, long currTime) { + this.isActive = true; + this.nextRequestTimeStamp = currTime + delayMSec * 1000000L; + } + + /** + * Schedules a request to retrieve the current value. + */ + public synchronized void refresh() { + nextRequestIn(0, System.nanoTime()); + this.numRetriesLeft = this.numTries; + } + + /** + * Checks whether sending a new request is required (should be called periodically). + * + * @param timeoutMSec the time to wait for a response before retrying the request + * @param currTime the current time stamp + * @return true to indicate a new request should be sent + * @throws LcnException when a status request timed out + */ + synchronized boolean shouldSendNextRequest(long timeoutMSec, long currTime) throws LcnException { + if (this.isActive) { + if (this.nextRequestTimeStamp != 0 && currTime >= this.nextRequestTimeStamp) { + return true; + } + // Retry of current request (after no response was received) + if (this.isTimeout(timeoutMSec, currTime)) { + if (this.numRetriesLeft > 0) { + return true; + } else if (isPending()) { + currRequestTimeStamp = 0; + throw new LcnException(label + ": Failed finally after " + numTries + " tries"); + } + } + } + return false; + } + + /** + * Must be called right after a new request has been sent. + * Must be activated first. + * + * @param currTime the current time stamp + */ + public synchronized void onRequestSent(long currTime) { + if (!this.isActive) { + logger.warn("Tried to send a request which is not active"); + } + // Updates retry counter + if (this.currRequestTimeStamp == 0) { + this.numRetriesLeft = this.numTries - 1; + } else if (this.numRetriesLeft > 0) { // Should not happen if used correctly + --this.numRetriesLeft; + } + // Mark request as pending + this.currRequestTimeStamp = currTime; + // Schedule next request + if (this.maxAgeMSec != -1) { + this.nextRequestIn(this.maxAgeMSec, currTime); + } else { + this.nextRequestTimeStamp = 0; + } + } + + /** Must be called when a response (requested or not) has been received. */ + public synchronized void onResponseReceived() { + if (this.isActive) { + this.currRequestTimeStamp = 0; // Mark request (if any) as successful + } + } + + /** + * Sets the timeout of this RequestStatus. + * + * @param maxAgeMSec the timeout in ms + */ + public void setMaxAgeMSec(long maxAgeMSec) { + this.maxAgeMSec = maxAgeMSec; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java new file mode 100644 index 0000000000000..a271f6a7902c8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java @@ -0,0 +1,38 @@ +/** + * 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.lcn.internal.connection; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Base class for a packet to be send to LCN-PCHK. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +public abstract class SendData { + /** + * Writes the packet's data into the given buffer. + * Called right before the packet is actually sent to LCN-PCHK. + * + * @param buffer the target buffer + * @param localSegId the local segment id + * @return true if everything was set-up correctly and data was written + * @throws IOException if an I/O error occurs + */ + abstract boolean write(OutputStream buffer, int localSegId) throws IOException; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java new file mode 100644 index 0000000000000..f04b9688d8489 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java @@ -0,0 +1,77 @@ +/** + * 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.lcn.internal.connection; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.BufferOverflowException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.PckGenerator; + +/** + * A PCK command to be send to LCN-PCHK. + * It is already encoded as bytes to allow different text-encodings (ANSI, UTF-8). + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +class SendDataPck extends SendData { + /** The target LCN address. */ + private final LcnAddr addr; + + /** true to acknowledge the command on receipt. */ + private final boolean wantsAck; + + /** PCK command (without address header) encoded as bytes. */ + private final byte[] data; + + /** + * Constructor. + * + * @param addr the target LCN address + * @param wantsAck true to claim receipt + * @param data the PCK command encoded as bytes + */ + SendDataPck(LcnAddr addr, boolean wantsAck, byte[] data) { + this.addr = addr; + this.wantsAck = wantsAck; + this.data = data; + } + + /** + * Gets the PCK command. + * + * @return the PCK command encoded as bytes + */ + byte[] getData() { + return this.data; + } + + @Override + boolean write(OutputStream buffer, int localSegId) throws BufferOverflowException, IOException { + buffer.write(PckGenerator.generateAddressHeader(this.addr, localSegId == -1 ? 0 : localSegId, this.wantsAck) + .getBytes(LcnDefs.LCN_ENCODING)); + buffer.write(this.data); + buffer.write(PckGenerator.TERMINATION.getBytes(LcnDefs.LCN_ENCODING)); + return true; + } + + @Override + public String toString() { + return "Addr: " + addr + ": " + new String(data, 0, data.length, LcnDefs.LCN_ENCODING); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java new file mode 100644 index 0000000000000..65e31e8f2d903 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java @@ -0,0 +1,61 @@ +/** + * 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.lcn.internal.connection; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.PckGenerator; + +/** + * A plain text to be send to LCN-PCHK. + * + * @author Tobias Jüttner - Initial Contribution + * @author Fabian Wolter - Migration to OH2 + */ +@NonNullByDefault +class SendDataPlainText extends SendData { + /** The text. */ + private final String text; + + /** + * Constructor. + * + * @param text the text + */ + SendDataPlainText(String text) { + this.text = text; + } + + /** + * Gets the text. + * + * @return the text + */ + String getText() { + return this.text; + } + + @Override + boolean write(OutputStream buffer, int localSegId) throws IOException { + buffer.write((this.text + PckGenerator.TERMINATION).getBytes(LcnDefs.LCN_ENCODING)); + return true; + } + + @Override + public String toString() { + return text; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java new file mode 100644 index 0000000000000..456bbe847da48 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java @@ -0,0 +1,118 @@ +/** + * 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.lcn.internal.converter; + +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for all LCN variable value converters. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class Converter { + private final Logger logger = LoggerFactory.getLogger(Converter.class); + private @Nullable final Unit unit; + private final Function toHuman; + private final Function toNative; + + public Converter(@Nullable Unit unit, Function toHuman, Function toNative) { + this.unit = unit; + this.toHuman = toHuman; + this.toNative = toNative; + } + + /** + * Converts the given human readable value into the native LCN value. + * + * @param humanReadableValue the value to convert + * @return the native value + */ + protected long toNative(double humanReadableValue) { + return toNative.apply(humanReadableValue); + } + + /** + * Converts the given native LCN value into a human readable value. + * + * @param nativeValue the value to convert + * @return the human readable value + */ + protected double toHumanReadable(long nativeValue) { + return toHuman.apply(nativeValue); + } + + /** + * Converts a human readable value into LCN native value. + * + * @param humanReadable value to convert + * @return the native LCN value + */ + public DecimalType onCommandFromItem(double humanReadable) { + return new DecimalType(toNative(humanReadable)); + } + + /** + * Converts a human readable value into LCN native value. + * + * @param humanReadable value to convert + * @return the native LCN value + * @throws LcnException when the value could not be converted to the base unit + */ + public DecimalType onCommandFromItem(QuantityType quantityType) throws LcnException { + Unit localUnit = unit; + if (localUnit == null) { + return onCommandFromItem(quantityType.doubleValue()); + } + + QuantityType quantityInBaseUnit = quantityType.toUnit(localUnit); + + if (quantityInBaseUnit != null) { + return onCommandFromItem(quantityInBaseUnit.doubleValue()); + } else { + throw new LcnException(quantityType + ": Incompatible Channel unit configured: " + localUnit); + } + } + + /** + * Converts a state update from the Thing into a human readable unit. + * + * @param state from the Thing + * @return human readable State + */ + public State onStateUpdateFromHandler(State state) { + State result = state; + + if (state instanceof DecimalType) { + Unit localUnit = unit; + if (localUnit != null) { + result = QuantityType.valueOf(toHumanReadable(((DecimalType) state).longValue()), localUnit); + } + } else { + logger.warn("Unexpected state type: {}", state.getClass().getSimpleName()); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java new file mode 100644 index 0000000000000..3a8c5fff7aa04 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java @@ -0,0 +1,62 @@ +/** + * 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.lcn.internal.converter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; + +/** + * Holds all Converter objects. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class Converters { + public static final Converter TEMPERATURE; + public static final Converter LIGHT; + public static final Converter CO2; + public static final Converter CURRENT; + public static final Converter VOLTAGE; + public static final Converter ANGLE; + public static final Converter WINDSPEED; + public static final Converter IDENTITY; + + static { + TEMPERATURE = new Converter(SIUnits.CELSIUS, n -> (n - 1000) / 10d, h -> Math.round(h * 10) + 1000); + LIGHT = new Converter(SmartHomeUnits.LUX, Converters::lightToHumanReadable, Converters::lightToNative); + CO2 = new Converter(SmartHomeUnits.PARTS_PER_MILLION, n -> (double) n, Math::round); + CURRENT = new Converter(SmartHomeUnits.AMPERE, n -> n / 100d, h -> Math.round(h * 100)); + VOLTAGE = new Converter(SmartHomeUnits.VOLT, n -> n / 400d, h -> Math.round(h * 400)); + ANGLE = new Converter(SmartHomeUnits.DEGREE_ANGLE, n -> (n - 1000) / 10d, Converters::angleToNative); + WINDSPEED = new Converter(SmartHomeUnits.METRE_PER_SECOND, n -> n / 10d, h -> Math.round(h * 10)); + IDENTITY = new Converter(null, n -> (double) n, Math::round); + } + + private static long lightToNative(double value) { + return Math.round(Math.log(value) * 100); + } + + private static double lightToHumanReadable(long value) { + // Max. value hardware can deliver is 100klx. Apply hard limit, because higher native values lead to very big + // lux values. + if (value > lightToNative(100e3)) { + return Double.NaN; + } + return Math.exp(value / 100d); + } + + private static long angleToNative(double h) { + return (Math.round(h * 10) + 1000); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java new file mode 100644 index 0000000000000..b2b4989a668c8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java @@ -0,0 +1,55 @@ +/** + * 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.lcn.internal.converter; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for S0 counter value converters. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class S0Converter extends Converter { + private final Logger logger = LoggerFactory.getLogger(S0Converter.class); + protected double pulsesPerKwh; + + public S0Converter(@Nullable Object parameter) { + super(SmartHomeUnits.WATT, n -> 0d, h -> 0L); + + if (parameter == null) { + pulsesPerKwh = 1000; + logger.debug("Pulses per kWh not set. Assuming 1000 imp./kWh."); + } else if (parameter instanceof BigDecimal) { + pulsesPerKwh = ((BigDecimal) parameter).doubleValue(); + } else { + logger.warn("Could not parse 'pulses', unexpected type, should be float or integer: {}", parameter); + } + } + + @Override + public long toNative(double value) { + return Math.round(value * pulsesPerKwh / 1000); + } + + @Override + public double toHumanReadable(long value) { + return value / pulsesPerKwh * 1000; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java new file mode 100644 index 0000000000000..1cf6f92bd24f0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java @@ -0,0 +1,39 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.thoughtworks.xstream.annotations.XStreamConverter; +import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter; + +/** + * Used for deserializing the XML response of the LCN-PCHK discovery protocol. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" }) +public class ExtService { + private final int localPort; + @SuppressWarnings("unused") + private final String content = ""; + + public ExtService(int localPort) { + this.localPort = localPort; + } + + public int getLocalPort() { + return localPort; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java new file mode 100644 index 0000000000000..da2ec561faa8c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java @@ -0,0 +1,33 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Used for deserializing the XML response of the LCN-PCHK discovery protocol. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class ExtServices { + private final ExtService ExtService; + + public ExtServices(ExtService extService) { + ExtService = extService; + } + + public ExtService getExtService() { + return ExtService; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java new file mode 100644 index 0000000000000..be8bb3fbc0925 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java @@ -0,0 +1,161 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.StaxDriver; + +/** + * Discovers LCN-PCK gateways, such as LCN-PCHK. + * + * Scan approach: + * 1. Determines all local network interfaces + * 2. Send a multicast message on each interface to the PCHK multicast address 234.5.6.7 (not configurable by user). + * 3. Evaluate multicast responses of PCK gateways in the network + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.lcn") +public class LcnPchkDiscoveryService extends AbstractDiscoveryService { + private final Logger logger = LoggerFactory.getLogger(LcnPchkDiscoveryService.class); + private static final String HOSTNAME = "hostname"; + private static final String PORT = "port"; + private static final String MAC_ADDRESS = "macAddress"; + private static final String PCHK_DISCOVERY_MULTICAST_ADDRESS = "234.5.6.7"; + private static final int PCHK_DISCOVERY_PORT = 4220; + private static final int INTERFACE_TIMEOUT_SEC = 2; + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_PCK_GATEWAY).collect(Collectors.toSet())); + private static final String DISCOVER_REQUEST = "openHAB"; + + public LcnPchkDiscoveryService() throws IllegalArgumentException { + super(SUPPORTED_THING_TYPES_UIDS, 0, false); + } + + private List getLocalAddresses() { + List result = new LinkedList<>(); + try { + for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) { + try { + if (networkInterface.isUp() && !networkInterface.isLoopback() + && !networkInterface.isPointToPoint()) { + result.addAll(Collections.list(networkInterface.getInetAddresses())); + } + } catch (SocketException exception) { + // ignore + } + } + } catch (SocketException exception) { + return Collections.emptyList(); + } + return result; + } + + @Override + protected void startScan() { + try { + InetAddress multicastAddress = InetAddress.getByName(PCHK_DISCOVERY_MULTICAST_ADDRESS); + + getLocalAddresses().forEach(localInterfaceAddress -> { + logger.debug("Searching on {} ...", localInterfaceAddress.getHostAddress()); + try (MulticastSocket socket = new MulticastSocket(PCHK_DISCOVERY_PORT)) { + socket.setInterface(localInterfaceAddress); + socket.setReuseAddress(true); + socket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000); + socket.joinGroup(multicastAddress); + + byte[] requestData = DISCOVER_REQUEST.getBytes(LcnDefs.LCN_ENCODING); + DatagramPacket request = new DatagramPacket(requestData, requestData.length, multicastAddress, + PCHK_DISCOVERY_PORT); + socket.send(request); + + do { + byte[] rxbuf = new byte[8192]; + DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length); + socket.receive(packet); + + InetAddress addr = packet.getAddress(); + String response = new String(packet.getData(), LcnDefs.LCN_ENCODING); + + if (response.contains("ServicesRequest")) { + continue; + } + + ServicesResponse deserialized = xmlToServiceResponse(response); + + String macAddress = deserialized.getServer().getMachineId().replace(":", ""); + ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_PCK_GATEWAY, macAddress); + + Map properties = new HashMap<>(3); + properties.put(HOSTNAME, addr.getHostAddress()); + properties.put(PORT, deserialized.getExtServices().getExtService().getLocalPort()); + properties.put(MAC_ADDRESS, macAddress); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties).withRepresentationProperty(MAC_ADDRESS) + .withLabel(deserialized.getServer().getContent() + " (" + + deserialized.getServer().getMachineName() + ")"); + + thingDiscovered(discoveryResult.build()); + } while (true); // left by SocketTimeoutException + } catch (IOException e) { + logger.debug("Discovery failed for {}: {}", localInterfaceAddress, e.getMessage()); + } + }); + } catch (UnknownHostException e) { + logger.warn("Discovery failed: {}", e.getMessage()); + } + } + + ServicesResponse xmlToServiceResponse(String response) { + XStream xstream = new XStream(new StaxDriver()); + xstream.setClassLoader(getClass().getClassLoader()); + xstream.autodetectAnnotations(true); + xstream.alias("ServicesResponse", ServicesResponse.class); + xstream.alias("Server", Server.class); + xstream.alias("Version", Server.class); + xstream.alias("ExtServices", ExtServices.class); + xstream.alias("ExtService", ExtService.class); + + return (ServicesResponse) xstream.fromXML(response); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java new file mode 100644 index 0000000000000..ee39fa6ab7254 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java @@ -0,0 +1,73 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamConverter; +import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter; + +/** + * Used for deserializing the XML response of the LCN-PCHK discovery protocol. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" }) +public class Server { + @XStreamAsAttribute + private final int requestId; + @XStreamAsAttribute + private final String machineId; + @XStreamAsAttribute + private final String machineName; + @XStreamAsAttribute + private final String osShort; + @XStreamAsAttribute + private final String osLong; + private final String content; + + public Server(int requestId, String machineId, String machineName, String osShort, String osLong, String content) { + this.requestId = requestId; + this.machineId = machineId; + this.machineName = machineName; + this.osShort = osShort; + this.osLong = osLong; + this.content = content; + } + + public int getRequestId() { + return requestId; + } + + public String getMachineId() { + return machineId; + } + + public String getOsShort() { + return osShort; + } + + public String getOsLong() { + return osLong; + } + + public String getContent() { + return content; + } + + public Object getMachineName() { + return machineName; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java new file mode 100644 index 0000000000000..e2a29e2434405 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java @@ -0,0 +1,47 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Used for deserializing the XML response of the LCN-PCHK discovery protocol. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class ServicesResponse { + private final Version Version; + private final Server Server; + private final ExtServices ExtServices; + @SuppressWarnings("unused") + private final Object Services = new Object(); + + public ServicesResponse(Version version, Server server, ExtServices extServices) { + this.Version = version; + this.Server = server; + this.ExtServices = extServices; + } + + public Server getServer() { + return Server; + } + + public Version getVersion() { + return Version; + } + + public ExtServices getExtServices() { + return ExtServices; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java new file mode 100644 index 0000000000000..6c406662474ae --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java @@ -0,0 +1,43 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + +/** + * Used for deserializing the XML response of the LCN-PCHK discovery protocol. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class Version { + @XStreamAsAttribute + private final int major; + @XStreamAsAttribute + private final int minor; + + public Version(int major, int minor) { + this.major = major; + this.minor = minor; + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java new file mode 100644 index 0000000000000..3988283d50305 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java @@ -0,0 +1,178 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.common.VariableValue; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for LCN module Thing sub handlers. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractLcnModuleSubHandler implements ILcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleSubHandler.class); + protected final LcnModuleHandler handler; + protected final ModInfo info; + + public AbstractLcnModuleSubHandler(LcnModuleHandler handler, ModInfo info) { + this.handler = handler; + this.info = info; + } + + @Override + public void handleRefresh(String groupId) { + // can be overwritten by subclasses. + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup) + throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandString(StringType command, int number) throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + unsupportedCommand(command); + } + + @Override + public void handleCommandHsb(HSBType command, String groupId) throws LcnException { + unsupportedCommand(command); + } + + private void unsupportedCommand(Command command) { + logger.warn("Unsupported command: {}: {}", getClass().getSimpleName(), command.getClass().getSimpleName()); + } + + /** + * Tries to parses the given PCK message. Fails silently to let another sub handler give the chance to process the + * message. + * + * @param pck the message to process + * @return true, if the message could be processed successfully + */ + public boolean tryParse(String pck) { + Optional firstSuccessfulMatcher = getPckStatusMessagePatterns().stream().map(p -> p.matcher(pck)) + .filter(Matcher::matches).filter(m -> handler.isMyAddress(m.group("segId"), m.group("modId"))) + .findAny(); + + firstSuccessfulMatcher.ifPresent(matcher -> { + try { + handleStatusMessage(matcher); + } catch (LcnException e) { + logger.warn("Parse error: {}", e.getMessage()); + } + }); + + return firstSuccessfulMatcher.isPresent(); + } + + /** + * Creates a RelayStateModifier array with all elements set to NOCHANGE. + * + * @return the created array + */ + protected RelayStateModifier[] createRelayStateModifierArray() { + RelayStateModifier[] ret = new LcnDefs.RelayStateModifier[LcnChannelGroup.RELAY.getCount()]; + Arrays.fill(ret, LcnDefs.RelayStateModifier.NOCHANGE); + return ret; + } + + /** + * Updates the state of the LCN module. + * + * @param type the channel type which shall be updated + * @param number the Channel's number within the channel type, zero-based + * @param state the new state + */ + protected void fireUpdate(LcnChannelGroup type, int number, State state) { + handler.updateChannel(type, (number + 1) + "", state); + } + + /** + * Fires the current state of a Variable to openHAB. Resets running value request logic. + * + * @param matcher the pre-matched matcher + * @param channelId the Channel's ID to update + * @param variable the Variable to update + * @return the new variable's value + */ + protected VariableValue fireUpdateAndReset(Matcher matcher, String channelId, Variable variable) { + VariableValue value = new VariableValue(Long.parseLong(matcher.group("value" + channelId))); + + info.updateVariableValue(variable, value); + info.onVariableResponseReceived(variable); + + fireUpdate(variable.getChannelType(), variable.getThresholdNumber().orElse(variable.getNumber()), + value.getState(variable)); + return value; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java new file mode 100644 index 0000000000000..5589d1d258197 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java @@ -0,0 +1,140 @@ +/** + * 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.lcn.internal.subhandler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for LCN module Thing sub handlers processing variables. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractLcnModuleVariableSubHandler extends AbstractLcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleVariableSubHandler.class); + + public AbstractLcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + requestVariable(info, channelGroup, number); + info.requestFirmwareVersion(); + } + + /** + * Requests the current state of the given Channel. + * + * @param info the modules ModInfo cache + * @param channelGroup the Channel group + * @param number the Channel's number within the Channel group + */ + protected void requestVariable(ModInfo info, LcnChannelGroup channelGroup, int number) { + try { + Variable var = getVariable(channelGroup, number); + info.refreshVariable(var); + } catch (IllegalArgumentException e) { + logger.warn("Could not parse variable name: {}{}", channelGroup, (number + 1)); + } + } + + /** + * Gets a Variable from the given parameters. + * + * @param channelGroup the Channel group the Variable is in + * @param number the number of the Variable's Channel + * @return the Variable + * @throws IllegalArgumentException when the Channel group and number do not exist + */ + protected Variable getVariable(LcnChannelGroup channelGroup, int number) throws IllegalArgumentException { + return Variable.valueOf(channelGroup.name() + (number + 1)); + } + + /** + * Calculates the relative change between the current and the demanded value of a Variable. + * + * @param command the requested value + * @param variable the Variable type + * @return the difference + * @throws LcnException when the difference is too big + */ + protected int getRelativeChange(DecimalType command, Variable variable) throws LcnException { + // LCN doesn't support setting thresholds or variables with absolute values. So, calculate the relative change. + int relativeVariableChange = (int) (command.longValue() - info.getVariableValue(variable)); + + int result; + if (relativeVariableChange > 0) { + result = Math.min(relativeVariableChange, getMaxAbsChange(variable)); + } else { + result = Math.max(relativeVariableChange, -getMaxAbsChange(variable)); + } + if (result != relativeVariableChange) { + logger.warn("Relative change of {} too big, limiting: {}", variable, relativeVariableChange); + } + return result; + } + + private int getMaxAbsChange(Variable variable) { + switch (variable) { + case RVARSETPOINT1: + case RVARSETPOINT2: + case THRESHOLDREGISTER11: + case THRESHOLDREGISTER12: + case THRESHOLDREGISTER13: + case THRESHOLDREGISTER14: + case THRESHOLDREGISTER15: + case THRESHOLDREGISTER21: + case THRESHOLDREGISTER22: + case THRESHOLDREGISTER23: + case THRESHOLDREGISTER24: + case THRESHOLDREGISTER31: + case THRESHOLDREGISTER32: + case THRESHOLDREGISTER33: + case THRESHOLDREGISTER34: + case THRESHOLDREGISTER41: + case THRESHOLDREGISTER42: + case THRESHOLDREGISTER43: + case THRESHOLDREGISTER44: + return 1000; + case VARIABLE1: + case VARIABLE2: + case VARIABLE3: + case VARIABLE4: + case VARIABLE5: + case VARIABLE6: + case VARIABLE7: + case VARIABLE8: + case VARIABLE9: + case VARIABLE10: + case VARIABLE11: + case VARIABLE12: + return 4000; + case UNKNOWN: + case S0INPUT1: + case S0INPUT2: + case S0INPUT3: + case S0INPUT4: + default: + return 0; + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java new file mode 100644 index 0000000000000..aff33cc1f0ebd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java @@ -0,0 +1,155 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Interface for LCN module Thing sub handlers processing variables. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public interface ILcnModuleSubHandler { + /** + * Gets the Patterns, the sub handler is capable to process. + * + * @return the Patterns + */ + Collection getPckStatusMessagePatterns(); + + /** + * Processes the payload of a pre-matched PCK message. + * + * @param matcher the pre-matched matcher. + * @throws LcnException when the message cannot be processed + */ + void handleStatusMessage(Matcher matcher) throws LcnException; + + /** + * Processes a refresh request from openHAB. + * + * @param channelGroup the Channel group that shall be refreshed + * @param number the Channel number within the Channel group + */ + void handleRefresh(LcnChannelGroup channelGroup, int number); + + /** + * Processes a refresh request from openHAB. + * + * @param groupId the Channel ID that shall be refreshed + */ + void handleRefresh(String groupId); + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param idWithoutGroup the Channel's name within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup) + throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandString(StringType command, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param channelGroup the addressed Channel group + * @param number the Channel's number within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) throws LcnException; + + /** + * Handles a Command from openHAB. + * + * @param command the command to handle + * @param groupId the Channel's name within the Channel group + * @throws LcnException when the command could not processed + */ + void handleCommandHsb(HSBType command, String groupId) throws LcnException; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java new file mode 100644 index 0000000000000..a911662d25cb0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java @@ -0,0 +1,62 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles State changes of binary sensors of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleBinarySensorSubHandler extends AbstractLcnModuleSubHandler { + private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Bx(?\\d+)"); + + public LcnModuleBinarySensorSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshBinarySensors(); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + info.onBinarySensorsResponseReceived(); + + boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue"))); + + IntStream.range(0, LcnChannelGroup.BINARYSENSOR.getCount()) + .forEach(i -> fireUpdate(LcnChannelGroup.BINARYSENSOR, i, + states[i] ? OpenClosedType.OPEN : OpenClosedType.CLOSED)); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java new file mode 100644 index 0000000000000..69a9102013a56 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java @@ -0,0 +1,108 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles State changes of transponders and remote controls of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleCodeSubHandler extends AbstractLcnModuleSubHandler { + private static final Pattern TRANSPONDER_PATTERN = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.ZT(?\\d{3})(?\\d{3})(?\\d{3})"); + private static final Pattern REMOTE_CONTROL_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.ZI(?\\d{3})(?\\d{3})(?\\d{3})(?\\d{3})(?\\d{3})"); + + public LcnModuleCodeSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + // nothing + } + + @Override + public void handleStatusMessage(Matcher matcher) { + String code = String.format("%02X%02X%02X", Integer.parseInt(matcher.group("byte0")), + Integer.parseInt(matcher.group("byte1")), Integer.parseInt(matcher.group("byte2"))); + + if (matcher.pattern() == TRANSPONDER_PATTERN) { + handler.triggerChannel(LcnChannelGroup.CODE, "transponder", code); + } else if (matcher.pattern() == REMOTE_CONTROL_PATTERN) { + int keyNumber = Integer.parseInt(matcher.group("key")); + String keyLayer; + + if (keyNumber > 30) { + keyLayer = "D"; + keyNumber -= 30; + } else if (keyNumber > 20) { + keyLayer = "C"; + keyNumber -= 20; + } else if (keyNumber > 10) { + keyLayer = "B"; + keyNumber -= 10; + } else if (keyNumber > 0) { + keyLayer = "A"; + } else { + return; + } + + int action = Integer.parseInt(matcher.group("action")); + + if (action > 10) { + handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolbatterylow", code); + action -= 10; + } + + LcnDefs.SendKeyCommand actionType; + switch (action) { + case 1: + actionType = LcnDefs.SendKeyCommand.HIT; + break; + case 2: + actionType = LcnDefs.SendKeyCommand.MAKE; + break; + case 3: + actionType = LcnDefs.SendKeyCommand.BREAK; + break; + default: + return; + } + + handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolkey", + keyLayer + keyNumber + ":" + actionType.name()); + + handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolcode", + code + ":" + keyLayer + keyNumber + ":" + actionType.name()); + } + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(TRANSPONDER_PATTERN, REMOTE_CONTROL_PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java new file mode 100644 index 0000000000000..b05116476e3cd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java @@ -0,0 +1,95 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.KeyLockStateModifier; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles Commands and State changes of key table locks of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleKeyLockTableSubHandler extends AbstractLcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleKeyLockTableSubHandler.class); + private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.TX(?\\d{3})(?\\d{3})(?\\d{3})((?\\d{3}))?"); + + public LcnModuleKeyLockTableSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshStatusLockedKeys(); + } + + @Override + public void handleRefresh(String groupId) { + // nothing + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + KeyLockStateModifier[] keyLockStateModifiers = new LcnDefs.KeyLockStateModifier[channelGroup.getCount()]; + Arrays.fill(keyLockStateModifiers, LcnDefs.KeyLockStateModifier.NOCHANGE); + keyLockStateModifiers[number] = command == OnOffType.ON ? LcnDefs.KeyLockStateModifier.ON + : LcnDefs.KeyLockStateModifier.OFF; + int tableId = channelGroup.ordinal() - LcnChannelGroup.KEYLOCKTABLEA.ordinal(); + handler.sendPck(PckGenerator.lockKeys(tableId, keyLockStateModifiers)); + info.refreshStatusStatusLockedKeysAfterChange(); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + info.onLockedKeysResponseReceived(); + + IntStream.range(0, LcnDefs.KEY_TABLE_COUNT).forEach(tableId -> { + String stateString = matcher.group(String.format("table%d", tableId)); + if (stateString != null) { + boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(stateString)); + try { + LcnChannelGroup channelGroup = LcnChannelGroup.fromTableId(tableId); + for (int i = 0; i < states.length; i++) { + fireUpdate(channelGroup, i, states[i] ? OnOffType.ON : OnOffType.OFF); + } + } catch (LcnException e) { + logger.warn("Failed to set key table lock state: {}", e.getMessage()); + } + } + }); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java new file mode 100644 index 0000000000000..6fd5d95cefa9f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java @@ -0,0 +1,66 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of LEDs of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLedSubHandler extends AbstractLcnModuleSubHandler { + public LcnModuleLedSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshLedsAndLogic(); + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + handleCommandString(new StringType(command.toString()), number); + } + + @Override + public void handleCommandString(StringType command, int number) throws LcnException { + handler.sendPck(PckGenerator.controlLed(number, LcnDefs.LedStatus.valueOf(command.toString()))); + info.refreshStatusLedsAnLogicAfterChange(); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + /** Status messages are handled in {@link LcnModuleLogicSubHandler}. */ + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java new file mode 100644 index 0000000000000..21b5403ae16b1 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java @@ -0,0 +1,126 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StringType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.LogicOpStatus; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles State changes of logic operations of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLogicSubHandler extends AbstractLcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleLogicSubHandler.class); + private static final Pattern PATTERN_SINGLE_LOGIC = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "S(?\\d{1})(?\\d{3})"); + private static final Pattern PATTERN_ALL = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.TL(?[AEBF]{12})(?[NTV]{4})"); + + public LcnModuleLogicSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshLedsAndLogic(); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + info.onLedsAndLogicResponseReceived(); + + if (matcher.pattern() == PATTERN_ALL) { + IntStream.range(0, LcnChannelGroup.LED.getCount()).forEach(i -> { + switch (matcher.group("ledStates").toUpperCase().charAt(i)) { + case 'A': + fireLed(i, LcnDefs.LedStatus.OFF); + break; + case 'E': + fireLed(i, LcnDefs.LedStatus.ON); + break; + case 'B': + fireLed(i, LcnDefs.LedStatus.BLINK); + break; + case 'F': + fireLed(i, LcnDefs.LedStatus.FLICKER); + break; + default: + logger.warn("Failed to parse LED state: {}", matcher.group("ledStates")); + } + }); + IntStream.range(0, LcnChannelGroup.LOGIC.getCount()).forEach(i -> { + switch (matcher.group("logicOpStates").toUpperCase().charAt(i)) { + case 'N': + fireLogic(i, LcnDefs.LogicOpStatus.NOT); + break; + case 'T': + fireLogic(i, LcnDefs.LogicOpStatus.OR); + break; + case 'V': + fireLogic(i, LcnDefs.LogicOpStatus.AND); + break; + default: + logger.warn("Failed to parse logic state: {}", matcher.group("logicOpStates")); + } + }); + } else if (matcher.pattern() == PATTERN_SINGLE_LOGIC) { + String rawState = matcher.group("logicOpState"); + + LogicOpStatus state; + switch (rawState) { + case "000": + state = LcnDefs.LogicOpStatus.NOT; + break; + case "025": + state = LcnDefs.LogicOpStatus.OR; + break; + case "050": + state = LcnDefs.LogicOpStatus.AND; + break; + default: + logger.warn("Failed to parse logic state: {}", rawState); + return; + } + fireLogic(Integer.parseInt(matcher.group("id")) - 1, state); + } + } + + private void fireLed(int number, LcnDefs.LedStatus status) { + fireUpdate(LcnChannelGroup.LED, number, new StringType(status.toString())); + } + + private void fireLogic(int number, LcnDefs.LogicOpStatus status) { + fireUpdate(LcnChannelGroup.LOGIC, number, new StringType(status.toString())); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN_ALL, PATTERN_SINGLE_LOGIC); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java new file mode 100644 index 0000000000000..6ced1c8c35699 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java @@ -0,0 +1,90 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handle Acks received from an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleMetaAckSubHandler extends AbstractLcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleMetaAckSubHandler.class); + /** The pattern for the Ack PCK message */ + public static final Pattern PATTERN_POS = Pattern.compile("-M(?\\d{3})(?\\d{3})!"); + private static final Pattern PATTERN_NEG = Pattern.compile("-M(?\\d{3})(?\\d{3})(?\\d+)"); + + public LcnModuleMetaAckSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + // nothing + } + + @Override + public void handleStatusMessage(Matcher matcher) { + if (matcher.pattern() == PATTERN_POS) { + handler.onAckRceived(); + } else if (matcher.pattern() == PATTERN_NEG) { + logger.warn("{}: NACK received: {}", handler.getStatusMessageAddress(), + codeToString(Integer.parseInt(matcher.group("code")))); + } + } + + private String codeToString(int code) { + switch (code) { + case LcnBindingConstants.CODE_ACK: + return "ACK"; + case 5: + return "Unknown command"; + case 6: + return "Invalid parameter count"; + case 7: + return "Invalid parameter"; + case 8: + return "Command not allowed (e.g. output locked)"; + case 9: + return "Command not allowed by module's configuration"; + case 10: + return "Module not capable"; + case 11: + return "Periphery missing"; + case 12: + return "Programming mode necessary"; + case 14: + return "Mains fuse blown"; + default: + return "Unknown"; + } + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN_POS, PATTERN_NEG); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java new file mode 100644 index 0000000000000..59621e8e1261f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java @@ -0,0 +1,55 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles serial number and firmware versions received from an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleMetaFirmwareSubHandler extends AbstractLcnModuleSubHandler { + /** The pattern for the serial number and firmware PCK message */ + public static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.SN(?[0-9|A-F]{10})(?[0-9|A-F]{2})FW(?[0-9|A-F]{6})HW(?\\d+)"); + + public LcnModuleMetaFirmwareSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + // nothing + } + + @Override + public void handleStatusMessage(Matcher matcher) { + info.setFirmwareVersion(Integer.parseInt(matcher.group("firmwareVersion"), 16)); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java new file mode 100644 index 0000000000000..d7f5d1fcb179c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java @@ -0,0 +1,182 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles Commands and State changes of dimmer outputs of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleOutputSubHandler extends AbstractLcnModuleSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleOutputSubHandler.class); + private static final int COLOR_RAMP_MS = 1000; + private static final String OUTPUT_COLOR = "color"; + private static final Pattern PERCENT_PATTERN; + private static final Pattern NATIVE_PATTERN; + private volatile HSBType currentColor = new HSBType(); + private volatile PercentType output4 = new PercentType(); + + public LcnModuleOutputSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + static { + PERCENT_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "A(?\\d)(?\\d+)"); + NATIVE_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "O(?\\d)(?\\d+)"); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(NATIVE_PATTERN, PERCENT_PATTERN); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshOutput(number); + } + + @Override + public void handleRefresh(String groupId) { + if (OUTPUT_COLOR.equals(groupId)) { + info.refreshAllOutputs(); + } + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + // don't use OnOffType.as() here, because it returns @Nullable + handler.sendPck(PckGenerator.dimOutput(number, command == OnOffType.ON ? 100 : 0, 0)); + } + + @Override + public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), 0)); + } + + @Override + public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup) + throws LcnException { + if (!OUTPUT_COLOR.equals(idWithoutGroup)) { + throw new LcnException("Unknown group ID: " + idWithoutGroup); + } + updateAndSendColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), command)); + } + + @Override + public void handleCommandHsb(HSBType command, String groupId) throws LcnException { + if (!OUTPUT_COLOR.equals(groupId)) { + throw new LcnException("Unknown group ID: " + groupId); + } + updateAndSendColor(command); + } + + private synchronized void updateAndSendColor(HSBType hsbType) throws LcnException { + currentColor = hsbType; + handler.updateChannel(LcnChannelGroup.OUTPUT, OUTPUT_COLOR, currentColor); + + if (info.getFirmwareVersion() >= LcnBindingConstants.FIRMWARE_2014) { + handler.sendPck(PckGenerator.dimAllOutputs(currentColor.getRed().doubleValue(), + currentColor.getGreen().doubleValue(), currentColor.getBlue().doubleValue(), output4.doubleValue(), + COLOR_RAMP_MS)); + } else { + handler.sendPck(PckGenerator.dimOutput(0, currentColor.getRed().doubleValue(), COLOR_RAMP_MS)); + handler.sendPck(PckGenerator.dimOutput(1, currentColor.getGreen().doubleValue(), COLOR_RAMP_MS)); + handler.sendPck(PckGenerator.dimOutput(2, currentColor.getBlue().doubleValue(), COLOR_RAMP_MS)); + } + } + + @Override + public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException { + int rampMs = command.getRampMs(); + if (command.isControlAllOutputs()) { // control all dimmer outputs + if (rampMs == LcnDefs.FIXED_RAMP_MS) { + // compatibility command + handler.sendPck(PckGenerator.controlAllOutputs(command.intValue())); + } else { + // command since firmware 180501 + handler.sendPck(PckGenerator.dimAllOutputs(command.doubleValue(), command.doubleValue(), + command.doubleValue(), command.doubleValue(), rampMs)); + } + } else if (command.isControlOutputs12()) { // control dimmer outputs 1+2 + if (command.intValue() == 0 || command.intValue() == 100) { + handler.sendPck(PckGenerator.controlOutputs12(command.intValue() > 0, rampMs >= LcnDefs.FIXED_RAMP_MS)); + } else { + // ignore ramp when dimming + handler.sendPck(PckGenerator.dimOutputs12(command.doubleValue())); + } + } else { + handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), rampMs)); + } + } + + @Override + public void handleStatusMessage(Matcher matcher) { + int outputId = Integer.parseInt(matcher.group("outputId")) - 1; + + if (!LcnChannelGroup.OUTPUT.isValidId(outputId)) { + logger.warn("outputId out of range: {}", outputId); + return; + } + double percent; + if (matcher.pattern() == PERCENT_PATTERN) { + percent = Integer.parseInt(matcher.group("percent")); + } else if (matcher.pattern() == NATIVE_PATTERN) { + percent = (double) Integer.parseInt(matcher.group("value")) / 2; + } else { + logger.warn("Unexpected pattern: {}", matcher.pattern()); + return; + } + + info.onOutputResponseReceived(outputId); + + percent = Math.min(100, Math.max(0, percent)); + + PercentType percentType = new PercentType((int) Math.round(percent)); + fireUpdate(LcnChannelGroup.OUTPUT, outputId, percentType); + + if (outputId == 3) { + output4 = percentType; + } + + if (percent > 0) { + if (outputId == 0) { + fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.UP); + } else if (outputId == 1) { + fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.DOWN); + } + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java new file mode 100644 index 0000000000000..d2b166cb67adc --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java @@ -0,0 +1,86 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of Relays of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRelaySubHandler extends AbstractLcnModuleSubHandler { + private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Rx(?\\d+)"); + + public LcnModuleRelaySubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshRelays(); + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray(); + relayStateModifiers[number] = command == OnOffType.ON ? LcnDefs.RelayStateModifier.ON + : LcnDefs.RelayStateModifier.OFF; + handler.sendPck(PckGenerator.controlRelays(relayStateModifiers)); + } + + @Override + public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + // don't use OnOffType.as(), because it returns @Nullable + handleCommandOnOff(command.intValue() > 0 ? OnOffType.ON : OnOffType.OFF, channelGroup, number); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + info.onRelayResponseReceived(); + + boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue"))); + + IntStream.range(0, LcnChannelGroup.RELAY.getCount()) + .forEach(i -> fireUpdate(LcnChannelGroup.RELAY, i, OnOffType.from(states[i]))); + + IntStream.range(0, LcnChannelGroup.ROLLERSHUTTERRELAY.getCount()).forEach(i -> { + UpDownType state = states[i * 2 + 1] ? UpDownType.DOWN : UpDownType.UP; + fireUpdate(LcnChannelGroup.ROLLERSHUTTERRELAY, i, state); + }); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java new file mode 100644 index 0000000000000..71b7521ebcd93 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java @@ -0,0 +1,82 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of roller shutters connected to dimmer outputs of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterOutputSubHandler extends AbstractLcnModuleSubHandler { + public LcnModuleRollershutterOutputSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshOutput(number); + } + + @Override + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + // When configured as shutter in LCN-PRO, an output gets switched off, when the other is + // switched on and vice versa. + if (command == UpDownType.UP) { + // first output: 100% + handler.sendPck(PckGenerator.dimOutput(0, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS)); + } else { + // second output: 100% + handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS)); + } + } + + @Override + public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + if (command == StopMoveType.STOP) { + // both outputs off + handler.sendPck(PckGenerator.dimOutput(0, 0, 0)); + handler.sendPck(PckGenerator.dimOutput(1, 0, 0)); + } else { + // roller shutters on outputs are stateless, assume always down when MOVE is sent + // second output: 100% + handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS)); + } + } + + @Override + public void handleStatusMessage(Matcher matcher) { + // status messages of roller shutters on dimmer outputs are handled in the dimmer output sub handler + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java new file mode 100644 index 0000000000000..ef46add8a5f01 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java @@ -0,0 +1,77 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of roller shutters connected to relays of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterRelaySubHandler extends AbstractLcnModuleSubHandler { + public LcnModuleRollershutterRelaySubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + info.refreshRelays(); + } + + @Override + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray(); + // direction relay + relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ? LcnDefs.RelayStateModifier.ON + : LcnDefs.RelayStateModifier.OFF; + // power relay + relayStateModifiers[number * 2] = LcnDefs.RelayStateModifier.ON; + handler.sendPck(PckGenerator.controlRelays(relayStateModifiers)); + } + + @Override + public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray(); + // power relay + relayStateModifiers[number * 2] = command == StopMoveType.MOVE ? LcnDefs.RelayStateModifier.ON + : LcnDefs.RelayStateModifier.OFF; + handler.sendPck(PckGenerator.controlRelays(relayStateModifiers)); + } + + @Override + public void handleStatusMessage(Matcher matcher) { + // status messages of roller shutters on relays are handled in the relay sub handler + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java new file mode 100644 index 0000000000000..a2611cc38213c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java @@ -0,0 +1,66 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of regulator locks of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarLockSubHandler extends AbstractLcnModuleVariableSubHandler { + public LcnModuleRvarLockSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleRefresh(LcnChannelGroup channelGroup, int number) { + super.handleRefresh(LcnChannelGroup.RVARSETPOINT, number); + } + + @Override + public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException { + boolean locked = command == OnOffType.ON; + handler.sendPck(PckGenerator.lockRegulator(number, locked)); + + // request new lock state, if the module doesn't send it on itself + Variable variable = getVariable(LcnChannelGroup.RVARSETPOINT, number); + if (variable.shouldPollStatusAfterRegulatorLock(info.getFirmwareVersion(), locked)) { + info.refreshVariable(variable); + } + } + + @Override + public void handleStatusMessage(Matcher matcher) { + // status messages are handled in the RVar setpoint sub handler + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java new file mode 100644 index 0000000000000..5bf2d0c29c79d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java @@ -0,0 +1,79 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.common.VariableValue; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of regulator setpoints of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarSetpointSubHandler extends AbstractLcnModuleVariableSubHandler { + private static final Pattern PATTERN = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.S(?\\d)(?\\d+)"); + + public LcnModuleRvarSetpointSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + Variable variable = getVariable(channelGroup, number); + + if (info.hasExtendedMeasurementProcessing()) { + handler.sendPck(PckGenerator.setSetpointAbsolute(number, command.intValue())); + } else { + try { + int relativeVariableChange = getRelativeChange(command, variable); + handler.sendPck( + PckGenerator.setSetpointRelative(number, LcnDefs.RelVarRef.CURRENT, relativeVariableChange)); + } catch (LcnException e) { + // current value unknown for some reason, refresh it in case we come again here + info.refreshVariable(variable); + throw e; + } + } + } + + @Override + public void handleStatusMessage(Matcher matcher) throws LcnException { + Variable variable = Variable.setPointIdToVar(Integer.parseInt(matcher.group("id")) - 1); + VariableValue value = fireUpdateAndReset(matcher, "", variable); + + fireUpdate(LcnChannelGroup.RVARLOCK, variable.getNumber(), OnOffType.from(value.isRegulatorLocked())); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java new file mode 100644 index 0000000000000..428b8352f822c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java @@ -0,0 +1,58 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Handles Commands and State changes of S0 counter inputs of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleS0CounterSubHandler extends AbstractLcnModuleVariableSubHandler { + private static final Pattern PATTERN = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.C(?\\d)(?\\d+)"); + + public LcnModuleS0CounterSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + throw new LcnException("Setting S0 counters is not supported"); + } + + @Override + public void handleStatusMessage(Matcher matcher) throws LcnException { + fireUpdateAndReset(matcher, "", Variable.s0IdToVar(Integer.parseInt(matcher.group("id")) - 1)); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java new file mode 100644 index 0000000000000..744b61db88f9f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java @@ -0,0 +1,105 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles Commands and State changes of thresholds of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleThresholdSubHandler extends AbstractLcnModuleVariableSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleThresholdSubHandler.class); + private static final Pattern PATTERN = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.T(?\\d)(?\\d)(?\\d+)"); + private static final Pattern PATTERN_BEFORE_2013 = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.S1(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})"); + + public LcnModuleThresholdSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + Variable variable = getVariable(channelGroup, number); + try { + int relativeChange = getRelativeChange(command, variable); + handler.sendPck(PckGenerator.setThresholdRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange, + info.hasExtendedMeasurementProcessing())); + + // request new value, if the module doesn't send it on itself + if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) { + info.refreshVariable(variable); + } + } catch (LcnException e) { + // current value unknown for some reason, refresh it in case we come again here + info.refreshVariable(variable); + throw e; + } + } + + @Override + public void handleStatusMessage(Matcher matcher) { + IntStream stream; + Optional groupSuffix; + int registerNumber; + if (matcher.pattern() == PATTERN) { + int thresholdId = Integer.parseInt(matcher.group("thresholdId")) - 1; + registerNumber = Integer.parseInt(matcher.group("registerId")) - 1; + stream = IntStream.rangeClosed(thresholdId, thresholdId); + groupSuffix = Optional.of(""); + } else if (matcher.pattern() == PATTERN_BEFORE_2013) { + stream = IntStream.range(0, LcnDefs.THRESHOLD_COUNT_BEFORE_2013); + groupSuffix = Optional.empty(); + registerNumber = 0; + } else { + logger.warn("Unexpected pattern: {}", matcher.pattern()); + return; + } + + stream.forEach(i -> { + try { + fireUpdateAndReset(matcher, groupSuffix.orElse(String.valueOf(i)), + Variable.thrsIdToVar(registerNumber, i)); + } catch (LcnException e) { + logger.warn("Parse error: {}", e.getMessage()); + } + }); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN, PATTERN_BEFORE_2013); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java new file mode 100644 index 0000000000000..ac1ce6f3cc945 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java @@ -0,0 +1,88 @@ +/** + * 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.lcn.internal.subhandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.PckGenerator; +import org.openhab.binding.lcn.internal.common.Variable; +import org.openhab.binding.lcn.internal.connection.ModInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles Commands and State changes of variables of an LCN module. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleVariableSubHandler extends AbstractLcnModuleVariableSubHandler { + private final Logger logger = LoggerFactory.getLogger(LcnModuleVariableSubHandler.class); + private static final Pattern PATTERN = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.A(?\\d{3})(?\\d+)"); + private static final Pattern PATTERN_LEGACY = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.(?\\d+)"); + + public LcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) { + super(handler, info); + } + + @Override + public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) + throws LcnException { + Variable variable = getVariable(channelGroup, number); + try { + int relativeChange = getRelativeChange(command, variable); + handler.sendPck(PckGenerator.setVariableRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange)); + + // request new value, if the module doesn't send it on itself + if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) { + info.refreshVariable(variable); + } + } catch (LcnException e) { + // current value unknown for some reason, refresh it in case we come again here + info.refreshVariable(variable); + throw e; + } + } + + @Override + public void handleStatusMessage(Matcher matcher) throws LcnException { + Variable variable; + if (matcher.pattern() == PATTERN) { + variable = Variable.varIdToVar(Integer.parseInt(matcher.group("id")) - 1); + } else if (matcher.pattern() == PATTERN_LEGACY) { + variable = info.getLastRequestedVarWithoutTypeInResponse(); + info.setLastRequestedVarWithoutTypeInResponse(Variable.UNKNOWN); // Reset + } else { + logger.warn("Unexpected pattern: {}", matcher.pattern()); + return; + } + fireUpdateAndReset(matcher, "", variable); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN, PATTERN_LEGACY); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..c494e66acc295 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + LCN Binding + This is the binding for Local Control Network (LCN) + Fabian Wolter + + diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml new file mode 100644 index 0000000000000..439256829226b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml @@ -0,0 +1,102 @@ + + + + + + + The hostname or the IP address of the PCK gateway + network-address + true + + + + The IP port of the PCK gateway + 4114 + true + + + + The login username of the PCK gateway + true + + + + The login password of the PCK gateway + password + + + + IMPORTANT: Dimming range of all modules. Must be the same value as configured in LCN-PRO (Options/Settings/Expert Settings). If you only have modules with firmware newer than Feb. 2013, you probably want to choose 0 - 200.]]> + native200 + + + + + true + + + + Period after which an LCN command is resent, when no acknowledge has been received (in ms). + 3500 + true + + + + + + + The module ID, configured in LCN-PRO + + + + The segment ID the module is in (0 if no segments are present) + + + + + + + The group number, configured in LCN-PRO + + + + The module ID of any module in the group. The state of this module is used for visualization of the + group as representative for all modules. + + + + The segment ID of all modules in this group (0 if no segments are present) + 0 + + + + + + + Unit of the sensor + native + + + + + + + + + + + + + true + + + + Only for S0 counters (power or energy) + 1000 + + + diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties new file mode 100644 index 0000000000000..29ca7960c205e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties @@ -0,0 +1,171 @@ +# binding +binding.lcn.name = LCN Binding +binding.lcn.description = Binding fr Local Control Network (LCN) + +# thing types +thing-type.lcn.pckGateway.label = LCN-PCK-Koppler +thing-type.lcn.pckGateway.description = z.B. die LCN-PCHK-Software oder das Hutschienenmodul LCN-PKE +thing-type.lcn.module.label = LCN-Modul +thing-type.lcn.module.description = z.B. LCN-UPP, LCN-SH, LCN-HU +thing-type.lcn.group.label = LCN-Gruppe +thing-type.lcn.group.description = Eine Gruppe mit mehreren Modulen, wie in LCN-PRO parametriert + +# thing type config description +thing-type.config.lcn.pckGateway.hostname.description = Hostname oder die IP-Adresse des PCK-Kopplers +thing-type.config.lcn.pckGateway.port.description = Netzwerk-Port auf dem der PCK-Koppler luft +thing-type.config.lcn.pckGateway.username.description = Benutzername vom PCK-Koppler +thing-type.config.lcn.pckGateway.password.description = Login-Passwort vom PCK-Koppler +thing-type.config.lcn.pckGateway.mode.description = WICHTIG: Der Dimmbereich von allen LCN-Modulen. Muss der gleiche Wert, wie in LCN-PRO sein (Optionen/Einstellungen/Experteneinstellungen). Wenn nur Module lter als 2013 im Bus vorhanden sind, muss hier wahrscheinlich 0 - 200 ausgewhlt werden. +thing-type.config.lcn.pckGateway.timeoutMs.description = Zeit nach der eine PCK-Nachricht erneut gesendet wird, wenn vom Modul keine positive Quittung empfangen wurde. + +thing-type.config.lcn.module.moduleId.label = Modul-ID +thing-type.config.lcn.module.moduleId.description = Modul-ID, wie in LCN-PRO parametriert +thing-type.config.lcn.module.segmentId.label = Segment-ID +thing-type.config.lcn.module.segmentId.description = ID des Segments, in dem sich das Modul befindet (0 wenn keine Segmente vorhanden sind) + +thing-type.config.lcn.group.groupId.label = Gruppennummer +thing-type.config.lcn.group.groupId.description = Nummer der Gruppe, wie in LCN-PRO parametriert +thing-type.config.lcn.group.moduleId.label = Modul-ID eines Moduls aus der Gruppe +thing-type.config.lcn.group.moduleId.description = Der Zustand dieses Moduls wird zur Visualisierung der Gruppe, stellvertretend fr alle Module, genutzt +thing-type.config.lcn.group.segmentId.label = Segment-ID +thing-type.config.lcn.group.segmentId.description = Segment-ID in dem sich die Module der Gruppe befinden (0 wenn keine Segmente vorhanden sind) + +# channel type config description +channel-type.config.lcn.variable.unit.label = Einheit +channel-type.config.lcn.variable.unit.description = Einheit des Sensors +channel-type.config.lcn.variable.unit.option.native = LCN-Wert +channel-type.config.lcn.variable.unit.option.temperature = Temperatur (C) +channel-type.config.lcn.variable.unit.option.light = Licht (Lux) +channel-type.config.lcn.variable.unit.option.co2 = CO\u2082 (ppm) +channel-type.config.lcn.variable.unit.option.power = Leistung (W) +channel-type.config.lcn.variable.unit.option.energy = Zhlerstand (kWh) +channel-type.config.lcn.variable.unit.option.current = Strom (mA) +channel-type.config.lcn.variable.unit.option.voltage = Spannung (V) +channel-type.config.lcn.variable.unit.option.angle = Winkel () +channel-type.config.lcn.variable.unit.option.windspeed = Windgeschwindigkeit (m/s) +channel-type.config.lcn.variable.parameter.label = Impulse pro kWh +channel-type.config.lcn.variable.parameter.description = Nur fr S0-Zhler + +# channel types +channel-group-type.lcn.outputs.label = Ausgnge +channel-group-type.lcn.outputs.channel.1.label = Ausgang 1 +channel-group-type.lcn.outputs.channel.2.label = Ausgang 2 +channel-group-type.lcn.outputs.channel.3.label = Ausgang 3 +channel-group-type.lcn.outputs.channel.4.label = Ausgang 4 +channel-group-type.lcn.outputs.channel.color.label = RGB-Steuerung fr Ausgnge 1-3 +channel-group-type.lcn.rollershutteroutputs.label = Rolllden an Ausgngen +channel-group-type.lcn.rollershutteroutputs.channel.1.label = Rolllden an Ausgngen 1+2 +channel-group-type.lcn.relays.label = Relais +channel-group-type.lcn.relays.channel.1.label = Relais 1 +channel-group-type.lcn.relays.channel.2.label = Relais 2 +channel-group-type.lcn.relays.channel.3.label = Relais 3 +channel-group-type.lcn.relays.channel.4.label = Relais 4 +channel-group-type.lcn.relays.channel.5.label = Relais 5 +channel-group-type.lcn.relays.channel.6.label = Relais 6 +channel-group-type.lcn.relays.channel.7.label = Relais 7 +channel-group-type.lcn.relays.channel.8.label = Relais 8 +channel-group-type.lcn.rollershutterrelays.label = Rolllden an Relais +channel-group-type.lcn.rollershutterrelays.channel.1.label = Rolllden an Relais 1+2 +channel-group-type.lcn.rollershutterrelays.channel.2.label = Rolllden an Relais 3+4 +channel-group-type.lcn.rollershutterrelays.channel.3.label = Rolllden an Relais 5+6 +channel-group-type.lcn.rollershutterrelays.channel.4.label = Rolllden an Relais 7+8 +channel-group-type.lcn.logics.label = Logik-Funktionen +channel-group-type.lcn.logics.channel.1.label = Logik-Funktion 1 +channel-group-type.lcn.logics.channel.2.label = Logik-Funktion 2 +channel-group-type.lcn.logics.channel.3.label = Logik-Funktion 3 +channel-group-type.lcn.logics.channel.4.label = Logik-Funktion 4 +channel-group-type.lcn.binarysensors.label = Binrsensoren +channel-group-type.lcn.binarysensors.channel.1.label = Binrsensor 1 +channel-group-type.lcn.binarysensors.channel.2.label = Binrsensor 2 +channel-group-type.lcn.binarysensors.channel.3.label = Binrsensor 3 +channel-group-type.lcn.binarysensors.channel.4.label = Binrsensor 4 +channel-group-type.lcn.binarysensors.channel.5.label = Binrsensor 5 +channel-group-type.lcn.binarysensors.channel.6.label = Binrsensor 6 +channel-group-type.lcn.binarysensors.channel.7.label = Binrsensor 7 +channel-group-type.lcn.binarysensors.channel.8.label = Binrsensor 8 +channel-group-type.lcn.variables.label = Variablen +channel-group-type.lcn.variables.channel.1.label = Variable 1 +channel-group-type.lcn.variables.channel.2.label = Variable 2 +channel-group-type.lcn.variables.channel.3.label = Variable 3 +channel-group-type.lcn.variables.channel.4.label = Variable 4 +channel-group-type.lcn.variables.channel.5.label = Variable 5 +channel-group-type.lcn.variables.channel.6.label = Variable 6 +channel-group-type.lcn.variables.channel.7.label = Variable 7 +channel-group-type.lcn.variables.channel.8.label = Variable 8 +channel-group-type.lcn.variables.channel.9.label = Variable 9 +channel-group-type.lcn.variables.channel.10.label = Variable 10 +channel-group-type.lcn.variables.channel.11.label = Variable 11 +channel-group-type.lcn.variables.channel.12.label = Variable 12 +channel-group-type.lcn.rvarsetpoints.label = Regler +channel-group-type.lcn.rvarsetpoints.channel.1.label = Regler 1 Sollwert +channel-group-type.lcn.rvarsetpoints.channel.2.label = Regler 2 Sollwert +channel-group-type.lcn.rvarlocks.label = Regler Sperren +channel-group-type.lcn.rvarlocks.channel.1.label = Regler 1 Sperre +channel-group-type.lcn.rvarlocks.channel.2.label = Regler 2 Sperre +channel-group-type.lcn.thresholdregisters1.label = Schwellwertregister 1 +channel-group-type.lcn.thresholdregisters1.channel.1.label = Schwellwert 1 +channel-group-type.lcn.thresholdregisters1.channel.2.label = Schwellwert 2 +channel-group-type.lcn.thresholdregisters1.channel.3.label = Schwellwert 3 +channel-group-type.lcn.thresholdregisters1.channel.4.label = Schwellwert 4 +channel-group-type.lcn.thresholdregisters1.channel.5.label = Schwellwert 5 +channel-group-type.lcn.thresholdregisters2.label = Schwellwertregister 2 +channel-group-type.lcn.thresholdregisters2.channel.1.label = Schwellwert 1 +channel-group-type.lcn.thresholdregisters2.channel.2.label = Schwellwert 2 +channel-group-type.lcn.thresholdregisters2.channel.3.label = Schwellwert 3 +channel-group-type.lcn.thresholdregisters2.channel.4.label = Schwellwert 4 +channel-group-type.lcn.thresholdregisters3.label = Schwellwertregister 3 +channel-group-type.lcn.thresholdregisters3.channel.1.label = Schwellwert 1 +channel-group-type.lcn.thresholdregisters3.channel.2.label = Schwellwert 2 +channel-group-type.lcn.thresholdregisters3.channel.3.label = Schwellwert 3 +channel-group-type.lcn.thresholdregisters3.channel.4.label = Schwellwert 4 +channel-group-type.lcn.thresholdregisters4.label = Schwellwertregister 4 +channel-group-type.lcn.thresholdregisters4.channel.1.label = Schwellwert 1 +channel-group-type.lcn.thresholdregisters4.channel.2.label = Schwellwert 2 +channel-group-type.lcn.thresholdregisters4.channel.3.label = Schwellwert 3 +channel-group-type.lcn.thresholdregisters4.channel.4.label = Schwellwert 4 +channel-group-type.lcn.s0inputs.label = S0-Zhler +channel-group-type.lcn.s0inputs.channel.1.label = S0-Zhler 1 +channel-group-type.lcn.s0inputs.channel.2.label = S0-Zhler 2 +channel-group-type.lcn.s0inputs.channel.3.label = S0-Zhler 3 +channel-group-type.lcn.s0inputs.channel.4.label = S0-Zhler 4 +channel-group-type.lcn.keyslocktablea.label = Tastensperren Tabelle A +channel-group-type.lcn.keyslocktablea.channel.1.label = A1 Sperre +channel-group-type.lcn.keyslocktablea.channel.2.label = A2 Sperre +channel-group-type.lcn.keyslocktablea.channel.3.label = A3 Sperre +channel-group-type.lcn.keyslocktablea.channel.4.label = A4 Sperre +channel-group-type.lcn.keyslocktablea.channel.5.label = A5 Sperre +channel-group-type.lcn.keyslocktablea.channel.6.label = A6 Sperre +channel-group-type.lcn.keyslocktablea.channel.7.label = A7 Sperre +channel-group-type.lcn.keyslocktablea.channel.8.label = A8 Sperre +channel-group-type.lcn.keyslocktableb.label = Tastensperren Tabelle B +channel-group-type.lcn.keyslocktableb.channel.1.label = B1 Sperre +channel-group-type.lcn.keyslocktableb.channel.2.label = B2 Sperre +channel-group-type.lcn.keyslocktableb.channel.3.label = B3 Sperre +channel-group-type.lcn.keyslocktableb.channel.4.label = B4 Sperre +channel-group-type.lcn.keyslocktableb.channel.5.label = B5 Sperre +channel-group-type.lcn.keyslocktableb.channel.6.label = B6 Sperre +channel-group-type.lcn.keyslocktableb.channel.7.label = B7 Sperre +channel-group-type.lcn.keyslocktableb.channel.8.label = B8 Sperre +channel-group-type.lcn.keyslocktablec.label = Tastensperren Tabelle C +channel-group-type.lcn.keyslocktablec.channel.1.label = C1 Sperre +channel-group-type.lcn.keyslocktablec.channel.2.label = C2 Sperre +channel-group-type.lcn.keyslocktablec.channel.3.label = C3 Sperre +channel-group-type.lcn.keyslocktablec.channel.4.label = C4 Sperre +channel-group-type.lcn.keyslocktablec.channel.5.label = C5 Sperre +channel-group-type.lcn.keyslocktablec.channel.6.label = C6 Sperre +channel-group-type.lcn.keyslocktablec.channel.7.label = C7 Sperre +channel-group-type.lcn.keyslocktablec.channel.8.label = C8 Sperre +channel-group-type.lcn.keyslocktabled.label = Tastensperren Tabelle D +channel-group-type.lcn.keyslocktabled.channel.1.label = D1 Sperre +channel-group-type.lcn.keyslocktabled.channel.2.label = D2 Sperre +channel-group-type.lcn.keyslocktabled.channel.3.label = D3 Sperre +channel-group-type.lcn.keyslocktabled.channel.4.label = D4 Sperre +channel-group-type.lcn.keyslocktabled.channel.5.label = D5 Sperre +channel-group-type.lcn.keyslocktabled.channel.6.label = D6 Sperre +channel-group-type.lcn.keyslocktabled.channel.7.label = D7 Sperre +channel-group-type.lcn.keyslocktabled.channel.8.label = D8 Sperre +channel-group-type.lcn.codes.label = Transponder & Fernbedienungen +channel-group-type.lcn.codes.channel.transponder.label = Transponder-Code +channel-group-type.lcn.codes.channel.remotecontrolkey.label = Fernbedienung Tasten +channel-group-type.lcn.codes.channel.remotecontrolcode.label = Fernbedienung Tasten mit Zutrittscode +channel-group-type.lcn.codes.channel.remotecontrolbatterylow.label = Fernbedienung Batterie leer diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..7921cd5534715 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,634 @@ + + + + + + An LCN gateway speaking the PCK language. E.g. LCN-PCHK software or the DIN rail device LCN-PKE. + + + + + + + + + + + An LCN bus module, e.g. LCN-UPP, LCN-SH, LCN-HU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An LCN group with multiple modules, configured in LCN-PRO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dimmer + + veto + + + + Color + + veto + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rollershutter + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contact + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Only before Feb. 2013 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + trigger + + + + + + trigger + + + + + + trigger + + + + + + trigger + + + + diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java new file mode 100644 index 0000000000000..2867607cc6d90 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java @@ -0,0 +1,199 @@ +/** + * 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.lcn.internal; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class for {@link LcnModuleActions}. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class ModuleActionsTest { + private LcnModuleActions a = new LcnModuleActions(); + private final LcnModuleHandler handler = mock(LcnModuleHandler.class); + @Captor + private @NonNullByDefault({}) ArgumentCaptor byteBufferCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + a = new LcnModuleActions(); + a.setThingHandler(handler); + } + + private byte[] stringToByteBuffer(String string) { + return string.getBytes(LcnDefs.LCN_ENCODING); + } + + @Test + public void testSendDynamicText1CharRow1() throws LcnException { + a.sendDynamicText(1, "a"); + + verify(handler).sendPck(stringToByteBuffer("GTDT11a\0\0\0\0\0\0\0\0\0\0\0")); + } + + @Test + public void testSendDynamicText1ChunkRow1() throws LcnException { + a.sendDynamicText(1, "abcdfghijklm"); + + verify(handler).sendPck(stringToByteBuffer("GTDT11abcdfghijklm")); + } + + @Test + public void testSendDynamicText1Chunk1CharRow1() throws LcnException { + a.sendDynamicText(1, "abcdfghijklmn"); + + verify(handler, times(2)).sendPck(byteBufferCaptor.capture()); + + assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT11abcdfghijklm"), + stringToByteBuffer("GTDT12n\0\0\0\0\0\0\0\0\0\0\0"))); + } + + @Test + public void testSendDynamicText5ChunksRow1() throws LcnException { + a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijk"); + + verify(handler, times(5)).sendPck(byteBufferCaptor.capture()); + + assertThat(byteBufferCaptor.getAllValues(), + containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"), + stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"), + stringToByteBuffer("GTDT15yzabcdfghijk"))); + } + + @Test + public void testSendDynamicText5Chunks1CharRow1Truncated() throws LcnException { + a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl"); + + verify(handler, times(5)).sendPck(byteBufferCaptor.capture()); + + assertThat(byteBufferCaptor.getAllValues(), + containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"), + stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"), + stringToByteBuffer("GTDT15yzabcdfghijk"))); + } + + @Test + public void testSendDynamicText5Chunks1UmlautRow1Truncated() throws LcnException { + a.sendDynamicText(1, "äcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl"); + + verify(handler, times(5)).sendPck(byteBufferCaptor.capture()); + + assertThat(byteBufferCaptor.getAllValues(), + containsInAnyOrder(stringToByteBuffer("GTDT11äcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"), + stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"), + stringToByteBuffer("GTDT15yzabcdfghijk"))); + } + + @Test + public void testSendDynamicTextRow4() throws LcnException { + a.sendDynamicText(4, "abcdfghijklmn"); + + verify(handler, times(2)).sendPck(byteBufferCaptor.capture()); + + assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT41abcdfghijklm"), + stringToByteBuffer("GTDT42n\0\0\0\0\0\0\0\0\0\0\0"))); + } + + @Test + public void testSendDynamicTextSplitInCharacter() throws LcnException { + a.sendDynamicText(4, "Test 123 öäüß"); + + verify(handler, times(2)).sendPck(byteBufferCaptor.capture()); + + String string1 = "GTDT41Test 123 ö"; + ByteBuffer chunk1 = ByteBuffer.allocate(stringToByteBuffer(string1).length + 1); + chunk1.put(stringToByteBuffer(string1)); + chunk1.put((byte) -61); // first byte of ä + + ByteBuffer chunk2 = ByteBuffer.allocate(18); + chunk2.put(stringToByteBuffer("GTDT42")); + chunk2.put((byte) -92); // second byte of ä + chunk2.put(stringToByteBuffer("üß\0\0\0\0\0\0")); + + assertThat(byteBufferCaptor.getAllValues(), contains(chunk1.array(), chunk2.array())); + } + + @Test + public void testSendKeysInvalidTable() throws LcnException { + a.hitKey("E", 3, "MAKE"); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysNullTable() throws LcnException { + a.hitKey(null, 3, "MAKE"); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysNullAction() throws LcnException { + a.hitKey("D", 3, null); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysInvalidKey0() throws LcnException { + a.hitKey("D", 0, "MAKE"); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysInvalidKey9() throws LcnException { + a.hitKey("D", 9, "MAKE"); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysInvalidAction() throws LcnException { + a.hitKey("D", 8, "invalid"); + verify(handler, times(0)).sendPck(anyString()); + } + + @Test + public void testSendKeysA1Hit() throws LcnException { + a.hitKey("a", 1, "HIT"); + + verify(handler).sendPck("TSK--10000000"); + } + + @Test + public void testSendKeysC8Hit() throws LcnException { + a.hitKey("C", 8, "break"); + + verify(handler).sendPck("TS--O00000001"); + } + + @Test + public void testSendKeysD3Make() throws LcnException { + a.hitKey("D", 3, "MAKE"); + + verify(handler).sendPck("TS---L00100000"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java new file mode 100644 index 0000000000000..38feefcc26362 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java @@ -0,0 +1,58 @@ +/** + * 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.lcn.internal.pchkdiscovery; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Before; +import org.junit.Test; + +/** + * Test class for {@link LcnPchkDiscoveryService}. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnPchkDiscoveryServiceTest { + private LcnPchkDiscoveryService s = new LcnPchkDiscoveryService(); + private ServicesResponse r = s.xmlToServiceResponse(RESPONSE); + private static final String RESPONSE = "LCN-PCHK 3.2.2 running on Unix/LinuxPCHK 3.2.2 bus"; + + @Before + public void setUp() { + s = new LcnPchkDiscoveryService(); + r = s.xmlToServiceResponse(RESPONSE); + } + + @Test + public void testXmlMachineId() { + assertThat(r.getServer().getMachineId(), is("b8:27:eb:fe:a4:bb")); + } + + @Test + public void testXmlMachineName() { + assertThat(r.getServer().getMachineName(), is("raspberrypi")); + } + + @Test + public void testXmlServerContent() { + assertThat(r.getServer().getContent(), is("LCN-PCHK 3.2.2 running on Unix/Linux")); + } + + @Test + public void testXmlPort() { + assertThat(r.getExtServices().getExtService().getLocalPort(), is(4114)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java new file mode 100644 index 0000000000000..7cee0058d48b5 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java @@ -0,0 +1,43 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.when; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.lcn.internal.LcnModuleHandler; +import org.openhab.binding.lcn.internal.connection.ModInfo; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class AbstractTestLcnModuleSubHandler { + @Mock + protected @NonNullByDefault({}) LcnModuleHandler handler; + @Mock + protected @NonNullByDefault({}) ModInfo info; + + public AbstractTestLcnModuleSubHandler() { + setUp(); + } + + public void setUp() { + MockitoAnnotations.initMocks(this); + when(handler.isMyAddress("000", "005")).thenReturn(true); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java new file mode 100644 index 0000000000000..ba6f4a2581cf8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java @@ -0,0 +1,77 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleBinarySensorSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleBinarySensorSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleBinarySensorSubHandler(handler, info); + } + + @Test + public void testStatusAllClosed() { + l.tryParse("=M000005Bx000"); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED); + } + + @Test + public void testStatusAllOpen() { + l.tryParse("=M000005Bx255"); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.OPEN); + } + + @Test + public void testStatus1And7Closed() { + l.tryParse("=M000005Bx065"); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN); + verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java new file mode 100644 index 0000000000000..011bc56801970 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java @@ -0,0 +1,173 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleKeyLockTableSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleKeyLockTableSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleKeyLockTableSubHandler(handler, info); + } + + @Test + public void testStatus() { + l.tryParse("=M000005.TX098036000255"); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "4", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "5", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "8", OnOffType.ON); + } + + @Test + public void testHandleCommandA1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 0); + verify(handler).sendPck("TXA0-------"); + } + + @Test + public void testHandleCommandA1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 0); + verify(handler).sendPck("TXA1-------"); + } + + @Test + public void testHandleCommandA8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 7); + verify(handler).sendPck("TXA-------0"); + } + + @Test + public void testHandleCommandA8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 7); + verify(handler).sendPck("TXA-------1"); + } + + @Test + public void testHandleCommandB1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 0); + verify(handler).sendPck("TXB0-------"); + } + + @Test + public void testHandleCommandB1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 0); + verify(handler).sendPck("TXB1-------"); + } + + @Test + public void testHandleCommandB8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 7); + verify(handler).sendPck("TXB-------0"); + } + + @Test + public void testHandleCommandB8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 7); + verify(handler).sendPck("TXB-------1"); + } + + @Test + public void testHandleCommandC1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 0); + verify(handler).sendPck("TXC0-------"); + } + + @Test + public void testHandleCommandC1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 0); + verify(handler).sendPck("TXC1-------"); + } + + @Test + public void testHandleCommandC8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 7); + verify(handler).sendPck("TXC-------0"); + } + + @Test + public void testHandleCommandC8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 7); + verify(handler).sendPck("TXC-------1"); + } + + @Test + public void testHandleCommandD1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 0); + verify(handler).sendPck("TXD0-------"); + } + + @Test + public void testHandleCommandD1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 0); + verify(handler).sendPck("TXD1-------"); + } + + @Test + public void testHandleCommandD8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 7); + verify(handler).sendPck("TXD-------0"); + } + + @Test + public void testHandleCommandD8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 7); + verify(handler).sendPck("TXD-------1"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java new file mode 100644 index 0000000000000..aea07c1f19923 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java @@ -0,0 +1,84 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLedSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleLedSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleLedSubHandler(handler, info); + } + + @Test + public void testHandleCommandLed1Off() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.OFF.name()), 0); + verify(handler).sendPck("LA001A"); + } + + @Test + public void testHandleCommandLed1On() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 0); + verify(handler).sendPck("LA001E"); + } + + @Test + public void testHandleCommandLed1Blink() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.BLINK.name()), 0); + verify(handler).sendPck("LA001B"); + } + + @Test + public void testHandleCommandLed1Flicker() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.FLICKER.name()), 0); + verify(handler).sendPck("LA001F"); + } + + @Test + public void testHandleCommandLed12On() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 11); + verify(handler).sendPck("LA012E"); + } + + @Test + public void testHandleOnOffCommandLed1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.LED, 0); + verify(handler).sendPck("LA001A"); + } + + @Test + public void testHandleOnOffCommandLed1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.LED, 0); + verify(handler).sendPck("LA001E"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java new file mode 100644 index 0000000000000..106fd18f6d45d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java @@ -0,0 +1,106 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLogicSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private static final StringType ON = new StringType("ON"); + private static final StringType OFF = new StringType("OFF"); + private static final StringType BLINK = new StringType("BLINK"); + private static final StringType FLICKER = new StringType("FLICKER"); + private static final StringType NOT = new StringType("NOT"); + private static final StringType OR = new StringType("OR"); + private static final StringType AND = new StringType("AND"); + private @NonNullByDefault({}) LcnModuleLogicSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleLogicSubHandler(handler, info); + } + + @Test + public void testStatusLedOffLogicNot() { + l.tryParse("=M000005.TLAAAAAAAAAAAANNNN"); + verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "2", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "3", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "4", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "12", OFF); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", NOT); + } + + @Test + public void testStatusMixed() { + l.tryParse("=M000005.TLAEBFAAAAAAAFNVNT"); + verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "2", ON); + verify(handler).updateChannel(LcnChannelGroup.LED, "3", BLINK); + verify(handler).updateChannel(LcnChannelGroup.LED, "4", FLICKER); + verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "12", FLICKER); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", AND); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR); + } + + @Test + public void testStatusSingleLogic1Not() { + l.tryParse("=M000005S1000"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + } + + @Test + public void testStatusSingleLogic4Or() { + l.tryParse("=M000005S4025"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR); + } + + @Test + public void testStatusSingleLogic3And() { + l.tryParse("=M000005S3050"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", AND); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java new file mode 100644 index 0000000000000..e18eebff250cc --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java @@ -0,0 +1,198 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleOutputSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleOutputSubHandler(handler, info); + } + + @Test + public void testStatusOutput1OffPercent() { + l.tryParse("=M000005A1000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0)); + } + + @Test + public void testStatusOutput2OffPercent() { + l.tryParse("=M000005A2000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0)); + } + + @Test + public void testStatusOutput1OffNative() { + l.tryParse("=M000005O1000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0)); + } + + @Test + public void testStatusOutput2OffNative() { + l.tryParse("=M000005O2000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0)); + } + + @Test + public void testStatusOutput1OnPercent() { + l.tryParse("=M000005A1100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100)); + } + + @Test + public void testStatusOutput2OnPercent() { + l.tryParse("=M000005A2100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100)); + } + + @Test + public void testStatusOutput1OnNative() { + l.tryParse("=M000005O1200"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100)); + } + + @Test + public void testStatusOutput2OnNative() { + l.tryParse("=M000005O2200"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100)); + } + + @Test + public void testStatusOutput2On50Percent() { + l.tryParse("=M000005A2050"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(50)); + } + + @Test + public void testStatusOutput1On50Native() { + l.tryParse("=M000005O1100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(50)); + } + + @Test + public void testHandleCommandOutput1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI100000"); + } + + @Test + public void testHandleCommandOutput2On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI100000"); + } + + @Test + public void testHandleCommandOutput1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI000000"); + } + + @Test + public void testHandleCommandOutput2Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI000000"); + } + + @Test + public void testHandleCommandOutput1Percent10() throws LcnException { + l.handleCommandPercent(new PercentType(99), LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI099000"); + } + + @Test + public void testHandleCommandOutput2Percent1() throws LcnException { + l.handleCommandPercent(new PercentType(1), LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI001000"); + } + + @Test + public void testHandleCommandOutput1Percent995() throws LcnException { + l.handleCommandPercent(new PercentType(BigDecimal.valueOf(99.5)), LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("O1DI199000"); + } + + @Test + public void testHandleCommandOutput2Percent05() throws LcnException { + l.handleCommandPercent(new PercentType(BigDecimal.valueOf(0.5)), LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("O2DI001000"); + } + + @Test + public void testHandleCommandDimmerOutputAll60FixedRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(60), true, false, LcnDefs.FIXED_RAMP_MS), + 0); + verify(handler).sendPck("AH060"); + } + + @Test + public void testHandleCommandDimmerOutputAll40CustomRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), true, false, 1000), 0); + verify(handler).sendPck("OY080080080080004"); + } + + @Test + public void testHandleCommandDimmerOutput12Value100FixedRamp() throws LcnException { + l.handleCommandDimmerOutput( + new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, LcnDefs.FIXED_RAMP_MS), 0); + verify(handler).sendPck("X2001200200"); + } + + @Test + public void testHandleCommandDimmerOutput12Value0FixedRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, LcnDefs.FIXED_RAMP_MS), + 0); + verify(handler).sendPck("X2001000000"); + } + + @Test + public void testHandleCommandDimmerOutput12Value100NoRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, 0), 0); + verify(handler).sendPck("X2001253253"); + } + + @Test + public void testHandleCommandDimmerOutput12Value0NoRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, 0), 0); + verify(handler).sendPck("X2001252252"); + } + + @Test + public void testHandleCommandDimmerOutput12Value40() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), false, true, 0), 0); + verify(handler).sendPck("AY040040"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java new file mode 100644 index 0000000000000..25c181309b02e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java @@ -0,0 +1,114 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRelaySubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRelaySubHandler(handler, info); + } + + @Test + public void testStatusAllOff() { + l.tryParse("=M000005Rx000"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF); + } + + @Test + public void testStatusAllOn() { + l.tryParse("=M000005Rx255"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.ON); + } + + @Test + public void testStatusRelay1Relay7On() { + l.tryParse("=M000005Rx065"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF); + } + + @Test + public void testHandleCommandRelay1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 0); + verify(handler).sendPck("R81-------"); + } + + @Test + public void testHandleCommandRelay8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------1"); + } + + @Test + public void testHandleCommandRelay1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 0); + verify(handler).sendPck("R80-------"); + } + + @Test + public void testHandleCommandRelay8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------0"); + } + + @Test + public void testHandleCommandRelay8Percent1() throws LcnException { + l.handleCommandPercent(new PercentType(1), LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------1"); + } + + @Test + public void testHandleCommandRelay1Percent0() throws LcnException { + l.handleCommandPercent(PercentType.ZERO, LcnChannelGroup.RELAY, 0); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java new file mode 100644 index 0000000000000..3809f0a099788 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java @@ -0,0 +1,66 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRollershutterOutputSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRollershutterOutputSubHandler(handler, info); + } + + @Test + public void testUp() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A1DI100008"); + } + + @Test + public void testDown() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A2DI100008"); + } + + @Test + public void testStop() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A1DI000000"); + verify(handler).sendPck("A2DI000000"); + } + + @Test + public void testMove() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A2DI100008"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java new file mode 100644 index 0000000000000..99816dc13d055 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java @@ -0,0 +1,89 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRollershutterRelaySubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRollershutterRelaySubHandler(handler, info); + } + + @Test + public void testUp1() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R810------"); + } + + @Test + public void testUp4() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------10"); + } + + @Test + public void testDown1() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R811------"); + } + + @Test + public void testDown4() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------11"); + } + + @Test + public void testStop1() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R80-------"); + } + + @Test + public void testStop4() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------0-"); + } + + @Test + public void testMove1() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R81-------"); + } + + @Test + public void testMove4() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------1-"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java new file mode 100644 index 0000000000000..8acf7e33e4c63 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java @@ -0,0 +1,64 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarLockSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRvarLockSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRvarLockSubHandler(handler, info); + } + + @Test + public void testLock1() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 0); + verify(handler).sendPck("REAXS"); + } + + @Test + public void testLock2() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 1); + verify(handler).sendPck("REBXS"); + } + + @Test + public void testUnlock1() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 0); + verify(handler).sendPck("REAXA"); + } + + @Test + public void testUnlock2() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 1); + verify(handler).sendPck("REBXA"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java new file mode 100644 index 0000000000000..be0d3df53395b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java @@ -0,0 +1,138 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarSetpointSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRvarSetpointSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRvarSetpointSubHandler(handler, info); + } + + @Test + public void testhandleCommandRvar1Positive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1000), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("X2030032000"); + } + + @Test + public void testhandleCommandRvar2Positive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("X2030096100"); + } + + @Test + public void testhandleCommandRvar1Negative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(0), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("X2030043232"); + } + + @Test + public void testhandleCommandRvar2Negative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(999), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("X2030104001"); + } + + @Test + public void testhandleCommandRvar1PositiveLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("REASA+100"); + } + + @Test + public void testhandleCommandRvar2PositiveLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("REBSA+100"); + } + + @Test + public void testhandleCommandRvar1NegativeLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("REASA-100"); + } + + @Test + public void testhandleCommandRvar2NegativeLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("REBSA-100"); + } + + @Test + public void testRvar1() { + l.tryParse("=M000005.S11234"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF); + } + + @Test + public void testRvar2() { + l.tryParse("=M000005.S21234"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.OFF); + } + + @Test + public void testRvar1SensorDefective() { + l.tryParse("=M000005.S132512"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new StringType("DEFECTIVE")); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF); + } + + @Test + public void testRvar1Locked() { + l.tryParse("=M000005.S134002"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.ON); + } + + @Test + public void testRvar2Locked() { + l.tryParse("=M000005.S234002"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.ON); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java new file mode 100644 index 0000000000000..4f5f900c17e35 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java @@ -0,0 +1,57 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleS0CounterSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleS0CounterSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleS0CounterSubHandler(handler, info); + } + + @Test + public void testZero() { + l.tryParse("=M000005.C10"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(0)); + } + + @Test + public void testMaxValue() { + l.tryParse("=M000005.C14294967295"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(4294967295L)); + } + + @Test + public void test4() { + l.tryParse("=M000005.C412345"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "4", new DecimalType(12345)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java new file mode 100644 index 0000000000000..ad3ee3bb85ab5 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java @@ -0,0 +1,133 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleThresholdSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleThresholdSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleThresholdSubHandler(handler, info); + } + + @Test + public void testThreshold11() { + l.tryParse("=M000005.T1112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345)); + } + + @Test + public void testThreshold14() { + l.tryParse("=M000005.T140"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0)); + } + + @Test + public void testThreshold41() { + l.tryParse("=M000005.T4112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER4, "1", new DecimalType(12345)); + } + + @Test + public void testThresholdLegacy() { + l.tryParse("=M000005.S1123451123411123000000000112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "2", new DecimalType(11234)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "3", new DecimalType(11123)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "5", new DecimalType(1)); + } + + @Test + public void testhandleCommandThreshold11Positive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100AR11"); + } + + @Test + public void testhandleCommandThreshold11Negative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100SR11"); + } + + @Test + public void testhandleCommandThreshold44Positive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER4, 3); + verify(handler).sendPck("SSR0100AR44"); + } + + @Test + public void testhandleCommandThreshold44Negative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER4, 3); + verify(handler).sendPck("SSR0100SR44"); + } + + @Test + public void testhandleCommandThreshold11LegacyPositive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100A10000"); + } + + @Test + public void testhandleCommandThreshold11LegacyNegative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100S10000"); + } + + @Test + public void testhandleCommandThreshold14Legacy() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER14)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 3); + verify(handler).sendPck("SSR0100A00010"); + } + + @Test + public void testhandleCommandThreshold15Legacy() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER15)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 4); + verify(handler).sendPck("SSR0100A00001"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java new file mode 100644 index 0000000000000..de976ee4acb8d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java @@ -0,0 +1,89 @@ +/** + * 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.lcn.internal.subhandler; + +import static org.mockito.Mockito.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleVariableSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleVariableSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleVariableSubHandler(handler, info); + } + + @Test + public void testStatusVariable1() { + l.tryParse("=M000005.A00112345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "1", new DecimalType(12345)); + } + + @Test + public void testStatusVariable12() { + l.tryParse("=M000005.A01212345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "12", new DecimalType(12345)); + } + + @Test + public void testStatusLegacyVariable3() { + when(info.getLastRequestedVarWithoutTypeInResponse()).thenReturn(Variable.VARIABLE3); + l.tryParse("=M000005.12345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "3", new DecimalType(12345)); + } + + @Test + public void testHandleCommandLegacyTvarPositive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(1000L); + l.handleCommandDecimal(new DecimalType(1234), LcnChannelGroup.VARIABLE, 0); + verify(handler).sendPck("ZA234"); + } + + @Test + public void testHandleCommandLegacyTvarNegative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(2000L); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.VARIABLE, 0); + verify(handler).sendPck("ZS900"); + } + + @Test + public void testStatusVariable10SensorDefective() { + l.tryParse("=M000005.A01032512"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "10", new StringType("DEFECTIVE")); + } + + @Test + public void testStatusVariable8NotConfigured() { + l.tryParse("=M000005.A00865535"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "8", new StringType("Not configured in LCN-PRO")); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index b01a8236568ab..7259eabff90db 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -131,6 +131,7 @@ org.openhab.binding.konnected org.openhab.binding.kostalinverter org.openhab.binding.lametrictime + org.openhab.binding.lcn org.openhab.binding.leapmotion org.openhab.binding.lghombot org.openhab.binding.lgtvserial From 0f9e99da3052dfe968fe30629d91d0b706d55325 Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Thu, 18 Jun 2020 20:35:31 +0200 Subject: [PATCH 56/83] [netatmo] Add support for the floodlight of the Presence camera (#7927) * [#7912] Support for the floodlight of the Presence camera - Support for the floodlight of the Presence camera added (There are 2 new switches to set the floodlight auto-mode and to switch the floodlight on and off/auto). - Netatmo API swagger spec updated - Tests added * Exception handling and logging corrected * Potential crash fixed which could occur when a wrong JSON response is returned by the ping command request (when it is a valid JSON but without the expected attribute "local_url"). Signed-off-by: Sven Strohschein --- bundles/org.openhab.binding.netatmo/README.md | 49 +-- bundles/org.openhab.binding.netatmo/pom.xml | 2 +- .../internal/NetatmoBindingConstants.java | 4 + .../internal/NetatmoHandlerFactory.java | 5 +- .../internal/camera/CameraHandler.java | 6 +- .../internal/presence/CameraAddress.java | 65 ++++ .../presence/NAPresenceCameraHandler.java | 176 +++++++++ .../welcome/NAWelcomeHomeHandler.java | 3 +- .../main/resources/ESH-INF/thing/camera.xml | 14 + .../presence/NAPresenceCameraHandlerTest.java | 346 ++++++++++++++++++ 10 files changed, 643 insertions(+), 27 deletions(-) create mode 100644 bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java create mode 100644 bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java create mode 100644 bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java diff --git a/bundles/org.openhab.binding.netatmo/README.md b/bundles/org.openhab.binding.netatmo/README.md index 8c683a6d05871..3c9bff21af24c 100644 --- a/bundles/org.openhab.binding.netatmo/README.md +++ b/bundles/org.openhab.binding.netatmo/README.md @@ -502,34 +502,41 @@ All these channels are read only. ### Welcome and Presence Camera -All these channels are read only. - -Warning : the URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding. +Warning: The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding. So to get a refreshed picture, you need to use the refresh parameter in your sitemap image element. **Supported channels for the Welcome Camera thing:** -| Channel ID | Item Type | Description | -|-----------------------------|-----------|----------------------------------------------------------| -| welcomeCameraStatus | Switch | State of the camera | -| welcomeCameraSdStatus | Switch | State of the SD card | -| welcomeCameraAlimStatus | Switch | State of the power connector | -| welcomeCameraIsLocal | Switch | indicates whether the camera is on the same network than the openHAB Netatmo Binding | -| welcomeCameraLivePicture | Image | Camera Live Snapshot | -| welcomeCameraLivePictureUrl | String | Url of the live snapshot for this camera | -| welcomeCameraLiveStreamUrl | String | Url of the live stream for this camera | +| Channel ID | Item Type | Read/Write | Description | +|-----------------------------|-----------|------------|--------------------------------------------------------------| +| welcomeCameraStatus | Switch | Read-only | State of the camera | +| welcomeCameraSdStatus | Switch | Read-only | State of the SD card | +| welcomeCameraAlimStatus | Switch | Read-only | State of the power connector | +| welcomeCameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding | +| welcomeCameraLivePicture | Image | Read-only | Camera Live Snapshot | +| welcomeCameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera | +| welcomeCameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera | **Supported channels for the Presence Camera thing:** -| Channel ID | Item Type | Description | -|-----------------------------|-----------|----------------------------------------------------------| -| cameraStatus | Switch | State of the camera | -| cameraSdStatus | Switch | State of the SD card | -| cameraAlimStatus | Switch | State of the power connector | -| cameraIsLocal | Switch | indicates whether the camera is on the same network than the openHAB Netatmo Binding | -| cameraLivePicture | Image | Camera Live Snapshot | -| cameraLivePictureUrl | String | Url of the live snapshot for this camera | -| cameraLiveStreamUrl | String | Url of the live stream for this camera | +Warnings: +- Some features like the floodlight are accessed via the local network, so it may be helpful to set a static IP address +for the Presence camera within your local network. +- The floodlight auto-mode (cameraFloodlightAutoMode) isn't updated it is changed by another application. Therefore the +binding handles its own state of the auto-mode. This has the advantage that the user can define its own floodlight +switch off behaviour. + +| Channel ID | Item Type | Read/Write | Description | +|-----------------------------|-----------|------------|--------------------------------------------------------------| +| cameraStatus | Switch | Read-only | State of the camera | +| cameraSdStatus | Switch | Read-only | State of the SD card | +| cameraAlimStatus | Switch | Read-only | State of the power connector | +| cameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding | +| cameraLivePicture | Image | Read-only | Camera Live Snapshot | +| cameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera | +| cameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera | +| cameraFloodlightAutoMode | Switch | Read-write | When set the floodlight gets switched to auto instead of off | +| cameraFloodlight | Switch | Read-write | Switch for the floodlight | ### Welcome Person diff --git a/bundles/org.openhab.binding.netatmo/pom.xml b/bundles/org.openhab.binding.netatmo/pom.xml index cb05df716d9fd..edc5c5ec82c12 100644 --- a/bundles/org.openhab.binding.netatmo/pom.xml +++ b/bundles/org.openhab.binding.netatmo/pom.xml @@ -69,7 +69,7 @@ generate - https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/c95b5f5003d91ac9e73e9c855569263cb9013cc2/spec/swagger.yaml + https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/8ba3583f8d851e0b0c0bb4c5066338fe4898cb11/spec/swagger.yaml java retrofit diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java index 3e4c472bb5b9f..601f5b11711b8 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java @@ -271,6 +271,10 @@ public class NetatmoBindingConstants { public static final String WELCOME_PICTURE_IMAGEID = "image_id"; public static final String WELCOME_PICTURE_KEY = "key"; + // Presence outdoor camera specific channels + public static final String CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE = "cameraFloodlightAutoMode"; + public static final String CHANNEL_CAMERA_FLOODLIGHT = "cameraFloodlight"; + // List of all supported physical devices and modules public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Stream .of(MAIN_THING_TYPE, MODULE1_THING_TYPE, MODULE2_THING_TYPE, MODULE3_THING_TYPE, MODULE4_THING_TYPE, diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java index fb508ea07f401..b1d948b83ec0d 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java @@ -34,6 +34,7 @@ import org.openhab.binding.netatmo.internal.discovery.NetatmoModuleDiscoveryService; import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; import org.openhab.binding.netatmo.internal.homecoach.NAHealthyHomeCoachHandler; +import org.openhab.binding.netatmo.internal.presence.NAPresenceCameraHandler; import org.openhab.binding.netatmo.internal.station.NAMainHandler; import org.openhab.binding.netatmo.internal.station.NAModule1Handler; import org.openhab.binding.netatmo.internal.station.NAModule2Handler; @@ -109,8 +110,10 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new NATherm1Handler(thing, stateDescriptionProvider, timeZoneProvider); } else if (thingTypeUID.equals(WELCOME_HOME_THING_TYPE)) { return new NAWelcomeHomeHandler(thing, timeZoneProvider); - } else if (thingTypeUID.equals(WELCOME_CAMERA_THING_TYPE) || thingTypeUID.equals(PRESENCE_CAMERA_THING_TYPE)) { + } else if (thingTypeUID.equals(WELCOME_CAMERA_THING_TYPE)) { return new NAWelcomeCameraHandler(thing, timeZoneProvider); + } else if (thingTypeUID.equals(PRESENCE_CAMERA_THING_TYPE)) { + return new NAPresenceCameraHandler(thing, timeZoneProvider); } else if (thingTypeUID.equals(WELCOME_PERSON_THING_TYPE)) { return new NAWelcomePersonHandler(thing, timeZoneProvider); } else { diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java index 74e13492ca1aa..2c84de4eff119 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java @@ -33,11 +33,11 @@ * NAWelcomeCameraHandler) * */ -public class CameraHandler extends NetatmoModuleHandler { +public abstract class CameraHandler extends NetatmoModuleHandler { private static final String LIVE_PICTURE = "/live/snapshot_720.jpg"; - public CameraHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) { + protected CameraHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) { super(thing, timeZoneProvider); } @@ -133,7 +133,7 @@ private String getLiveStreamURL() { } @SuppressWarnings("null") - private String getVpnUrl() { + protected String getVpnUrl() { return (module == null) ? null : module.getVpnUrl(); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java new file mode 100644 index 0000000000000..ce55a1a5c3045 --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java @@ -0,0 +1,65 @@ +/** + * 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.netatmo.internal.presence; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.Objects; + +/** + * {@link CameraAddress} handles the data to address a camera (VPN and local address). + * + * @author Sven Strohschein + */ +@NonNullByDefault +public class CameraAddress { + + private final String vpnURL; + private final String localURL; + + CameraAddress(final String vpnURL, final String localURL) { + this.vpnURL = vpnURL; + this.localURL = localURL; + } + + public String getVpnURL() { + return vpnURL; + } + + public String getLocalURL() { + return localURL; + } + + /** + * Checks if the VPN URL was changed / isn't equal to the given VPN-URL. + * @param vpnURL old / known VPN URL + * @return true, when the VPN URL isn't equal given VPN URL, otherwise false + */ + public boolean isVpnURLChanged(String vpnURL) { + return !getVpnURL().equals(vpnURL); + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + CameraAddress that = (CameraAddress) object; + return vpnURL.equals(that.vpnURL) && localURL.equals(that.localURL); + } + + @Override + public int hashCode() { + return Objects.hash(vpnURL, localURL); + } +} diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java new file mode 100644 index 0000000000000..133240b1d54f2 --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java @@ -0,0 +1,176 @@ +/** + * 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.netatmo.internal.presence; + +import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.toOnOffType; + +import io.swagger.client.model.NAWelcomeCamera; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.net.http.HttpUtil; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.binding.netatmo.internal.camera.CameraHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Optional; + +import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT; +import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE; + +/** + * {@link NAPresenceCameraHandler} is the class used to handle Presence camera data + * + * @author Sven Strohschein + */ +@NonNullByDefault +public class NAPresenceCameraHandler extends CameraHandler { + + private static final String PING_URL_PATH = "/command/ping"; + private static final String FLOODLIGHT_SET_URL_PATH = "/command/floodlight_set_config"; + + private final Logger logger = LoggerFactory.getLogger(NAPresenceCameraHandler.class); + + private Optional cameraAddress = Optional.empty(); + private State floodlightAutoModeState = UnDefType.UNDEF; + + public NAPresenceCameraHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) { + super(thing, timeZoneProvider); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channelId = channelUID.getId(); + switch (channelId) { + case CHANNEL_CAMERA_FLOODLIGHT: + if (command == OnOffType.ON) { + switchFloodlight(true); + } else if (command == OnOffType.OFF) { + switchFloodlight(false); + } + break; + case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE: + if (command == OnOffType.ON) { + switchFloodlightAutoMode(true); + } else if (command == OnOffType.OFF) { + switchFloodlightAutoMode(false); + } + break; + } + super.handleCommand(channelUID, command); + } + + @Override + protected State getNAThingProperty(@NonNull String channelId) { + switch (channelId) { + case CHANNEL_CAMERA_FLOODLIGHT: + return getFloodlightState(); + case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE: + //The auto-mode state shouldn't be updated, because this isn't a dedicated information. When the + // floodlight is switched on the state within the Netatmo API is "on" and the information if the previous + // state was "auto" instead of "off" is lost... Therefore the binding handles its own auto-mode state. + if (floodlightAutoModeState == UnDefType.UNDEF) { + floodlightAutoModeState = getFloodlightAutoModeState(); + } + return floodlightAutoModeState; + } + return super.getNAThingProperty(channelId); + } + + private State getFloodlightState() { + if (module != null) { + final boolean isOn = module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON; + return toOnOffType(isOn); + } + return UnDefType.UNDEF; + } + + private State getFloodlightAutoModeState() { + if (module != null) { + return toOnOffType(module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO); + } + return UnDefType.UNDEF; + } + + private void switchFloodlight(boolean isOn) { + if (isOn) { + changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.ON); + } else { + switchFloodlightAutoMode(floodlightAutoModeState == OnOffType.ON); + } + } + + private void switchFloodlightAutoMode(boolean isAutoMode) { + floodlightAutoModeState = toOnOffType(isAutoMode); + if (isAutoMode) { + changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.AUTO); + } else { + changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.OFF); + } + } + + private void changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum mode) { + Optional localCameraURL = getLocalCameraURL(); + if (localCameraURL.isPresent()) { + String url = localCameraURL.get() + + FLOODLIGHT_SET_URL_PATH + + "?config=%7B%22mode%22:%22" + + mode.toString() + + "%22%7D"; + executeGETRequest(url); + } + } + + private Optional getLocalCameraURL() { + String vpnURL = getVpnUrl(); + if (vpnURL != null) { + //The local address is (re-)requested when it wasn't already determined or when the vpn address was changed. + if (!cameraAddress.isPresent() || cameraAddress.get().isVpnURLChanged(vpnURL)) { + Optional json = executeGETRequestJSON(vpnURL + PING_URL_PATH); + cameraAddress = json.map(j -> j.optString("local_url", null)) + .map(localURL -> new CameraAddress(vpnURL, localURL)); + } + } + return cameraAddress.map(CameraAddress::getLocalURL); + } + + private Optional executeGETRequestJSON(String url) { + try { + return executeGETRequest(url).map(JSONObject::new); + } catch (JSONException e) { + logger.warn("Error on parsing the content as JSON!", e); + } + return Optional.empty(); + } + + Optional executeGETRequest(String url) { + try { + String content = HttpUtil.executeUrl("GET", url, 5000); + if (content != null && !content.isEmpty()) { + return Optional.of(content); + } + } catch (IOException e) { + logger.warn("Error on accessing local camera url!", e); + } + return Optional.empty(); + } +} diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java index 9c39152311344..67dabbd2807e4 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java @@ -32,6 +32,7 @@ import org.eclipse.smarthome.core.types.UnDefType; import org.eclipse.smarthome.io.net.http.HttpUtil; import org.openhab.binding.netatmo.internal.ChannelTypeUtils; +import org.openhab.binding.netatmo.internal.camera.CameraHandler; import org.openhab.binding.netatmo.internal.handler.AbstractNetatmoThingHandler; import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler; import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; @@ -145,7 +146,7 @@ protected State getNAThingProperty(String channelId) { String cameraId = lastEvent.get().getCameraId(); Optional thing = getBridgeHandler().findNAThing(cameraId); if (thing.isPresent()) { - NAWelcomeCameraHandler eventCamera = (NAWelcomeCameraHandler) thing.get(); + CameraHandler eventCamera = (CameraHandler) thing.get(); String streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId()); if (streamUrl != null) { return new StringType(streamUrl); diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml index 06675b5b12e94..be0698f6e5af5 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml @@ -43,6 +43,8 @@ + + id @@ -100,4 +102,16 @@ + + Switch + + State of the floodlight auto-mode + + + + Switch + + State of the floodlight + + diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java new file mode 100644 index 0000000000000..94c4e8c0bd981 --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java @@ -0,0 +1,346 @@ +/** + * 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.netatmo.internal.presence; + +import io.swagger.client.model.NAWelcomeCamera; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.core.internal.i18n.I18nProviderImpl; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.internal.ThingImpl; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.UnDefType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.openhab.binding.netatmo.internal.NetatmoBindingConstants; + +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Sven Strohschein + */ +@RunWith(MockitoJUnitRunner.class) +public class NAPresenceCameraHandlerTest { + + private static final String DUMMY_VPN_URL = "https://dummytestvpnaddress.net/restricted/10.255.89.96/9826069dc689e8327ac3ed2ced4ff089/MTU5MTgzMzYwMDrQ7eHHhG0_OJ4TgmPhGlnK7QQ5pZ,,"; + private static final String DUMMY_LOCAL_URL = "http://192.168.178.76/9826069dc689e8327ac3ed2ced4ff089"; + private static final Optional DUMMY_PING_RESPONSE = createPingResponseContent(DUMMY_LOCAL_URL); + + @Mock + private RequestExecutor requestExecutorMock; + + private Thing presenceCameraThing; + private NAWelcomeCamera presenceCamera; + private ChannelUID floodlightChannelUID; + private ChannelUID floodlightAutoModeChannelUID; + private NAPresenceCameraHandler handler; + + @Before + public void before() { + presenceCameraThing = new ThingImpl(new ThingTypeUID("netatmo", "NOC"), "1"); + presenceCamera = new NAWelcomeCamera(); + floodlightChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT); + floodlightAutoModeChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE); + + handler = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()) { + { + module = presenceCamera; + } + + @Override + @NonNull Optional<@NonNull String> executeGETRequest(@NonNull String url) { + return requestExecutorMock.executeGETRequest(url); + } + }; + } + + @Test + public void testHandleCommand_Switch_Floodlight_on() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch on + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D"); + } + + @Test + public void testHandleCommand_Switch_Floodlight_off() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.OFF); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch off + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D"); + } + + @Test + public void testHandleCommand_Switch_Floodlight_off_with_AutoMode_on() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + + handler.handleCommand(floodlightChannelUID, OnOffType.OFF); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch off + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22auto%22%7D"); + } + + @Test + public void testHandleCommand_Switch_Floodlight_on_address_changed() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + //1.) execute ping + 2.) execute switch on + verify(requestExecutorMock, times(2)).executeGETRequest(any()); + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D"); + + handler.handleCommand(floodlightChannelUID, OnOffType.OFF); + //1.) execute ping + 2.) execute switch on + 3.) execute switch off + verify(requestExecutorMock, times(3)).executeGETRequest(any()); + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D"); + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D"); + + final String newDummyVPNURL = DUMMY_VPN_URL + "2"; + final String newDummyLocalURL = DUMMY_LOCAL_URL + "2"; + final Optional newDummyPingResponse = createPingResponseContent(newDummyLocalURL); + + when(requestExecutorMock.executeGETRequest(newDummyVPNURL + "/command/ping")).thenReturn(newDummyPingResponse); + + presenceCamera.setVpnUrl(newDummyVPNURL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + //1.) execute ping + 2.) execute switch on + 3.) execute switch off + 4.) execute ping + 5.) execute switch on + verify(requestExecutorMock, times(5)).executeGETRequest(any()); + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D"); + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D"); + verify(requestExecutorMock).executeGETRequest(newDummyLocalURL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D"); + } + + @Test + public void testHandleCommand_Switch_Floodlight_unknown_command() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, RefreshType.REFRESH); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed on a refresh command + } + + @Test + public void testHandleCommand_Switch_FloodlightAutoMode_on() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + + handler.handleCommand(floodlightAutoModeChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch auto-mode on + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22auto%22%7D"); + } + + @Test + public void testHandleCommand_Switch_FloodlightAutoMode_off() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + + handler.handleCommand(floodlightAutoModeChannelUID, OnOffType.OFF); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch off + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D"); + } + + @Test + public void testHandleCommand_Switch_FloodlightAutoMode_unknown_command() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightAutoModeChannelUID, RefreshType.REFRESH); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed on a refresh command + } + + /** + * The request "fails" because there is no response content of the ping command. + */ + @Test + public void testHandleCommand_Request_failed() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(1)).executeGETRequest(any()); //1.) execute ping + } + + @Test + public void testHandleCommand_VPN_URL_not_set() { + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //no executions because the VPN URL is still unknown + } + + @Test + public void testHandleCommand_Ping_failed_NULL_Response() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(Optional.of("")); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(1)).executeGETRequest(any()); //1.) execute ping + } + + @Test + public void testHandleCommand_Ping_failed_empty_Response() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(Optional.of("")); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(1)).executeGETRequest(any()); //1.) execute ping + } + + @Test + public void testHandleCommand_Ping_failed_wrong_Response() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(Optional.of("{ \"message\": \"error\" }")); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(1)).executeGETRequest(any()); //1.) execute ping + } + + @Test + public void testHandleCommand_Module_NULL() { + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + handlerWithoutModule.handleCommand(floodlightChannelUID, OnOffType.ON); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //no executions because the thing isn't initialized + } + + @Test + public void testGetNAThingProperty_Common_Channel() { + assertEquals(OnOffType.OFF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_CAMERA_STATUS)); + } + + @Test + public void testGetNAThingProperty_Floodlight_On() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_Off() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_Auto() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO); + //When the floodlight is set to auto-mode it is currently off. + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_without_LightModeState() { + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_Module_NULL() { + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_FloodlightAutoMode_Floodlight_Auto() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_FloodlightAutoMode_Floodlight_On() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON); + //When the floodlight is initially on (on starting the binding), there is no information about if the auto-mode + // was set before. Therefore the auto-mode is detected as deactivated / off. + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_FloodlightAutoMode_Floodlight_Off() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON); + //When the floodlight is initially off (on starting the binding), the auto-mode isn't set. + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_Scenario_with_AutoMode() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + + //The auto-mode was initially set, after that the floodlight was switched on by the user. + // In this case the binding should still know that the auto-mode is/was set. + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId())); + + //After that the user switched off the floodlight. + // In this case the binding should still know that the auto-mode is/was set. + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_Floodlight_Scenario_without_AutoMode() { + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + + //The auto-mode wasn't set, after that the floodlight was switched on by the user. + // In this case the binding should still know that the auto-mode isn't/wasn't set. + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId())); + + //After that the user switched off the floodlight. + // In this case the binding should still know that the auto-mode isn't/wasn't set. + presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId())); + } + + @Test + public void testGetNAThingProperty_FloodlightAutoMode_Module_NULL() { + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightAutoModeChannelUID.getId())); + } + + private static Optional createPingResponseContent(final String localURL) { + return Optional.of("{\"local_url\":\"" + localURL + "\",\"product_name\":\"Welcome Netatmo\"}"); + } + + private interface RequestExecutor { + + Optional executeGETRequest(String url); + } +} From 22978950b0ae18dad98837313eba377ecee0c9f4 Mon Sep 17 00:00:00 2001 From: LeeC77 Date: Thu, 18 Jun 2020 19:37:25 +0100 Subject: [PATCH 57/83] [squeezebox] Add some examples to the .items file example (#7925) * Add some example for the .items file As I struggled to sort out exactly how to set up the .items file for the item type 'image' and define the album art channel I though the documentation could be clarified with an example or 3. * Update README.md --- bundles/org.openhab.binding.squeezebox/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.squeezebox/README.md b/bundles/org.openhab.binding.squeezebox/README.md index 05ebe0a973a96..41de13e1d93c4 100644 --- a/bundles/org.openhab.binding.squeezebox/README.md +++ b/bundles/org.openhab.binding.squeezebox/README.md @@ -115,6 +115,16 @@ All devices support some of the following channels: | numberPlaylistTracks | Number | Number of playlist tracks | | playFavorite | String | ID of Favorite to play (channel's state options contains available favorites) | +## Example .Items File + +Add the items you want to the .items file. A few examples are shown below, the power, volume and album art channels are connected here to the items by copying across the channel discriptions from the Paper UI. Make suure each channel is linked for your needs See [openHAB New User Configuration documentation](https://www.openhab.org/docs/tutorial/configuration.html) for further details on linking and channels. + +``` +Switch YourPlayer_Power "Squeezebox Power" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:power"} +Dimmer YourPlayer_Volume "Squeezebox Volume" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:volume"} +Image YourPlayer_AlbumArt "Squeezebox Cover" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:coverartdata"} +``` + ## Playing Favorites Using the **playFavorite** channel, you can play a favorite from the *Favorites* list on the Logitech Media Server (LMS). @@ -128,7 +138,7 @@ Currently, only favorites from the root level of the LMS favorites list are expo - Add some favorites to your favorites list in LMS (local music playlists, Pandora, Slacker, Internet radio, etc.). Keep all favorites at the root level (i.e. favorites in sub-folders will be ignored). -- If you're on an older openHAB build, you may need to delete and readd your squeezebox server and player things to pick up the new channels. +- If you're on an older openHAB build, you may need to delete and re-add your squeezebox server and player things to pick up the new channels. - Create a new item on each player From 8cec10f0446d22f8c0587864ab2308d04848ff23 Mon Sep 17 00:00:00 2001 From: t2000 Date: Thu, 18 Jun 2020 21:05:28 +0200 Subject: [PATCH 58/83] [samsungtv] Make use of representation property in discovery (#7936) Contributes to #6317 and should hide a result if there is already a thing defined which does not have the same ThingUID but points to the same TV. Signed-off-by: Stefan Triller --- .../internal/discovery/SamsungTvDiscoveryParticipant.java | 2 +- .../src/main/resources/ESH-INF/thing/thing-types.xml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/discovery/SamsungTvDiscoveryParticipant.java b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/discovery/SamsungTvDiscoveryParticipant.java index 4bc16cb1fe1b4..cd67d3c682ba1 100644 --- a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/discovery/SamsungTvDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/discovery/SamsungTvDiscoveryParticipant.java @@ -57,7 +57,7 @@ public Set getSupportedThingTypeUIDs() { properties.put(HOST_NAME, device.getIdentity().getDescriptorURL().getHost()); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties) - .withLabel(getLabel(device)).build(); + .withRepresentationProperty(HOST_NAME).withLabel(getLabel(device)).build(); logger.debug("Created a DiscoveryResult for device '{}' with UDN '{}' and properties: {}", device.getDetails().getModelDetails().getModelName(), diff --git a/bundles/org.openhab.binding.samsungtv/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.samsungtv/src/main/resources/ESH-INF/thing/thing-types.xml index 34ced773ef24e..a06e47b38a081 100644 --- a/bundles/org.openhab.binding.samsungtv/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.samsungtv/src/main/resources/ESH-INF/thing/thing-types.xml @@ -28,6 +28,8 @@ + hostName + From 86ad836bcf1b928b87e66919db4428e3be628bf6 Mon Sep 17 00:00:00 2001 From: Benjamin Hahn Date: Thu, 18 Jun 2020 22:35:58 +0200 Subject: [PATCH 59/83] [deconz] Adjust color-temperature handling, Extend bulb support (#7900) * Add property for ct colormode upper and lower limit Signed-off-by: Benajmin Hahn --- bundles/org.openhab.binding.deconz/README.md | 2 +- .../deconz/internal/BindingConstants.java | 9 ++++ .../openhab/binding/deconz/internal/Util.java | 23 +++++++++++ .../discovery/ThingDiscoveryService.java | 24 ++++++++--- .../internal/handler/LightThingHandler.java | 41 +++++++++---------- .../internal/handler/SensorThingHandler.java | 1 - .../deconz/internal/types/LightType.java | 1 + .../types/ThermostatModeGsonTypeAdapter.java | 1 - .../ESH-INF/thing/light-thing-types.xml | 2 +- .../ESH-INF/thing/sensor-thing-types.xml | 2 +- .../openhab/binding/deconz/LightsTest.java | 2 +- .../binding/deconz/colortemperature.json | 2 +- 12 files changed, 77 insertions(+), 33 deletions(-) diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index 26d27b74d31fe..7546554cc5c88 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -147,7 +147,7 @@ Other devices support | brightness | Dimmer | R/W | Brightness of the light | `dimmablelight` | | switch | Switch | R/W | State of a ON/OFF device | `onofflight` | | color | Color | R/W | Color of an multi-color light | `colorlight`, `extendedcolorlight` | -| color_temperature | Number | R/W | `0`->`100` represents cold -> warm | `colortemperaturelight`, `extendedcolorlight` | +| color_temperature | Number | R/W | Color temperature in kelvin. The value range is determined by each individual light | `colortemperaturelight`, `extendedcolorlight` | | position | Rollershutter | R/W | Position of the blind | `windowcovering` | | heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` | | valve | Number:Dimensionless | R | Valve position in % | `thermostat` | diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 76f761456bbbe..85df47e81470d 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -107,4 +107,13 @@ public class BindingConstants { public static final String CONFIG_APIKEY = "apikey"; public static final String UNIQUE_ID = "uid"; + + public static final String PROPERTY_CT_MIN = "ctmin"; + public static final String PROPERTY_CT_MAX = "ctmax"; + + // CT value range according to ZCL Spec + public static final int ZCL_CT_UNDEFINED = 0; // 0x0000 + public static final int ZCL_CT_MIN = 1; + public static final int ZCL_CT_MAX = 65279; // 0xFEFF + public static final int ZCL_CT_INVALID = 65535; // 0xFFFF } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java index f087470cb5cbf..b49b513995835 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java @@ -35,4 +35,27 @@ public static String buildUrl(String host, int port, String... urlParts) { return url.toString(); } + + public static int miredToKelvin(int miredValue) { + return (int) (1000000.0 / miredValue); + } + + public static int kelvinToMired(int kelvinValue) { + return (int) (1000000.0 / kelvinValue); + } + + public static int constrainToRange(int intValue, int min, int max) { + return Math.max(min, Math.min(intValue, max)); + } + + public static int parseIntWithFallback(String text, int defaultValue) { + if (text == null || text.isEmpty()) { + return defaultValue; + } + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return defaultValue; + } + } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java index d275f7955da84..4fbcfc18fdeca 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java @@ -14,6 +14,8 @@ import static org.openhab.binding.deconz.internal.BindingConstants.*; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -26,6 +28,7 @@ import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingHandler; @@ -109,9 +112,19 @@ private void addLight(String lightID, LightMessage light) { return; } - if (light.uniqueid.isEmpty()) { - logger.warn("No unique id reported for light {} ({})", light.modelid, light.name); - return; + Map properties = new HashMap<>(); + properties.put("id", lightID); + properties.put(UNIQUE_ID, light.uniqueid); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion); + properties.put(Thing.PROPERTY_VENDOR, light.manufacturername); + properties.put(Thing.PROPERTY_MODEL_ID, light.modelid); + + if (light.ctmax != null && light.ctmin != null) { + int ctmax = (light.ctmax > ZCL_CT_MAX) ? ZCL_CT_MAX : light.ctmax; + properties.put(PROPERTY_CT_MAX, Integer.toString(ctmax)); + + int ctmin = (light.ctmin < ZCL_CT_MIN) ? ZCL_CT_MIN : light.ctmin; + properties.put(PROPERTY_CT_MIN, Integer.toString(ctmin)); } switch (lightType) { @@ -127,6 +140,7 @@ private void addLight(String lightID, LightMessage light) { thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT; break; case COLOR_DIMMABLE_LIGHT: + case COLOR_LIGHT: thingTypeUID = THING_TYPE_COLOR_LIGHT; break; case EXTENDED_COLOR_LIGHT: @@ -147,8 +161,8 @@ private void addLight(String lightID, LightMessage light) { ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", "")); DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) - .withLabel(light.name + " (" + light.manufacturername + ")").withProperty("id", lightID) - .withProperty(UNIQUE_ID, light.uniqueid).withRepresentationProperty(UNIQUE_ID).build(); + .withLabel(light.name + " (" + light.manufacturername + ")").withProperties(properties) + .withRepresentationProperty(UNIQUE_ID).build(); thingDiscovered(discoveryResult); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java index ca1fdba4b1d87..14dc8994d3d3a 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java @@ -13,7 +13,7 @@ package org.openhab.binding.deconz.internal.handler; import static org.openhab.binding.deconz.internal.BindingConstants.*; -import static org.openhab.binding.deconz.internal.Util.buildUrl; +import static org.openhab.binding.deconz.internal.Util.*; import java.util.Set; import java.util.stream.Collectors; @@ -78,8 +78,13 @@ public class LightThingHandler extends DeconzBaseThingHandler { private LightState lightStateCache = new LightState(); private LightState lastCommand = new LightState(); + private final int ct_max; + private final int ct_min; + public LightThingHandler(Thing thing, Gson gson) { super(thing, gson); + ct_max = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MAX), ZCL_CT_MAX); + ct_min = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MIN), ZCL_CT_MIN); } @Override @@ -169,7 +174,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_COLOR_TEMPERATURE: if (command instanceof DecimalType) { - newLightState.ct = unscaleColorTemperature(((DecimalType) command).doubleValue()); + int miredValue = kelvinToMired(((DecimalType) command).intValue()); + newLightState.ct = constrainToRange(miredValue,ct_min, ct_max); if (currentOn != null && !currentOn) { // sending new color temperature is only allowed when light is on @@ -263,16 +269,17 @@ private void valueUpdated(String channelId, LightState newState) { case CHANNEL_COLOR: if (on != null && on == false) { updateState(channelId, OnOffType.OFF); - } else { - double @Nullable [] xy = newState.xy; - Integer hue = newState.hue; - Integer sat = newState.sat; - if (hue != null && sat != null && bri != null) { - updateState(channelId, - new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri))); - } else if (xy != null && xy.length == 2) { - updateState(channelId, HSBType.fromXY((float) xy[0], (float) xy[1])); + } else if (bri != null && newState.colormode != null && newState.colormode.equals("xy")) { + final double @Nullable [] xy = newState.xy; + if (xy != null && xy.length == 2) { + HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]); + updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri))); } + } else if (bri != null && newState.hue != null && newState.sat != null) { + final Integer hue = newState.hue; + final Integer sat = newState.sat; + updateState(channelId, + new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri))); } break; case CHANNEL_BRIGHTNESS: @@ -284,8 +291,8 @@ private void valueUpdated(String channelId, LightState newState) { break; case CHANNEL_COLOR_TEMPERATURE: Integer ct = newState.ct; - if (ct != null) { - updateState(channelId, new DecimalType(scaleColorTemperature(ct))); + if (ct != null && ct >= ct_min && ct <= ct_max) { + updateState(channelId, new DecimalType(miredToKelvin(ct))); } break; case CHANNEL_POSITION: @@ -315,14 +322,6 @@ public void messageReceived(String sensorID, DeconzBaseMessage message) { } } - private int unscaleColorTemperature(double ct) { - return (int) (ct / 100.0 * (500 - 153) + 153); - } - - private double scaleColorTemperature(int ct) { - return 100.0 * (ct - 153) / (500 - 153); - } - private PercentType toPercentType(int val) { int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR); if (scaledValue < 0 || scaledValue > 100) { diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java index 1f06155bbc6c4..bf3e1d00faafa 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java @@ -238,7 +238,6 @@ protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState createChannel(CHANNEL_GESTURE, ChannelKind.STATE); createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER); } - } @Override diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java index 3d979ef6fd143..578e069c0784b 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java @@ -30,6 +30,7 @@ public enum LightType { ON_OFF_LIGHT("On/Off light"), ON_OFF_PLUGIN_UNIT("On/Off plug-in unit"), EXTENDED_COLOR_LIGHT("Extended color light"), + COLOR_LIGHT("Color light"), COLOR_DIMMABLE_LIGHT("Color dimmable light"), COLOR_TEMPERATURE_LIGHT("Color temperature light"), DIMMABLE_LIGHT("Dimmable light"), diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java index 2b727f2117cdc..652a2e13d751c 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java @@ -48,6 +48,5 @@ public ThermostatMode deserialize(@Nullable JsonElement json, @Nullable Type typ public JsonElement serialize(ThermostatMode src, @Nullable Type typeOfSrc, @Nullable JsonSerializationContext context) throws JsonParseException { return src != ThermostatMode.UNKNOWN ? new JsonPrimitive(src.getDeconzValue()) : JsonNull.INSTANCE; - } } diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/light-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/light-thing-types.xml index 4aa8cba1d0206..6d4005e2687cf 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/light-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/light-thing-types.xml @@ -121,7 +121,7 @@ Number - + diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml index 8c3ac5b38e3a7..5e049e650ddbd 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml @@ -508,7 +508,7 @@ - + diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java index 57657cc144bc4..0366f51c4f271 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java @@ -81,7 +81,7 @@ public void colorTemperatureLightUpdateTest() throws IOException { lightThingHandler.messageReceived("", lightMessage); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21"))); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("87.03170028818444"))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500"))); } @Test diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/colortemperature.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/colortemperature.json index 0af4a94c5ca0c..0e690d172dc0f 100644 --- a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/colortemperature.json +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/colortemperature.json @@ -6,7 +6,7 @@ "alert": null, "bri": 51, "colormode": "ct", - "ct": 455, + "ct": 400, "on": true, "reachable": true }, From 9a47ec159a794b460a417721294dd8d51b130b24 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 18 Jun 2020 19:15:08 -0700 Subject: [PATCH 60/83] [miio] add fan za4 (#7942) Add standing fan za4 Signed-off-by: Marcel Verpaalen marcel@verpaalen.com --- bundles/org.openhab.binding.miio/README.md | 38 ++++ .../binding/miio/internal/MiIoDevices.java | 1 + .../resources/database/zhimi.fan.za4.json | 171 ++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za4.json diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index 51944dedc10a2..064de3d512e28 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -124,6 +124,7 @@ However, for devices that are unsupported, you may override the value and try to | Mi Smart Pedestal Fan | miio:basic | [zhimi.fan.v3](#zhimi-fan-v3) | Yes | | | Xiaomi Mi Smart Pedestal Fan | miio:basic | [zhimi.fan.sa1](#zhimi-fan-sa1) | Yes | | | Xiaomi Mi Smart Pedestal Fan | miio:basic | [zhimi.fan.za1](#zhimi-fan-za1) | Yes | | +| Xiaomi Mi Smart Pedestal Fan | miio:basic | [zhimi.fan.za4](#zhimi-fan-za4) | Yes | | | Viomi Internet refrigerator iLive | miio:unsupported | viomi.fridge.v3 | No | | | Mi Smart Home Gateway v1 | miio:unsupported | lumi.gateway.v1 | No | | | Mi Smart Home Gateway v2 | miio:unsupported | lumi.gateway.v2 | No | | @@ -919,6 +920,23 @@ e.g. `smarthome:send actionCommand 'upd_timer["1498595904821", "on"]'` would ena | acPower | Switch | AC Power | | move | String | Move Direction | +### Xiaomi Mi Smart Pedestal Fan (zhimi.fan.za4) Channels + +| Channel | Type | Description | +|------------------|---------|-------------------------------------| +| power | Switch | Power | +| angleEnable | Switch | Rotation | +| usedhours | Number | Run Time | +| angle | Number | Angle | +| poweroffTime | Number | Timer | +| buzzer | Number | Buzzer | +| led_b | Number | LED | +| child_lock | Switch | Child Lock | +| speedLevel | Number | Speed Level | +| speed | Number | Speed | +| naturalLevel | Number | Natural Level | +| move | String | Move Direction | + ### Mi Humdifier (zhimi.humidifier.v1) Channels | Channel | Type | Description | @@ -2429,6 +2447,26 @@ Switch acPower "AC Power" (G_fan) {channel="miio:basic:fan:acPower"} String move "Move Direction" (G_fan) {channel="miio:basic:fan:move"} ``` +### Xiaomi Mi Smart Pedestal Fan (zhimi.fan.za4) item file lines + +note: Autogenerated example. Replace the id (fan) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +```java +Group G_fan "Xiaomi Mi Smart Pedestal Fan" +Switch power "Power" (G_fan) {channel="miio:basic:fan:power"} +Switch angleEnable "Rotation" (G_fan) {channel="miio:basic:fan:angleEnable"} +Number usedhours "Run Time" (G_fan) {channel="miio:basic:fan:usedhours"} +Number angle "Angle" (G_fan) {channel="miio:basic:fan:angle"} +Number poweroffTime "Timer" (G_fan) {channel="miio:basic:fan:poweroffTime"} +Number buzzer "Buzzer" (G_fan) {channel="miio:basic:fan:buzzer"} +Number led_b "LED" (G_fan) {channel="miio:basic:fan:led_b"} +Switch child_lock "Child Lock" (G_fan) {channel="miio:basic:fan:child_lock"} +Number speedLevel "Speed Level" (G_fan) {channel="miio:basic:fan:speedLevel"} +Number speed "Speed" (G_fan) {channel="miio:basic:fan:speed"} +Number naturalLevel "Natural Level" (G_fan) {channel="miio:basic:fan:naturalLevel"} +String move "Move Direction" (G_fan) {channel="miio:basic:fan:move"} +``` + ### Mi Humdifier (zhimi.humidifier.v1) item file lines note: Autogenerated example. Replace the id (humidifier) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java index d5f1330a894ac..b7fbe5393ab14 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java @@ -73,6 +73,7 @@ public enum MiIoDevices { FAN3("zhimi.fan.v3", "Mi Smart Pedestal Fan", THING_TYPE_BASIC), FAN_SA1("zhimi.fan.sa1", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC), FAN_ZA1("zhimi.fan.za1", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC), + FAN_ZA4("zhimi.fan.za4", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC), FRIDGE_V3("viomi.fridge.v3", "Viomi Internet refrigerator iLive", THING_TYPE_UNSUPPORTED), GATEWAY1("lumi.gateway.v1", "Mi Smart Home Gateway v1", THING_TYPE_UNSUPPORTED), GATEWAY2("lumi.gateway.v2", "Mi Smart Home Gateway v2", THING_TYPE_UNSUPPORTED), diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za4.json b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za4.json new file mode 100644 index 0000000000000..d6d63cd7fb8c1 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/zhimi.fan.za4.json @@ -0,0 +1,171 @@ +{ + "deviceMapping": { + "id": [ + "zhimi.fan.za4" + ], + "channels": [ + { + "property": "power", + "friendlyName": "Power", + "channel": "power", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_power", + "parameterType": "ONOFF" + } + ] + }, + { + "property": "angle_enable", + "friendlyName": "Rotation", + "channel": "angleEnable", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_angle_enable", + "parameterType": "ONOFF" + } + ] + }, + { + "property": "use_time", + "friendlyName": "Run Time", + "channel": "usedhours", + "type": "Number", + "refresh": true, + "transformation": "SecondsToHours", + "ChannelGroup": "Status", + "actions": [] + }, + { + "property": "angle", + "friendlyName": "Angle", + "channel": "angle", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_angle", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "poweroff_time", + "friendlyName": "Timer", + "channel": "poweroffTime", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_poweroff_time", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "buzzer", + "friendlyName": "Buzzer", + "channel": "buzzer", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_buzzer", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "led_b", + "friendlyName": "LED", + "channel": "led_b", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_led_b", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "child_lock", + "friendlyName": "Child Lock", + "channel": "child_lock", + "type": "Switch", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_child_lock", + "parameterType": "ONOFF" + } + ] + }, + { + "property": "speed_level", + "friendlyName": "Speed Level", + "channel": "speedLevel", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_speed_level", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "speed", + "friendlyName": "Speed", + "channel": "speed", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_speed", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "natural_level", + "friendlyName": "Natural Level", + "channel": "naturalLevel", + "type": "Number", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_natural_level", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "", + "friendlyName": "Move Direction", + "channel": "move", + "type": "String", + "refresh": true, + "ChannelGroup": "actions", + "actions": [ + { + "command": "set_move", + "parameterType": "STRING" + } + ] + } + ] + } +} From 020da77cd7d11935f4751037b959ab45e3c43a9a Mon Sep 17 00:00:00 2001 From: robnielsen Date: Fri, 19 Jun 2020 12:24:55 -0500 Subject: [PATCH 61/83] [insteon] keep track of group state at the device level (#7929) Signed-off-by: Rob Nielsen --- .../device/GroupMessageStateMachine.java | 24 ++++++++--- .../internal/device/InsteonDevice.java | 43 +++++++++++++++---- .../internal/device/MessageHandler.java | 28 ++---------- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java index 1f6cd84d6074f..21ba6d0b28fd8 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java @@ -101,18 +101,20 @@ enum State { EXPECT_SUCCESS }; - State state = State.EXPECT_BCAST; - int lastHops = 0; + private State state = State.EXPECT_BCAST; + private long lastUpdated = 0; + private boolean publish = false; /** * Advance the state machine and determine if update is genuine (no duplicate) * * @param a the group message (action) that was received - * @param hops number of hops that was given on the message. Currently not used. + * @param address the address of the device that this state machine belongs to + * @param group the group that this state machine belongs to * @return true if the group message is not a duplicate */ - public boolean action(GroupMessage a, int hops) { - boolean publish = false; + public boolean action(GroupMessage a, InsteonAddress address, int group) { + publish = false; switch (state) { case EXPECT_BCAST: switch (a) { @@ -166,7 +168,17 @@ public boolean action(GroupMessage a, int hops) { state = State.EXPECT_BCAST; break; } - logger.trace("group state: {} --{}--> {}, publish: {}", oldState, a, state, publish); + + lastUpdated = System.currentTimeMillis(); + logger.debug("{} group {} state: {} --{}--> {}, publish: {}", address, group, oldState, a, state, publish); return (publish); } + + public long getLastUpdated() { + return lastUpdated; + } + + public boolean getPublish() { + return publish; + } } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java index 97b9e00c9b0ae..dcdb26963d923 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.PriorityQueue; @@ -25,6 +26,7 @@ import org.eclipse.smarthome.core.types.Command; import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration; import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup; +import org.openhab.binding.insteon.internal.device.GroupMessageStateMachine.GroupMessage; import org.openhab.binding.insteon.internal.driver.Driver; import org.openhab.binding.insteon.internal.message.FieldException; import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException; @@ -63,14 +65,15 @@ public static enum DeviceStatus { private @Nullable Driver driver = null; private HashMap features = new HashMap<>(); private @Nullable String productKey = null; - private Long lastTimePolled = 0L; - private Long lastMsgReceived = 0L; + private volatile long lastTimePolled = 0L; + private volatile long lastMsgReceived = 0L; private boolean isModem = false; private PriorityQueue<@Nullable QEntry> mrequestQueue = new PriorityQueue<>(); private @Nullable DeviceFeature featureQueried = null; private long lastQueryTime = 0L; private boolean hasModemDBEntry = false; private DeviceStatus status = DeviceStatus.INITIALIZED; + private Map groupState = new HashMap<>(); /** * Constructor @@ -266,22 +269,18 @@ public void doPoll(long delay) { RequestQueueManager.instance().addQueue(this, now + delay); if (!l.isEmpty()) { - synchronized (lastTimePolled) { - lastTimePolled = now; - } + lastTimePolled = now; } } /** * Handle incoming message for this device by forwarding * it to all features that this device supports - * + * * @param msg the incoming message */ public void handleMessage(Msg msg) { - synchronized (lastMsgReceived) { - lastMsgReceived = System.currentTimeMillis(); - } + lastMsgReceived = System.currentTimeMillis(); synchronized (features) { // first update all features that are // not status features @@ -546,6 +545,32 @@ private void addFeature(String name, DeviceFeature f) { } } + /** + * Get the state of the state machine that suppresses duplicates for group messages. + * The state machine is advance the first time it is called for a message, + * otherwise return the current state. + * + * @param group the insteon group of the broadcast message + * @param a the type of group message came in (action etc) + * @return true if this is message is NOT a duplicate + */ + public boolean getGroupState(int group, GroupMessage a) { + GroupMessageStateMachine m = groupState.get(group); + if (m == null) { + m = new GroupMessageStateMachine(); + groupState.put(group, m); + logger.trace("{} created group {} state", address, group); + } else { + if (lastMsgReceived <= m.getLastUpdated()) { + logger.trace("{} using previous group {} state for {}", address, group, a); + return m.getPublish(); + } + } + + logger.trace("{} updating group {} state to {}", address, group, a); + return (m.action(a, address, group)); + } + @Override public String toString() { String s = address.toString(); diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java index 6a725a8ced971..5fb0578d74ba8 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java @@ -53,9 +53,8 @@ public abstract class MessageHandler { private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class); - DeviceFeature feature; - Map parameters = new HashMap<>(); - Map groupState = new HashMap<>(); + protected DeviceFeature feature; + protected Map parameters = new HashMap<>(); /** * Constructor @@ -253,7 +252,6 @@ protected boolean isDuplicate(Msg msg) { boolean isDuplicate = false; try { MsgType t = MsgType.fromValue(msg.getByte("messageFlags")); - int hops = msg.getHopsLeft(); if (t == MsgType.ALL_LINK_BROADCAST) { int group = msg.getAddress("toAddress").getLowByte() & 0xff; byte cmd1 = msg.getByte("command1"); @@ -261,12 +259,12 @@ protected boolean isDuplicate(Msg msg) { // from the original broadcaster, with which the device // confirms that it got all cleanup replies successfully. GroupMessage gm = (cmd1 == 0x06) ? GroupMessage.SUCCESS : GroupMessage.BCAST; - isDuplicate = !updateGroupState(group, hops, gm); + isDuplicate = !feature.getDevice().getGroupState(group, gm); } else if (t == MsgType.ALL_LINK_CLEANUP) { // the cleanup messages are direct messages, so the // group # is not in the toAddress, but in cmd2 int group = msg.getByte("command2") & 0xff; - isDuplicate = !updateGroupState(group, hops, GroupMessage.CLEAN); + isDuplicate = !feature.getDevice().getGroupState(group, GroupMessage.CLEAN); } } catch (IllegalArgumentException e) { logger.warn("cannot parse msg: {}", msg, e); @@ -276,24 +274,6 @@ protected boolean isDuplicate(Msg msg) { return (isDuplicate); } - /** - * Advance the state of the state machine that suppresses duplicates - * - * @param group the insteon group of the broadcast message - * @param hops number of hops left - * @param a what type of group message came in (action etc) - * @return true if this is message is NOT a duplicate - */ - private boolean updateGroupState(int group, int hops, GroupMessage a) { - GroupMessageStateMachine m = groupState.get(new Integer(group)); - if (m == null) { - m = new GroupMessageStateMachine(); - groupState.put(new Integer(group), m); - } - logger.trace("updating group state for {} to {}", group, a); - return (m.action(a, hops)); - } - /** * Extract button information from message * From b8579d36417cedf331b5719a393d888d20b52130 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Fri, 19 Jun 2020 22:51:18 +0200 Subject: [PATCH 62/83] [rotel] Add support for RC-1590 with firmware >= 1.40 (#7946) Signed-off-by: Laurent Garnier --- bundles/org.openhab.binding.rotel/README.md | 13 +++++++------ .../openhab/binding/rotel/internal/RotelModel.java | 2 +- .../src/main/resources/ESH-INF/thing/rc1590.xml | 6 +----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/bundles/org.openhab.binding.rotel/README.md b/bundles/org.openhab.binding.rotel/README.md index e8282b1ccfb29..f9a99b07c3c77 100644 --- a/bundles/org.openhab.binding.rotel/README.md +++ b/bundles/org.openhab.binding.rotel/README.md @@ -107,12 +107,13 @@ The thing requires the following configuration parameters: All things have the following parameters: serialPort, host and port. Some have additional parameters listed in the next table: -| Thing Type | Parameters available in addition to serialPort, host and port | -|------------|---------------------------------------------------------------| -| ra1572 | protocol (ASCII_V2 by default) | -| ra1592 | protocol (ASCII_V2 by default) | -| rc1572 | protocol (ASCII_V2 by default) | -| rcd1572 | protocol (ASCII_V2 by default) | +| Thing Type | Parameters available in addition to serialPort, host and port | +|------------|-----------------------------------------------------------------| +| ra1572 | protocol (ASCII_V2 by default); as of firmware V2.65, select V2 | +| ra1592 | protocol (ASCII_V2 by default); as of firmware V1.53, select V2 | +| rc1572 | protocol (ASCII_V2 by default); as of firmware V2.65, select V2 | +| rc1590 | protocol (ASCII_V2 by default); as of firmware V1.40, select V2 | +| rcd1572 | protocol (ASCII_V2 by default); as of firmware V2.33, select V2 | | rsp1066 | inputLabelVideo1, inputLabelVideo2, inputLabelVideo3, inputLabelVideo4, inputLabelVideo5 | | rsp1068 | inputLabelCd, inputLabelTuner, inputLabelTape, inputLabelVideo1, inputLabelVideo2, inputLabelVideo3, inputLabelVideo4, inputLabelVideo5 | | rsp1069 | inputLabelCd, inputLabelTuner, inputLabelTape, inputLabelVideo1, inputLabelVideo2, inputLabelVideo3, inputLabelVideo4, inputLabelVideo5 | diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelModel.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelModel.java index 7ac39c8d75fc7..ad4b594b48bcf 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelModel.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelModel.java @@ -76,7 +76,7 @@ public enum RotelModel { RotelConnector.NO_SPECIAL_CHARACTERS), RC1570("RC-1570", 115200, 7, 96, true, 10, true, -1, true, false, 6, 0, RotelConnector.SPECIAL_CHARACTERS), RC1572("RC-1572", 115200, 8, 96, true, 10, false, -1, true, true, 6, 0, RotelConnector.SPECIAL_CHARACTERS), - RC1590("RC-1590", 115200, 9, 96, true, 10, false, -1, true, false, 6, 0, RotelConnector.SPECIAL_CHARACTERS), + RC1590("RC-1590", 115200, 9, 96, true, 10, false, -1, true, true, 6, 0, RotelConnector.SPECIAL_CHARACTERS), RCD1570("RCD-1570", 115200, 0, null, false, null, true, -1, false, true, 6, 0, RotelConnector.SPECIAL_CHARACTERS), RCD1572("RCD-1572", 57600, 0, null, false, null, true, -1, false, true, 6, 0, RotelConnector.SPECIAL_CHARACTERS_RCD1572), diff --git a/bundles/org.openhab.binding.rotel/src/main/resources/ESH-INF/thing/rc1590.xml b/bundles/org.openhab.binding.rotel/src/main/resources/ESH-INF/thing/rc1590.xml index 80c0966a418a3..86930aa91cda0 100644 --- a/bundles/org.openhab.binding.rotel/src/main/resources/ESH-INF/thing/rc1590.xml +++ b/bundles/org.openhab.binding.rotel/src/main/resources/ESH-INF/thing/rc1590.xml @@ -20,11 +20,7 @@ - - ASCII_V1 - - - + From abc926ddd03ca667b6e1dfb1b37cb77491154f79 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Fri, 19 Jun 2020 22:53:58 +0200 Subject: [PATCH 63/83] [ntp] Clean up (#7834) * [ntp] Clean up Add null annotations Suppress one unused configutation setting (locale) Get the default timezone from TimeZoneProvider Map thing configuration and channel configuration to classes Use constructor injection for the discovery service * Use TimeZoneProvider each time the timezone is required * Call activate in the discovery service constructor Signed-off-by: Laurent Garnier --- .../ntp/internal/NtpHandlerFactory.java | 10 +- .../config/NtpStringChannelConfiguration.java | 27 ++ .../config/NtpThingConfiguration.java | 32 +++ .../ntp/internal/discovery/NtpDiscovery.java | 47 ++-- .../ntp/internal/handler/NtpHandler.java | 235 ++++++++---------- .../resources/ESH-INF/thing/thing-types.xml | 10 +- 6 files changed, 182 insertions(+), 179 deletions(-) create mode 100644 bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpStringChannelConfiguration.java create mode 100644 bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpThingConfiguration.java diff --git a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/NtpHandlerFactory.java b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/NtpHandlerFactory.java index 6e0ce58906f11..ea1fec57e5eff 100644 --- a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/NtpHandlerFactory.java +++ b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/NtpHandlerFactory.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; @@ -38,11 +38,11 @@ @Component(service = ThingHandlerFactory.class, configurationPid = "binding.ntp") public class NtpHandlerFactory extends BaseThingHandlerFactory { - private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; @Activate - public NtpHandlerFactory(final @Reference LocaleProvider localeProvider) { - this.localeProvider = localeProvider; + public NtpHandlerFactory(final @Reference TimeZoneProvider timeZoneProvider) { + this.timeZoneProvider = timeZoneProvider; } @Override @@ -55,7 +55,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_NTP.equals(thingTypeUID)) { - return new NtpHandler(thing, localeProvider); + return new NtpHandler(thing, timeZoneProvider); } return null; diff --git a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpStringChannelConfiguration.java b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpStringChannelConfiguration.java new file mode 100644 index 0000000000000..07ea950fb5696 --- /dev/null +++ b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpStringChannelConfiguration.java @@ -0,0 +1,27 @@ +/** + * 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.ntp.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link NtpStringChannelConfiguration} is responsible for holding + * the configuration settings for the channel "string" + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class NtpStringChannelConfiguration { + + public String DateTimeFormat = "yyyy-MM-dd HH:mm:ss z"; +} diff --git a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpThingConfiguration.java b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpThingConfiguration.java new file mode 100644 index 0000000000000..16eea0fd21e0d --- /dev/null +++ b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/config/NtpThingConfiguration.java @@ -0,0 +1,32 @@ +/** + * 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.ntp.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link NtpThingConfiguration} is responsible for holding + * the thing configuration settings + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class NtpThingConfiguration { + + public String hostname = "0.pool.ntp.org"; + public int refreshInterval = 60; + public int refreshNtp = 30; + public int serverPort = 123; + public @Nullable String timeZone; +} diff --git a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/discovery/NtpDiscovery.java b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/discovery/NtpDiscovery.java index 43ff128f22766..654f59b4a60c9 100644 --- a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/discovery/NtpDiscovery.java +++ b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/discovery/NtpDiscovery.java @@ -16,16 +16,19 @@ import java.util.HashMap; import java.util.Map; -import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.i18n.TranslationProvider; import org.eclipse.smarthome.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -36,21 +39,21 @@ * * @author Marcel Verpaalen - Initial contribution */ +@NonNullByDefault @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.ntp") public class NtpDiscovery extends AbstractDiscoveryService { - public NtpDiscovery() throws IllegalArgumentException { - super(SUPPORTED_THING_TYPES_UIDS, 2); - } - - @Override - protected void activate(Map configProperties) { - super.activate(configProperties); - } + private final TimeZoneProvider timeZoneProvider; - @Override - protected void modified(Map configProperties) { - super.modified(configProperties); + @Activate + public NtpDiscovery(final @Reference LocaleProvider localeProvider, + final @Reference TranslationProvider i18nProvider, final @Reference TimeZoneProvider timeZoneProvider, + @Nullable Map configProperties) throws IllegalArgumentException { + super(SUPPORTED_THING_TYPES_UIDS, 2); + this.localeProvider = localeProvider; + this.i18nProvider = i18nProvider; + this.timeZoneProvider = timeZoneProvider; + activate(configProperties); } @Override @@ -70,28 +73,10 @@ protected void startScan() { */ private void discoverNtp() { Map properties = new HashMap<>(4); - properties.put(PROPERTY_TIMEZONE, TimeZone.getDefault().getID()); + properties.put(PROPERTY_TIMEZONE, timeZoneProvider.getTimeZone().getId()); ThingUID uid = new ThingUID(THING_TYPE_NTP, "local"); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel("Local Time") .build(); thingDiscovered(result); } - - @Reference - protected void setLocaleProvider(final LocaleProvider localeProvider) { - this.localeProvider = localeProvider; - } - - protected void unsetLocaleProvider(final LocaleProvider localeProvider) { - this.localeProvider = null; - } - - @Reference - protected void setTranslationProvider(TranslationProvider i18nProvider) { - this.i18nProvider = i18nProvider; - } - - protected void unsetTranslationProvider(TranslationProvider i18nProvider) { - this.i18nProvider = null; - } } diff --git a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/handler/NtpHandler.java b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/handler/NtpHandler.java index dcd2569549b33..2c8b89a70debb 100644 --- a/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/handler/NtpHandler.java +++ b/bundles/org.openhab.binding.ntp/src/main/java/org/openhab/binding/ntp/internal/handler/NtpHandler.java @@ -15,24 +15,21 @@ import static org.openhab.binding.ntp.internal.NtpBindingConstants.*; import java.io.IOException; -import java.math.BigDecimal; import java.net.InetAddress; import java.net.UnknownHostException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.time.DateTimeException; import java.time.Instant; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.net.ntp.NTPUDPClient; import org.apache.commons.net.ntp.TimeInfo; -import org.eclipse.smarthome.config.core.Configuration; -import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.thing.Channel; @@ -42,6 +39,9 @@ import org.eclipse.smarthome.core.thing.ThingStatusDetail; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.ntp.internal.config.NtpStringChannelConfiguration; +import org.openhab.binding.ntp.internal.config.NtpThingConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,217 +54,180 @@ * to one of the channels. * * @author Marcel Verpaalen - Initial contribution OH2 ntp binding - * @author Thomas.Eichstaedt-Engelen OH1 ntp binding (getTime routine) + * @author Thomas.Eichstaedt-Engelen - OH1 ntp binding (getTime routine) * @author Markus Rathgeb - Add locale provider * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType + * @author Laurent Garnier - null annotations, TimeZoneProvider, configuration settings cleanup */ - +@NonNullByDefault public class NtpHandler extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(NtpHandler.class); - /** timeout for requests to the NTP server */ private static final int NTP_TIMEOUT = 30000; public static final String DATE_PATTERN_WITH_TZ = "yyyy-MM-dd HH:mm:ss z"; + private static final DateTimeFormatter DATE_FORMATTER_WITH_TZ = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ); + + private final Logger logger = LoggerFactory.getLogger(NtpHandler.class); - /** for logging purposes */ - private final DateFormat SDF = new SimpleDateFormat(DATE_PATTERN_WITH_TZ); + private final TimeZoneProvider timeZoneProvider; /** for publish purposes */ private DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ); - private final LocaleProvider localeProvider; + private NtpThingConfiguration configuration = new NtpThingConfiguration(); - ScheduledFuture refreshJob; + private @Nullable ScheduledFuture refreshJob; - /** NTP host */ - private String hostname; - /** NTP server port */ - private BigDecimal port; - /** refresh interval */ - private BigDecimal refreshInterval; - /** NTP refresh frequency */ - private BigDecimal refreshNtp = new BigDecimal(0); - /** Timezone */ - private TimeZone timeZone; - /** Locale */ - private Locale locale; + private @Nullable ZoneId timeZoneId; /** NTP refresh counter */ private int refreshNtpCount = 0; /** NTP system time delta */ private long timeOffset; - private ChannelUID dateTimeChannelUID; - private ChannelUID stringChannelUID; - - public NtpHandler(final Thing thing, final LocaleProvider localeProvider) { + public NtpHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) { super(thing); - this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; } @Override public void handleCommand(ChannelUID channelUID, Command command) { - // No specific commands tied to this, but we will trigger an update - this.refreshNtpCount = 0; - refreshTimeDate(); + if (command == RefreshType.REFRESH) { + logger.debug("Refreshing channel '{}' for '{}'.", channelUID.getId(), getThing().getUID()); + refreshTimeDate(); + } } @Override public void initialize() { - try { - logger.debug("Initializing NTP handler for '{}'.", getThing().getUID()); + logger.debug("Initializing NTP handler for '{}'.", getThing().getUID()); - Configuration config = getThing().getConfiguration(); - hostname = config.get(PROPERTY_NTP_SERVER_HOST).toString(); - port = (BigDecimal) config.get(PROPERTY_NTP_SERVER_PORT); - refreshInterval = (BigDecimal) config.get(PROPERTY_REFRESH_INTERVAL); - refreshNtp = (BigDecimal) config.get(PROPERTY_REFRESH_NTP); - refreshNtpCount = 0; + configuration = getConfigAs(NtpThingConfiguration.class); - try { - Object timeZoneConfigValue = config.get(PROPERTY_TIMEZONE); - if (timeZoneConfigValue != null) { - timeZone = TimeZone.getTimeZone(timeZoneConfigValue.toString()); - } else { - timeZone = TimeZone.getDefault(); - logger.debug("{} using default TZ '{}', because configuration property '{}' is null.", - getThing().getUID(), timeZone, PROPERTY_TIMEZONE); - } - } catch (Exception e) { - timeZone = TimeZone.getDefault(); - logger.debug("{} using default TZ '{}' due to an occurred exception: ", getThing().getUID(), timeZone, - e); - } + refreshNtpCount = 0; + if (configuration.timeZone != null) { + logger.debug("{} with timezone '{}' set in configuration setting '{}'", getThing().getUID(), + configuration.timeZone, PROPERTY_TIMEZONE); try { - Object localeStringConfigValue = config.get(PROPERTY_LOCALE); - if (localeStringConfigValue != null) { - locale = new Locale(localeStringConfigValue.toString()); - } else { - locale = localeProvider.getLocale(); - logger.debug("{} using default locale '{}', because configuration property '{}' is null.", - getThing().getUID(), locale, PROPERTY_LOCALE); - } - } catch (Exception e) { - locale = localeProvider.getLocale(); - logger.debug("{} using default locale '{}' due to an occurred exception: ", getThing().getUID(), locale, - e); + timeZoneId = ZoneId.of(configuration.timeZone); + } catch (DateTimeException e) { + timeZoneId = null; + logger.debug("{} using default timezone '{}', because configuration setting '{}' is invalid: {}", + getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE, e.getMessage()); } - dateTimeChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_DATE_TIME); - stringChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_STRING); - try { - Channel stringChannel = getThing().getChannel(stringChannelUID.getId()); - if (stringChannel != null) { - Configuration cfg = stringChannel.getConfiguration(); - String dateTimeFormatString = cfg.get(PROPERTY_DATE_TIME_FORMAT).toString(); - if (!(dateTimeFormatString == null || dateTimeFormatString.isEmpty())) { - dateTimeFormat = DateTimeFormatter.ofPattern(dateTimeFormatString); - } else { - logger.debug("No format set in channel config for {}. Using default format.", stringChannelUID); - dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ); - } - } else { - logger.debug("Missing channel: '{}'", stringChannelUID.getId()); + } else { + timeZoneId = null; + logger.debug("{} using default timezone '{}', because configuration setting '{}' is null.", + getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE); + } + ZoneId zoneId = timeZoneId != null ? timeZoneId : timeZoneProvider.getTimeZone(); + + Channel stringChannel = getThing().getChannel(CHANNEL_STRING); + if (stringChannel != null) { + String dateTimeFormatString = stringChannel.getConfiguration() + .as(NtpStringChannelConfiguration.class).DateTimeFormat; + if (!dateTimeFormatString.isEmpty()) { + logger.debug("Date format set in config for channel '{}': {}", CHANNEL_STRING, dateTimeFormatString); + try { + dateTimeFormat = DateTimeFormatter.ofPattern(dateTimeFormatString); + } catch (IllegalArgumentException ex) { + logger.debug("Invalid date format set in config for channel '{}'. Using default format. ({})", + CHANNEL_STRING, ex.getMessage()); + dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ); } - } catch (RuntimeException ex) { - logger.debug("No channel config or invalid format for {}. Using default format. ({})", stringChannelUID, - ex.getMessage()); + } else { + logger.debug("No date format set in config for channel '{}'. Using default format.", CHANNEL_STRING); dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ); } - SDF.setTimeZone(timeZone); - dateTimeFormat.withZone(timeZone.toZoneId()); - - logger.debug( - "Initialized NTP handler '{}' with configuration: host '{}', refresh interval {}, timezone {}, locale {}.", - getThing().getUID(), hostname, refreshInterval, timeZone, locale); - startAutomaticRefresh(); - } catch (Exception ex) { - logger.error("Error occurred while initializing NTP handler: {}", ex.getMessage(), ex); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.conf-error-init-handler"); + } else { + logger.debug("Missing channel: '{}'", CHANNEL_STRING); } - } + dateTimeFormat.withZone(zoneId); - @Override - public void dispose() { - refreshJob.cancel(true); - super.dispose(); - } + logger.debug( + "Initialized NTP handler '{}' with configuration: host '{}', port {}, refresh interval {}, refresh frequency {}, timezone {}.", + getThing().getUID(), configuration.hostname, configuration.serverPort, configuration.refreshInterval, + configuration.refreshNtp, zoneId); - private void startAutomaticRefresh() { refreshJob = scheduler.scheduleWithFixedDelay(() -> { try { refreshTimeDate(); } catch (Exception e) { logger.debug("Exception occurred during refresh: {}", e.getMessage(), e); } - }, 0, refreshInterval.intValue(), TimeUnit.SECONDS); + }, 0, configuration.refreshInterval, TimeUnit.SECONDS); } - private synchronized void refreshTimeDate() { - if (timeZone != null && locale != null) { - long networkTimeInMillis; - if (refreshNtpCount <= 0) { - networkTimeInMillis = getTime(hostname); - timeOffset = networkTimeInMillis - System.currentTimeMillis(); - logger.debug("{} delta system time: {}", getThing().getUID(), timeOffset); - refreshNtpCount = refreshNtp.intValue(); - } else { - networkTimeInMillis = System.currentTimeMillis() + timeOffset; - refreshNtpCount--; - } + @Override + public void dispose() { + logger.debug("Disposing NTP handler for '{}'.", getThing().getUID()); + ScheduledFuture job = refreshJob; + if (job != null) { + job.cancel(true); + } + refreshJob = null; + super.dispose(); + } - ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(networkTimeInMillis), - timeZone.toZoneId()); - updateState(dateTimeChannelUID, new DateTimeType(zoned)); - updateState(stringChannelUID, new StringType(dateTimeFormat.format(zoned))); + private synchronized void refreshTimeDate() { + long networkTimeInMillis; + if (refreshNtpCount <= 0) { + networkTimeInMillis = getTime(configuration.hostname, configuration.serverPort); + timeOffset = networkTimeInMillis - System.currentTimeMillis(); + logger.debug("{} delta system time: {}", getThing().getUID(), timeOffset); + refreshNtpCount = configuration.refreshNtp; } else { - logger.debug("Not refreshing, since we do not seem to be initialized yet"); + networkTimeInMillis = System.currentTimeMillis() + timeOffset; + refreshNtpCount--; } + + ZoneId zoneId = timeZoneId != null ? timeZoneId : timeZoneProvider.getTimeZone(); + ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(networkTimeInMillis), zoneId); + updateState(CHANNEL_DATE_TIME, new DateTimeType(zoned)); + dateTimeFormat.withZone(zoneId); + updateState(CHANNEL_STRING, new StringType(dateTimeFormat.format(zoned))); } /** * Queries the given timeserver hostname and returns the time * in milliseconds. * - * @param hostname the timeserver to query + * @param hostname the timeserver hostname to query + * @param port the timeserver port to query * @return the time in milliseconds or the current time of the system if an * error occurs. */ - public long getTime(String hostname) { + private long getTime(String hostname, int port) { try { NTPUDPClient timeClient = new NTPUDPClient(); timeClient.setDefaultTimeout(NTP_TIMEOUT); InetAddress inetAddress = InetAddress.getByName(hostname); - TimeInfo timeInfo = timeClient.getTime(inetAddress, port.intValue()); + TimeInfo timeInfo = timeClient.getTime(inetAddress, port); timeInfo.computeDetails(); long serverMillis = timeInfo.getReturnTime() + timeInfo.getOffset(); + ZoneId zoneId = timeZoneId != null ? timeZoneId : timeZoneProvider.getTimeZone(); + ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(serverMillis), zoneId); logger.debug("{} Got time update from host '{}': {}.", getThing().getUID(), hostname, - SDF.format(new Date(serverMillis))); - updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + zoned.format(DATE_FORMATTER_WITH_TZ)); + updateStatus(ThingStatus.ONLINE); return serverMillis; } catch (UnknownHostException uhe) { logger.debug( "{} The given hostname '{}' of the timeserver is unknown -> returning current sytem time instead. ({})", getThing().getUID(), hostname, uhe.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.comm-error-unknown-host [\"" + (hostname == null ? "null" : hostname) + "\"]"); + "@text/offline.comm-error-unknown-host [\"" + hostname + "\"]"); } catch (IOException ioe) { logger.debug( "{} Couldn't establish network connection to host '{}' -> returning current sytem time instead. ({})", getThing().getUID(), hostname, ioe.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.comm-error-connection [\"" + (hostname == null ? "null" : hostname) + "\"]"); + "@text/offline.comm-error-connection [\"" + hostname + "\"]"); } return System.currentTimeMillis(); } - - @Override - public void channelLinked(ChannelUID channelUID) { - refreshTimeDate(); - } } diff --git a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/thing/thing-types.xml index 737df4afc15f0..d4e6fea94c14c 100644 --- a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/thing/thing-types.xml @@ -17,14 +17,14 @@ The NTP server hostname. 0.pool.ntp.org - + Interval that new time updates are posted to the event bus in seconds. 60 - + Number of updates before querying the NTP server. @@ -36,14 +36,10 @@ The port that the NTP server could use. 123 - + The configured timezone. - - - The configured locale. - From 3023a8cc5aca0c8ed9146f4c7b32673f9b01e1d7 Mon Sep 17 00:00:00 2001 From: Christoph Weitkamp Date: Sat, 20 Jun 2020 10:27:59 +0200 Subject: [PATCH 64/83] Fixed usage of deprecated constructor for 'DateTimeType(Calendar)' (#7918) Signed-off-by: Christoph Weitkamp --- .../bigassfan/internal/handler/BigAssFanHandler.java | 11 ++++------- .../gardena/internal/handler/GardenaThingHandler.java | 4 +++- .../logreader/internal/handler/LogHandler.java | 5 +++-- .../lutron/internal/grxprg/PrgProtocolHandler.java | 11 ++++++++--- .../lutron/internal/handler/TimeclockHandler.java | 8 ++++++-- .../meteoblue/internal/handler/MeteoBlueHandler.java | 5 ++++- .../onebusaway/internal/handler/RouteHandler.java | 9 +++++++-- .../internal/handler/TelldusDevicesHandler.java | 5 ++++- .../internal/handler/UniFiClientThingHandler.java | 5 ++++- 9 files changed, 43 insertions(+), 20 deletions(-) diff --git a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java index 2cfff155bec87..b8669a32592b0 100644 --- a/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java +++ b/bundles/org.openhab.binding.bigassfan/src/main/java/org/openhab/binding/bigassfan/internal/handler/BigAssFanHandler.java @@ -25,11 +25,11 @@ import java.net.UnknownHostException; import java.nio.BufferOverflowException; import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.util.Arrays; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; @@ -41,7 +41,6 @@ import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -86,7 +85,7 @@ public class BigAssFanHandler extends BaseThingHandler { protected Map fanStateMap = Collections.synchronizedMap(new HashMap<>()); - public BigAssFanHandler(@NonNull Thing thing, String ipv4Address) { + public BigAssFanHandler(Thing thing, String ipv4Address) { super(thing); this.thing = thing; @@ -990,10 +989,8 @@ private void updateTime(String[] messageParts) { logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]); // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z) try { - Calendar cal = Calendar.getInstance(); Instant instant = Instant.parse(messageParts[3]); - cal.setTime(Date.from(instant)); - DateTimeType state = new DateTimeType(cal); + DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault())); updateChannel(CHANNEL_TIME, state); fanStateMap.put(CHANNEL_TIME, state); } catch (DateTimeParseException e) { diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaThingHandler.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaThingHandler.java index 1479431f8fce9..b62af9d39643c 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaThingHandler.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaThingHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*; import static org.openhab.binding.gardena.internal.GardenaSmartCommandName.*; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Map; import java.util.Map.Entry; @@ -185,7 +187,7 @@ private State convertToState(Device device, ChannelUID channelUID) throws Garden case "DateTime": Calendar cal = DateUtils.parseToCalendar(value); if (cal != null && !cal.before(VALID_DATE_START)) { - return new DateTimeType(cal); + return new DateTimeType(ZonedDateTime.ofInstant(cal.toInstant(), ZoneId.systemDefault())); } else { return UnDefType.NULL; } diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java index e9a440a163fe8..3db263fceb2eb 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java @@ -14,7 +14,8 @@ import static org.openhab.binding.logreader.internal.LogReaderBindingConstants.*; -import java.util.Calendar; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.regex.PatternSyntaxException; import org.eclipse.smarthome.core.library.types.DateTimeType; @@ -162,7 +163,7 @@ public void fileNotFound() { @Override public void fileRotated() { logger.debug("Log rotated"); - updateChannelIfLinked(CHANNEL_LOGROTATED, new DateTimeType(Calendar.getInstance())); + updateChannelIfLinked(CHANNEL_LOGROTATED, new DateTimeType(ZonedDateTime.now())); } @Override diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgProtocolHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgProtocolHandler.java index a1485fdc23120..048816f065015 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgProtocolHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgProtocolHandler.java @@ -13,6 +13,8 @@ package org.openhab.binding.lutron.internal.grxprg; import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.HashMap; import java.util.Map; @@ -884,7 +886,8 @@ private void handleReportTime(Matcher m, String resp) { final int yr = Integer.parseInt(m.group(5)); c.set(Calendar.YEAR, yr + (yr < 50 ? 1900 : 2000)); - _callback.stateChanged(PrgConstants.CHANNEL_TIMECLOCK, new DateTimeType(c)); + _callback.stateChanged(PrgConstants.CHANNEL_TIMECLOCK, + new DateTimeType(ZonedDateTime.ofInstant(c.toInstant(), ZoneId.systemDefault()))); } catch (NumberFormatException e) { logger.error("Invalid time response (can't parse number): '{}'", resp); } @@ -934,12 +937,14 @@ private void handleSunriseSunset(Matcher m, String resp) { final Calendar sunrise = Calendar.getInstance(); sunrise.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(1))); sunrise.set(Calendar.MINUTE, Integer.parseInt(m.group(2))); - _callback.stateChanged(PrgConstants.CHANNEL_SUNRISE, new DateTimeType(sunrise)); + _callback.stateChanged(PrgConstants.CHANNEL_SUNRISE, + new DateTimeType(ZonedDateTime.ofInstant(sunrise.toInstant(), ZoneId.systemDefault()))); final Calendar sunset = Calendar.getInstance(); sunset.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(3))); sunset.set(Calendar.MINUTE, Integer.parseInt(m.group(4))); - _callback.stateChanged(PrgConstants.CHANNEL_SUNSET, new DateTimeType(sunset)); + _callback.stateChanged(PrgConstants.CHANNEL_SUNSET, + new DateTimeType(ZonedDateTime.ofInstant(sunset.toInstant(), ZoneId.systemDefault()))); } catch (NumberFormatException e) { logger.error("Invalid sunrise/sunset response (can't parse number): '{}'", resp); } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/TimeclockHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/TimeclockHandler.java index 598e127b89c53..8b7fc26f2ad76 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/TimeclockHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/TimeclockHandler.java @@ -14,6 +14,8 @@ import static org.openhab.binding.lutron.internal.LutronBindingConstants.*; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -180,13 +182,15 @@ public void handleUpdate(LutronCommandType type, String... parameters) { } else if (parameters.length >= 2 && ACTION_SUNRISE.toString().equals(parameters[0])) { Calendar calendar = parseLutronTime(parameters[1]); if (calendar != null) { - updateState(CHANNEL_SUNRISE, new DateTimeType(calendar)); + updateState(CHANNEL_SUNRISE, + new DateTimeType(ZonedDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault()))); } } else if (parameters.length >= 2 && ACTION_SUNSET.toString().equals(parameters[0])) { Calendar calendar = parseLutronTime(parameters[1]); if (calendar != null) { - updateState(CHANNEL_SUNSET, new DateTimeType(calendar)); + updateState(CHANNEL_SUNSET, + new DateTimeType(ZonedDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault()))); } } else if (parameters.length >= 2 && ACTION_EXECEVENT.toString().equals(parameters[0])) { diff --git a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java index 98844d8b440bc..ca11e40fd0e5e 100644 --- a/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java +++ b/bundles/org.openhab.binding.meteoblue/src/main/java/org/openhab/binding/meteoblue/internal/handler/MeteoBlueHandler.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -230,7 +232,8 @@ private void updateChannel(String channelId) { // Build a State from this value State state = null; if (datapoint instanceof Calendar) { - state = new DateTimeType((Calendar) datapoint); + state = new DateTimeType( + ZonedDateTime.ofInstant(((Calendar) datapoint).toInstant(), ZoneId.systemDefault())); } else if (datapoint instanceof Integer) { state = getStateForType(channel.getAcceptedItemType(), (Integer) datapoint); } else if (datapoint instanceof Number) { diff --git a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/handler/RouteHandler.java b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/handler/RouteHandler.java index e461652dea138..8918d22d496a2 100644 --- a/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/handler/RouteHandler.java +++ b/bundles/org.openhab.binding.onebusaway/src/main/java/org/openhab/binding/onebusaway/internal/handler/RouteHandler.java @@ -14,6 +14,9 @@ import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.List; import java.util.Map; @@ -165,7 +168,8 @@ private void updatePropertiesFromArrivalAndDeparture(ArrivalAndDeparture data) { private void publishChannel(ChannelUID channelUID, Calendar now, long lastUpdateTime, List arrivalAndDepartures) { if (channelUID.getId().equals(CHANNEL_ID_UPDATE)) { - updateState(channelUID, new DateTimeType((new Calendar.Builder()).setInstant(lastUpdateTime).build())); + updateState(channelUID, new DateTimeType( + ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastUpdateTime), ZoneId.systemDefault()))); return; } @@ -197,7 +201,8 @@ private void publishChannel(ChannelUID channelUID, Calendar now, long lastUpdate logger.debug("Not notifying {} because it is in the past.", channelUID.getId()); continue; } - updateState(channelUID, new DateTimeType(time)); + updateState(channelUID, + new DateTimeType(ZonedDateTime.ofInstant(time.toInstant(), ZoneId.systemDefault()))); // Update properties only when we update arrival information. This is not perfect. if (channelUID.getId().equals(CHANNEL_ID_ARRIVAL)) { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java index c4220d87bbd0c..936b50e461f31 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.tellstick.internal.TellstickBindingConstants.*; import java.math.BigDecimal; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import org.eclipse.smarthome.config.core.Configuration; @@ -266,7 +268,8 @@ public void onDeviceStateChanged(Bridge bridge, Device device, TellstickEvent ev } Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(event.getTimestamp()); - updateState(timestampChannel, new DateTimeType(cal)); + updateState(timestampChannel, + new DateTimeType(ZonedDateTime.ofInstant(cal.toInstant(), ZoneId.systemDefault()))); } } diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiClientThingHandler.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiClientThingHandler.java index f4e05c28ee562..931d2d87b8cc8 100644 --- a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiClientThingHandler.java +++ b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiClientThingHandler.java @@ -16,6 +16,8 @@ import static org.eclipse.smarthome.core.thing.ThingStatusDetail.CONFIGURATION_ERROR; import static org.openhab.binding.unifi.internal.UniFiBindingConstants.*; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import org.apache.commons.lang.StringUtils; @@ -194,7 +196,8 @@ protected void refreshChannel(UniFiClient client, ChannelUID channelUID) { case CHANNEL_LAST_SEEN: // mgb: we don't check clientOnline as lastSeen is also included in the Insights data if (client.getLastSeen() != null) { - state = new DateTimeType(client.getLastSeen()); + state = new DateTimeType( + ZonedDateTime.ofInstant(client.getLastSeen().toInstant(), ZoneId.systemDefault())); } break; From 01be56353f8908593d2d8c6a031ddd1df4cb4188 Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Sat, 20 Jun 2020 13:34:24 +0200 Subject: [PATCH 65/83] Fix DateTimeType deprecations (#7948) Related to: * https://github.com/openhab/openhab-core/pull/1500 * https://github.com/openhab/openhab-addons/pull/7918 Signed-off-by: Wouter Born --- .../internal/handler/ClockAppHandler.java | 5 +-- .../internal/grxprg/PrgBridgeHandler.java | 7 ++-- .../internal/handler/RobonectHandlerTest.java | 42 +++++++++---------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/ClockAppHandler.java b/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/ClockAppHandler.java index 9039c9aa5af7b..481f3612f7813 100644 --- a/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/ClockAppHandler.java +++ b/bundles/org.openhab.binding.lametrictime/src/main/java/org/openhab/binding/lametrictime/internal/handler/ClockAppHandler.java @@ -14,9 +14,7 @@ import static org.openhab.binding.lametrictime.internal.LaMetricTimeBindingConstants.*; -import java.time.Instant; import java.time.LocalTime; -import java.time.ZoneId; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.StringType; @@ -52,8 +50,7 @@ public void handleAppCommand(ChannelUID channelUID, Command command) { try { switch (channelUID.getId()) { case CHANNEL_APP_SET_ALARM: { - LocalTime time = Instant.ofEpochMilli(((DateTimeType) command).getCalendar().getTimeInMillis()) - .atZone(ZoneId.systemDefault()).toLocalTime(); + LocalTime time = ((DateTimeType) command).getZonedDateTime().toLocalTime(); getDevice().doAction(getWidget(), CoreApps.clock().setAlarm(true, time, null)); updateActiveAppOnDevice(); break; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgBridgeHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgBridgeHandler.java index c8bec6438b295..5b59e67a4947c 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgBridgeHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/grxprg/PrgBridgeHandler.java @@ -13,7 +13,8 @@ package org.openhab.binding.lutron.internal.grxprg; import java.io.IOException; -import java.util.Calendar; +import java.time.ZonedDateTime; +import java.util.GregorianCalendar; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -167,8 +168,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (id.equals(PrgConstants.CHANNEL_TIMECLOCK)) { if (command instanceof DateTimeType) { - final Calendar c = ((DateTimeType) command).getCalendar(); - _protocolHandler.setTime(c); + final ZonedDateTime zdt = ((DateTimeType) command).getZonedDateTime(); + _protocolHandler.setTime(GregorianCalendar.from(zdt)); } else { logger.error("Received a TIMECLOCK channel command with a non DateTimeType: {}", command); } diff --git a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java index 4790904caadd7..fdc84608011f9 100644 --- a/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java +++ b/bundles/org.openhab.binding.robonect/src/test/java/org/openhab/binding/robonect/internal/handler/RobonectHandlerTest.java @@ -16,8 +16,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import java.time.Month; import java.time.ZoneId; -import java.util.Calendar; +import java.time.ZonedDateTime; import org.eclipse.jetty.client.HttpClient; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; @@ -113,15 +114,14 @@ public void shouldUpdateNextTimerChannelWithDateTimeState() throws InterruptedEx State value = stateCaptor.getValue(); assertTrue(value instanceof DateTimeType); - DateTimeType dateTimeType = (DateTimeType) value; - assertEquals(1, dateTimeType.getCalendar().get(Calendar.DAY_OF_MONTH)); - - assertEquals(2017, dateTimeType.getCalendar().get(Calendar.YEAR)); - // calendar january is 0 - assertEquals(4, dateTimeType.getCalendar().get(Calendar.MONTH)); - assertEquals(19, dateTimeType.getCalendar().get(Calendar.HOUR_OF_DAY)); - assertEquals(0, dateTimeType.getCalendar().get(Calendar.MINUTE)); - assertEquals(0, dateTimeType.getCalendar().get(Calendar.SECOND)); + + ZonedDateTime zdt = ((DateTimeType) value).getZonedDateTime(); + assertEquals(1, zdt.getDayOfMonth()); + assertEquals(2017, zdt.getYear()); + assertEquals(Month.MAY, zdt.getMonth()); + assertEquals(19, zdt.getHour()); + assertEquals(0, zdt.getMinute()); + assertEquals(0, zdt.getSecond()); } @Test @@ -136,7 +136,7 @@ public void shouldUpdateErrorChannelsIfErrorStatusReturned() throws InterruptedE error.setDate("01.05.2017"); error.setTime("19:00:00"); error.setUnix("1493665200"); - error.setErrorCode(new Integer(22)); + error.setErrorCode(Integer.valueOf(22)); error.setErrorMessage("Dummy Message"); mowerInfo.getStatus().setStatus(MowerStatus.ERROR_STATUS); mowerInfo.setError(error); @@ -164,14 +164,14 @@ public void shouldUpdateErrorChannelsIfErrorStatusReturned() throws InterruptedE State errorDate = errorDateCaptor.getValue(); assertTrue(errorDate instanceof DateTimeType); - DateTimeType dateTimeType = (DateTimeType) errorDate; - assertEquals(1, dateTimeType.getCalendar().get(Calendar.DAY_OF_MONTH)); - assertEquals(2017, dateTimeType.getCalendar().get(Calendar.YEAR)); - // calendar january is 0 - assertEquals(4, dateTimeType.getCalendar().get(Calendar.MONTH)); - assertEquals(19, dateTimeType.getCalendar().get(Calendar.HOUR_OF_DAY)); - assertEquals(0, dateTimeType.getCalendar().get(Calendar.MINUTE)); - assertEquals(0, dateTimeType.getCalendar().get(Calendar.SECOND)); + + ZonedDateTime zdt = ((DateTimeType) errorDate).getZonedDateTime(); + assertEquals(1, zdt.getDayOfMonth()); + assertEquals(2017, zdt.getYear()); + assertEquals(Month.MAY, zdt.getMonth()); + assertEquals(19, zdt.getHour()); + assertEquals(0, zdt.getMinute()); + assertEquals(0, zdt.getSecond()); State errorMessage = errorMessageCaptor.getValue(); assertTrue(errorMessage instanceof StringType); @@ -298,8 +298,8 @@ public void shouldUpdateAllChannels() { assertEquals("Mowy", stateCaptorName.getValue().toFullString()); assertEquals(99, ((DecimalType) stateCaptorBattery.getValue()).intValue()); assertEquals(4, ((DecimalType) stateCaptorStatus.getValue()).intValue()); - assertEquals(55, ((QuantityType) stateCaptorDuration.getValue()).intValue()); - assertEquals(22, ((QuantityType) stateCaptorHours.getValue()).intValue()); + assertEquals(55, ((QuantityType) stateCaptorDuration.getValue()).intValue()); + assertEquals(22, ((QuantityType) stateCaptorHours.getValue()).intValue()); assertEquals(MowerMode.AUTO.name(), stateCaptorMode.getValue().toFullString()); assertEquals(OnOffType.ON, stateCaptorStarted.getValue()); assertEquals(-88, ((DecimalType) stateCaptorWlan.getValue()).intValue()); From 118a5ce3520e1960fbe7be3e060eb0dec602fc3f Mon Sep 17 00:00:00 2001 From: Martin van Wingerden Date: Sat, 20 Jun 2020 17:31:21 +0200 Subject: [PATCH 66/83] [dark-sky] Fixed rendering of table in readme (#7950) Signed-off-by: Martin van Wingerden --- bundles/org.openhab.binding.darksky/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.darksky/README.md b/bundles/org.openhab.binding.darksky/README.md index 1086e2c2c6587..4e03b3a9d028c 100644 --- a/bundles/org.openhab.binding.darksky/README.md +++ b/bundles/org.openhab.binding.darksky/README.md @@ -29,14 +29,14 @@ Once the system location will be changed, the background discovery updates the c ### Dark Sky Account | Parameter | Description | -| apikey | API key to access the Dark Sky API. **Mandatory** | |-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| apikey | API key to access the Dark Sky API. **Mandatory** | | refreshInterval | Specifies the refresh interval (in minutes). Optional, the default value is 60, the minimum value is 1. Note: when using a free API key (1000 calls/day), do not use an interval less than 2. | | language | Language to be used by the Dark Sky API. Optional, valid values are: `ar`, `az`, `be`, `bg`, `bn`, `bs`, `ca`, `cs`, `da`, `de`, `el`, `en`, `eo`, `es`, `et`, `fi`, `fr`, `he`, `hi`, `hr`, `hu`, `id`, `is`, `it`, `ja`, `ka`, `ko`, `kn`, `kw`, `lv`, `mr`, `nb`, `nl`, `no`, `pa`, `pl`, `pt`, `ro`, `ru`, `sk`, `sl`, `sr`, `sv`, `ta`, `te`, `tet`, `tr`, `uk`, `x-pig-latin`, `zh`, `zh-tw`. | ### Current Weather And Forecast -| Parameter | Description | +| Parameter | Description | |----------------|-------------------------------------------------------------------------------------------------------------------------------| | location | Location of weather in geographical coordinates (latitude/longitude/altitude). **Mandatory** | | forecastHours | Number of hours for hourly forecast. Optional, the default value is 24 (min="0", max="48", step="1"). | From 4b7b6eccc7594b98737c5b56c1305e9bf2783b1c Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sat, 20 Jun 2020 19:17:00 +0200 Subject: [PATCH 67/83] [netatmo] Set the minimum refresh interval to 2s (#7955) Signed-off-by: Laurent Garnier --- .../internal/handler/NetatmoDeviceHandler.java | 15 ++++++++++++++- .../src/main/resources/ESH-INF/config/config.xml | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java index dccc175b69895..ff274376ad233 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java @@ -49,6 +49,9 @@ */ public abstract class NetatmoDeviceHandler extends AbstractNetatmoThingHandler { + private static final int MIN_REFRESH_INTERVAL = 2000; + private static final int DEFAULT_REFRESH_INTERVAL = 300000; + private Logger logger = LoggerFactory.getLogger(NetatmoDeviceHandler.class); private ScheduledFuture refreshJob; private RefreshStrategy refreshStrategy; @@ -209,7 +212,17 @@ private void defineRefreshInterval() { } } else { Object interval = config.get(REFRESH_INTERVAL); - dataValidityPeriod = (BigDecimal) interval; + if (interval instanceof BigDecimal) { + dataValidityPeriod = (BigDecimal) interval; + if (dataValidityPeriod.intValue() < MIN_REFRESH_INTERVAL) { + logger.info( + "Refresh interval setting is too small for thing {}, {} ms is considered as refresh interval.", + thing.getUID(), MIN_REFRESH_INTERVAL); + dataValidityPeriod = new BigDecimal(MIN_REFRESH_INTERVAL); + } + } else { + dataValidityPeriod = new BigDecimal(DEFAULT_REFRESH_INTERVAL); + } } refreshStrategy = new RefreshStrategy(dataValidityPeriod.intValue()); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/config/config.xml index 8d5f222af057e..9b82d5a2f4e60 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/config/config.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/config/config.xml @@ -92,7 +92,7 @@ true - + The refresh interval to poll Netatmo API (in ms). 300000 @@ -141,7 +141,7 @@ UUID of the home - + The refresh interval to poll Netatmo API (in ms). 300000 From ac8c88b5706d43217463408ee6d950b74fba409a Mon Sep 17 00:00:00 2001 From: eugen Date: Sat, 20 Jun 2020 19:19:36 +0200 Subject: [PATCH 68/83] [homekit] fix corrupt storage (#7940) * fix for race conditions * incorporate feedback * reduce number of interactions with the storage. fix bug in clear method Signed-off-by: Eugen Freiter --- .../java/org/openhab/io/homekit/Homekit.java | 5 +++ .../io/homekit/internal/Debouncer.java | 15 ++++++- .../homekit/internal/HomekitAuthInfoImpl.java | 45 ++++++++++--------- .../internal/HomekitChangeListener.java | 6 ++- .../internal/HomekitCommandExtension.java | 20 +-------- .../io/homekit/internal/HomekitImpl.java | 23 ++++++++-- 6 files changed, 69 insertions(+), 45 deletions(-) diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java index fd2f4f2af1749..825775ce9edb7 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java @@ -48,4 +48,9 @@ public interface Homekit { * returns list of HomeKit accessories registered at bridge. */ List getAccessories(); + + /** + * clear all pairings with HomeKit clients + */ + public void clearHomekitPairings(); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java index ee4a39ad8d9e7..380d539cf6811 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java @@ -15,6 +15,7 @@ import java.time.Clock; import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -41,6 +42,7 @@ class Debouncer { private final Logger logger = LoggerFactory.getLogger(Debouncer.class); private volatile Long lastCallAttempt; + private ScheduledFuture feature; /** * Highly performant generic debouncer @@ -74,7 +76,16 @@ void call() { lastCallAttempt = clock.millis(); calls.incrementAndGet(); if (pending.compareAndSet(false, true)) { - scheduler.schedule(this::tryActionOrPostpone, delayMs, TimeUnit.MILLISECONDS); + feature = scheduler.schedule(this::tryActionOrPostpone, delayMs, TimeUnit.MILLISECONDS); + } + } + + public void stop() { + logger.trace("stop debouncer"); + if (feature != null) { + feature.cancel(true); + calls.set(0); + pending.set(false); } } @@ -100,7 +111,7 @@ private void tryActionOrPostpone() { // Note: we use Math.max as there's a _very_ small chance lastCallAttempt could advance in another thread, // and result in a negative calculation long delay = Math.max(1, lastCallAttempt - now + delayMs); - scheduler.schedule(this::tryActionOrPostpone, delay, TimeUnit.MILLISECONDS); + feature = scheduler.schedule(this::tryActionOrPostpone, delay, TimeUnit.MILLISECONDS); } } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java index ea7a6afbce1e2..6b741ea874db9 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java @@ -18,8 +18,9 @@ import java.util.Collection; import java.util.HashSet; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.storage.Storage; -import org.eclipse.smarthome.core.storage.StorageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,18 +37,15 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo { private final Logger logger = LoggerFactory.getLogger(HomekitAuthInfoImpl.class); private final Storage storage; - private final String mac; - private final BigInteger salt; - private final byte[] privateKey; + private String mac; + private BigInteger salt; + private byte[] privateKey; private final String pin; - public HomekitAuthInfoImpl(StorageService storageService, String pin) throws InvalidAlgorithmParameterException { - storage = storageService.getStorage("homekit"); - initializeStorage(); + public HomekitAuthInfoImpl(final Storage storage, final String pin) throws InvalidAlgorithmParameterException { + this.storage = storage; this.pin = pin; - mac = storage.get("mac"); - salt = new BigInteger(storage.get("salt")); - privateKey = Base64.getDecoder().decode(storage.get("privateKey")); + initializeStorage(); } @Override @@ -77,7 +75,7 @@ public BigInteger getSalt() { @Override public byte[] getUserPublicKey(String username) { - String encodedKey = storage.get(createUserKey(username)); + final String encodedKey = storage.get(createUserKey(username)); if (encodedKey != null) { return Base64.getDecoder().decode(encodedKey); } else { @@ -98,32 +96,39 @@ public boolean hasUser() { public void clear() { for (String key : new HashSet<>(storage.getKeys())) { - if (isUserKey("user_")) { + if (isUserKey(key)) { storage.remove(key); } } } - private String createUserKey(String username) { + private String createUserKey(final String username) { return "user_" + username; } - private boolean isUserKey(String key) { + private boolean isUserKey(final String key) { return key.startsWith("user_"); } private void initializeStorage() throws InvalidAlgorithmParameterException { - if (storage.get("mac") == null) { + mac = storage.get("mac"); + salt = new BigInteger(storage.get("salt")); + privateKey = Base64.getDecoder().decode(storage.get("privateKey")); + + if (mac == null) { logger.warn( "Could not find existing MAC in {}. Generating new MAC. This will require re-pairing of iOS devices.", storage.getClass().getName()); - storage.put("mac", HomekitServer.generateMac()); + mac = HomekitServer.generateMac(); + storage.put("mac", mac); } - if (storage.get("salt") == null) { - storage.put("salt", HomekitServer.generateSalt().toString()); + if (salt == null) { + salt = HomekitServer.generateSalt(); + storage.put("salt", salt.toString()); } - if (storage.get("privateKey") == null) { - storage.put("privateKey", Base64.getEncoder().encodeToString(HomekitServer.generateKey())); + if (privateKey == null) { + privateKey = HomekitServer.generateKey(); + storage.put("privateKey", Base64.getEncoder().encodeToString(privateKey)); } } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java index bae3bda5ab352..41b6c47c3a34a 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java @@ -149,8 +149,11 @@ private Optional getItemOptional(String name) { } public void makeNewConfigurationRevision() { - storage.put(REVISION_CONFIG, "" + accessoryRegistry.makeNewConfigurationRevision()); + final int newRevision = accessoryRegistry.makeNewConfigurationRevision(); lastAccessoryCount = accessoryRegistry.getAllAccessories().size(); + logger.trace("make new configuration revision. new revision number {}, number of accessories {}", newRevision, + lastAccessoryCount); + storage.put(REVISION_CONFIG, "" + newRevision); storage.put(ACCESSORY_COUNT, "" + lastAccessoryCount); } @@ -189,6 +192,7 @@ public synchronized void setBridge(HomekitRoot bridge) { } public synchronized void unsetBridge() { + applyUpdatesDebouncer.stop(); accessoryRegistry.unsetBridge(); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java index ee54da0c5a2ca..aca612686324e 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.concurrent.ExecutionException; -import org.eclipse.smarthome.core.storage.StorageService; import org.eclipse.smarthome.io.console.Console; import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension; import org.eclipse.smarthome.io.console.extensions.ConsoleCommandExtension; @@ -43,7 +42,6 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension { private static final String LEGACY_SUBCMD_PRINT_ACCESSORY = "printAccessory"; private final Logger logger = LoggerFactory.getLogger(HomekitCommandExtension.class); - private StorageService storageService; private Homekit homekit; public HomekitCommandExtension() { @@ -107,15 +105,6 @@ public List getUsages() { "enables or disables unauthenticated access to facilitate debugging")); } - @Reference - public void setStorageService(StorageService storageService) { - this.storageService = storageService; - } - - public void unsetStorageService(StorageService storageService) { - this.storageService = null; - } - @Reference public void setHomekit(Homekit homekit) { this.homekit = homekit; @@ -126,13 +115,8 @@ public void unsetHomekit(Homekit homekit) { } private void clearHomekitPairings(Console console) { - try { - new HomekitAuthInfoImpl(storageService, null).clear(); - homekit.refreshAuthInfo(); - console.println("Cleared HomeKit pairings"); - } catch (Exception e) { - logger.warn("Could not clear HomeKit pairings", e); - } + homekit.clearHomekitPairings(); + console.println("Cleared HomeKit pairings"); } private void allowUnauthenticatedHomekitRequests(boolean allow, Console console) { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java index 0c345685c3cab..85478165a95ad 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java @@ -30,6 +30,7 @@ import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.items.MetadataRegistry; import org.eclipse.smarthome.core.net.NetworkAddressService; +import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.storage.StorageService; import org.openhab.io.homekit.Homekit; import org.osgi.framework.Constants; @@ -60,7 +61,7 @@ public class HomekitImpl implements Homekit { private final Logger logger = LoggerFactory.getLogger(HomekitImpl.class); - private final StorageService storageService; + private final Storage storage; private final NetworkAddressService networkAddressService; private final HomekitChangeListener changeListener; @@ -68,6 +69,7 @@ public class HomekitImpl implements Homekit { private @Nullable InetAddress networkInterface; private @Nullable HomekitServer homekitServer; private @Nullable HomekitRoot bridge; + private @Nullable HomekitAuthInfoImpl authInfo; private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); @@ -76,7 +78,7 @@ public class HomekitImpl implements Homekit { public HomekitImpl(@Reference StorageService storageService, @Reference ItemRegistry itemRegistry, @Reference NetworkAddressService networkAddressService, Map config, @Reference MetadataRegistry metadataRegistry) throws IOException, InvalidAlgorithmParameterException { - this.storageService = storageService; + this.storage = storageService.getStorage("homekit"); this.networkAddressService = networkAddressService; this.settings = processConfig(config); this.changeListener = new HomekitChangeListener(itemRegistry, settings, metadataRegistry, storageService); @@ -125,8 +127,9 @@ private void stopBridge() { private void startBridge() throws InvalidAlgorithmParameterException, IOException { final HomekitServer homekitServer = this.homekitServer; if (homekitServer != null && bridge == null) { - final HomekitRoot bridge = homekitServer.createBridge(new HomekitAuthInfoImpl(storageService, settings.pin), - settings.name, HomekitSettings.MANUFACTURER, HomekitSettings.MODEL, HomekitSettings.SERIAL_NUMBER, + authInfo = new HomekitAuthInfoImpl(storage, settings.pin); + final HomekitRoot bridge = homekitServer.createBridge(authInfo, settings.name, HomekitSettings.MANUFACTURER, + HomekitSettings.MODEL, HomekitSettings.SERIAL_NUMBER, FrameworkUtil.getBundle(getClass()).getVersion().toString(), HomekitSettings.HARDWARE_REVISION); changeListener.setBridge(bridge); this.bridge = bridge; @@ -203,4 +206,16 @@ public void allowUnauthenticatedRequests(boolean allow) { public List getAccessories() { return new ArrayList(this.changeListener.getAccessories().values()); } + + @Override + public void clearHomekitPairings() { + try { + if (authInfo != null) { + authInfo.clear(); + refreshAuthInfo(); + } + } catch (Exception e) { + logger.warn("Could not clear HomeKit pairings", e); + } + } } From 033a263e85f6f9844cda42993252f71d134cc611 Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Sat, 20 Jun 2020 19:21:36 +0200 Subject: [PATCH 69/83] [netatmo] NAPresenceCameraHandlerTest uses internal I18nProviderImpl class (#7954) - Test-usage of internal classes removed (org.eclipse.smarthome.core.internal) due to OH3 compatibility - New tests added Signed-off-by: Sven Strohschein --- .../presence/NAPresenceCameraHandlerTest.java | 113 +++++++++++++++--- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java index 94c4e8c0bd981..977f754e232f8 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java @@ -14,13 +14,15 @@ import io.swagger.client.model.NAWelcomeCamera; import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.smarthome.core.internal.i18n.I18nProviderImpl; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.internal.ThingImpl; import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.UnDefType; import org.junit.Before; import org.junit.Test; @@ -46,30 +48,24 @@ public class NAPresenceCameraHandlerTest { @Mock private RequestExecutor requestExecutorMock; + @Mock + private TimeZoneProvider timeZoneProviderMock; private Thing presenceCameraThing; private NAWelcomeCamera presenceCamera; private ChannelUID floodlightChannelUID; private ChannelUID floodlightAutoModeChannelUID; - private NAPresenceCameraHandler handler; + private NAPresenceCameraHandlerAccessible handler; @Before public void before() { presenceCameraThing = new ThingImpl(new ThingTypeUID("netatmo", "NOC"), "1"); presenceCamera = new NAWelcomeCamera(); + floodlightChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT); floodlightAutoModeChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE); - handler = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()) { - { - module = presenceCamera; - } - - @Override - @NonNull Optional<@NonNull String> executeGETRequest(@NonNull String url) { - return requestExecutorMock.executeGETRequest(url); - } - }; + handler = new NAPresenceCameraHandlerAccessible(presenceCameraThing, presenceCamera); } @Test @@ -191,7 +187,7 @@ public void testHandleCommand_Request_failed() { } @Test - public void testHandleCommand_VPN_URL_not_set() { + public void testHandleCommand_without_VPN() { handler.handleCommand(floodlightChannelUID, OnOffType.ON); verify(requestExecutorMock, never()).executeGETRequest(any()); //no executions because the VPN URL is still unknown @@ -229,7 +225,7 @@ public void testHandleCommand_Ping_failed_wrong_Response() { @Test public void testHandleCommand_Module_NULL() { - NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, timeZoneProviderMock); handlerWithoutModule.handleCommand(floodlightChannelUID, OnOffType.ON); verify(requestExecutorMock, never()).executeGETRequest(any()); //no executions because the thing isn't initialized @@ -266,7 +262,7 @@ public void testGetNAThingProperty_Floodlight_without_LightModeState() { @Test public void testGetNAThingProperty_Floodlight_Module_NULL() { - NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, timeZoneProviderMock); assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightChannelUID.getId())); } @@ -331,10 +327,72 @@ public void testGetNAThingProperty_Floodlight_Scenario_without_AutoMode() { @Test public void testGetNAThingProperty_FloodlightAutoMode_Module_NULL() { - NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, new I18nProviderImpl()); + NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing, timeZoneProviderMock); assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightAutoModeChannelUID.getId())); } + @Test + public void testGetStreamURL() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + String streamURL = handler.getStreamURL("dummyVideoId"); + assertNotNull(streamURL); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + } + + @Test + public void testGetStreamURL_local() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + presenceCamera.setIsLocal(true); + + String streamURL = handler.getStreamURL("dummyVideoId"); + assertNotNull(streamURL); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL); + } + + @Test + public void testGetStreamURL_not_local() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + presenceCamera.setIsLocal(false); + + String streamURL = handler.getStreamURL("dummyVideoId"); + assertNotNull(streamURL); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + } + + @Test + public void testGetStreamURL_without_VPN() { + String streamURL = handler.getStreamURL("dummyVideoId"); + assertNull(streamURL); + } + + @Test + public void testGetLivePictureURLState() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + + State livePictureURLState = handler.getLivePictureURLState(); + assertEquals(new StringType(DUMMY_VPN_URL + "/live/snapshot_720.jpg"), livePictureURLState); + } + + @Test + public void testGetLivePictureURLState_without_VPN() { + State livePictureURLState = handler.getLivePictureURLState(); + assertEquals(UnDefType.UNDEF, livePictureURLState); + } + + @Test + public void testGetLiveStreamState() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + + State liveStreamState = handler.getLiveStreamState(); + assertEquals(new StringType(DUMMY_VPN_URL + "/live/index.m3u8"), liveStreamState); + } + + @Test + public void testGetLiveStreamState_without_VPN() { + State liveStreamState = handler.getLiveStreamState(); + assertEquals(UnDefType.UNDEF, liveStreamState); + } + private static Optional createPingResponseContent(final String localURL) { return Optional.of("{\"local_url\":\"" + localURL + "\",\"product_name\":\"Welcome Netatmo\"}"); } @@ -343,4 +401,27 @@ private interface RequestExecutor { Optional executeGETRequest(String url); } + + private class NAPresenceCameraHandlerAccessible extends NAPresenceCameraHandler { + + public NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) { + super(thing, timeZoneProviderMock); + module = presenceCamera; + } + + @Override + protected @NonNull Optional<@NonNull String> executeGETRequest(@NonNull String url) { + return requestExecutorMock.executeGETRequest(url); + } + + @Override + protected @NonNull State getLivePictureURLState() { + return super.getLivePictureURLState(); + } + + @Override + protected @NonNull State getLiveStreamState() { + return super.getLiveStreamState(); + } + } } From 7d4ce8b4edd0d5e6778cbf540fcaaf4653fad633 Mon Sep 17 00:00:00 2001 From: robnielsen Date: Sat, 20 Jun 2020 12:23:53 -0500 Subject: [PATCH 70/83] [insteon] Fix Java 11 deprecation messages (#7951) Signed-off-by: Rob Nielsen --- .../org/openhab/binding/insteon/internal/device/X10.java | 4 ++-- .../org/openhab/binding/insteon/internal/driver/Port.java | 2 +- .../org/openhab/binding/insteon/internal/message/Msg.java | 6 +++--- .../openhab/binding/insteon/internal/message/MsgType.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/X10.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/X10.java index 1190e79b60192..122e616530bd7 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/X10.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/X10.java @@ -69,7 +69,7 @@ public byte code() { * @return clear text house code, i.e letter A-P */ public static String houseToString(byte c) { - String s = houseCodeToString.get(new Integer(c & 0xff)); + String s = houseCodeToString.get(c & 0xff); return (s == null) ? "X" : s; } @@ -80,7 +80,7 @@ public static String houseToString(byte c) { * @return decoded integer, i.e. number 0-16 */ public static int unitToInt(byte c) { - Integer i = unitCodeToInt.get(new Integer(c & 0xff)); + Integer i = unitCodeToInt.get(c & 0xff); return (i == null) ? -1 : i; } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java index 2737eee2735ac..e10313f6f7d57 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java @@ -374,7 +374,7 @@ private int dropBytes(byte[] buffer, int len) { ArrayList l = new ArrayList<>(); for (int i = 0; i < len; i++) { if (rng.nextInt(100) >= dropRate) { - l.add(new Byte(buffer[i])); + l.add(buffer[i]); } } for (int i = 0; i < l.size(); i++) { diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/Msg.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/Msg.java index 7dbcf68fe677f..6776722813282 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/Msg.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/Msg.java @@ -536,7 +536,7 @@ public int compare(@Nullable Field f1, @Nullable Field f2) { * @return the length of the header to expect */ public static int getHeaderLength(byte cmd) { - Integer len = HEADER_MAP.get(new Integer(cmd)); + Integer len = HEADER_MAP.get((int) cmd); if (len == null) { return (-1); // not found } @@ -603,7 +603,7 @@ private static int cmdToKey(byte cmd, boolean isExtended) { private static void buildHeaderMap() { for (Msg m : MSG_MAP.values()) { if (m.getDirection() == Direction.FROM_MODEM) { - HEADER_MAP.put(new Integer(m.getCommandNumber()), m.getHeaderLength()); + HEADER_MAP.put((int) m.getCommandNumber(), m.getHeaderLength()); } } } @@ -611,7 +611,7 @@ private static void buildHeaderMap() { private static void buildLengthMap() { for (Msg m : MSG_MAP.values()) { if (m.getDirection() == Direction.FROM_MODEM) { - Integer key = new Integer(cmdToKey(m.getCommandNumber(), m.isExtended())); + int key = cmdToKey(m.getCommandNumber(), m.isExtended()); REPLY_MAP.put(key, m); } } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/MsgType.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/MsgType.java index c0eac34a86bda..2902043730002 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/MsgType.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/message/MsgType.java @@ -62,7 +62,7 @@ public enum MsgType { static { for (MsgType t : MsgType.values()) { - Integer i = new Integer(t.getByteValue() & 0xff); + int i = t.getByteValue() & 0xff; hash.put(i, t); } } @@ -72,7 +72,7 @@ private int getByteValue() { } public static MsgType fromValue(byte b) throws IllegalArgumentException { - Integer i = new Integer((b & 0xe0)); + int i = b & 0xe0; @Nullable MsgType mt = hash.get(i); if (mt == null) { From 1ba697ff1a393a542094ef36d79fa1bf703ce90c Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Sun, 21 Jun 2020 00:18:46 +0200 Subject: [PATCH 71/83] [netatmo] The lastEvent is detected/compared incorrectly (#7963) (#7964) * Bug-fix - The last event wasn't detected/compared correctly... * Tests regarding lastEvent added Signed-off-by: Sven Strohschein --- .../welcome/NAWelcomeHomeHandler.java | 2 +- .../welcome/NAWelcomeHomeHandlerTest.java | 150 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java index 67dabbd2807e4..19b4ba0b81557 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java @@ -96,7 +96,7 @@ public NAWelcomeHomeHandler(Thing thing, final TimeZoneProvider timeZoneProvider }); Optional previousLastEvent = lastEvent; - lastEvent = result.getEvents().stream().min(Comparator.comparingInt(NAWelcomeEvent::getTime)); + lastEvent = result.getEvents().stream().max(Comparator.comparingInt(NAWelcomeEvent::getTime)); isNewLastEvent = previousLastEvent.isPresent() && !previousLastEvent.equals(lastEvent); } } diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java new file mode 100644 index 0000000000000..e71fcaac44a1a --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java @@ -0,0 +1,150 @@ +/** + * 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.netatmo.internal.welcome; + +import io.swagger.client.model.NAWelcomeEvent; +import io.swagger.client.model.NAWelcomeHome; +import io.swagger.client.model.NAWelcomeHomeData; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.internal.ThingImpl; +import org.eclipse.smarthome.core.types.UnDefType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.openhab.binding.netatmo.internal.NetatmoBindingConstants; +import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; +import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Sven Strohschein + */ +@RunWith(MockitoJUnitRunner.class) +public class NAWelcomeHomeHandlerTest { + + private static final String DUMMY_HOME_ID = "1"; + + @Mock + private TimeZoneProvider timeZoneProviderMock; + private Thing welcomeHomeThing; + private NAWelcomeHomeHandler handler; + @Mock + private NetatmoBridgeHandler bridgeHandlerMock; + + @Before + public void before() { + welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1"); + handler = new NAWelcomeHomeHandler(welcomeHomeThing, timeZoneProviderMock) { + @Override + protected NetatmoBridgeHandler getBridgeHandler() { + return bridgeHandlerMock; + } + + @Override + protected String getId() { + return DUMMY_HOME_ID; + } + }; + } + + @Test + public void testUpdateReadings_with_Events() { + NAWelcomeEvent event_1 = new NAWelcomeEvent(); + event_1.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + event_1.setTime(1592661881); + + NAWelcomeEvent event_2 = new NAWelcomeEvent(); + event_2.setType(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString()); + event_2.setTime(1592661882); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Arrays.asList(event_1, event_2)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + //the second (last) event is expected + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_2, event_1)); + //the second (last) event is still expected (independent from the order of these are added) + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + } + + @Test + public void testUpdateReadings_with_1_Event() { + NAWelcomeEvent event = new NAWelcomeEvent(); + event.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Collections.singletonList(event)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + assertEquals(new StringType("person"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_no_Events() { + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_empty_HomeData() { + when(bridgeHandlerMock.getWelcomeDataBody(any())).thenReturn(new NAWelcomeHomeData()); + + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_no_HomeData() { + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } +} From 3e7b97ea11048ff515ee176bfc2aa36c799db87d Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Sun, 21 Jun 2020 00:42:59 +0200 Subject: [PATCH 72/83] Line-endings fixed... (#7966) Signed-off-by: Sven Strohschein --- .../welcome/NAWelcomeHomeHandlerTest.java | 300 +++++++++--------- 1 file changed, 150 insertions(+), 150 deletions(-) diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java index e71fcaac44a1a..683e77392e1f4 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java @@ -1,150 +1,150 @@ -/** - * 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.netatmo.internal.welcome; - -import io.swagger.client.model.NAWelcomeEvent; -import io.swagger.client.model.NAWelcomeHome; -import io.swagger.client.model.NAWelcomeHomeData; -import org.eclipse.smarthome.core.i18n.TimeZoneProvider; -import org.eclipse.smarthome.core.library.types.StringType; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.eclipse.smarthome.core.thing.internal.ThingImpl; -import org.eclipse.smarthome.core.types.UnDefType; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.openhab.binding.netatmo.internal.NetatmoBindingConstants; -import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; -import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Sven Strohschein - */ -@RunWith(MockitoJUnitRunner.class) -public class NAWelcomeHomeHandlerTest { - - private static final String DUMMY_HOME_ID = "1"; - - @Mock - private TimeZoneProvider timeZoneProviderMock; - private Thing welcomeHomeThing; - private NAWelcomeHomeHandler handler; - @Mock - private NetatmoBridgeHandler bridgeHandlerMock; - - @Before - public void before() { - welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1"); - handler = new NAWelcomeHomeHandler(welcomeHomeThing, timeZoneProviderMock) { - @Override - protected NetatmoBridgeHandler getBridgeHandler() { - return bridgeHandlerMock; - } - - @Override - protected String getId() { - return DUMMY_HOME_ID; - } - }; - } - - @Test - public void testUpdateReadings_with_Events() { - NAWelcomeEvent event_1 = new NAWelcomeEvent(); - event_1.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); - event_1.setTime(1592661881); - - NAWelcomeEvent event_2 = new NAWelcomeEvent(); - event_2.setType(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString()); - event_2.setTime(1592661882); - - NAWelcomeHome home = new NAWelcomeHome(); - home.setId(DUMMY_HOME_ID); - home.setEvents(Arrays.asList(event_1, event_2)); - - NAWelcomeHomeData homeData = new NAWelcomeHomeData(); - homeData.setHomes(Collections.singletonList(home)); - - when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); - - handler.updateReadings(); - - //the second (last) event is expected - assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - - home.setEvents(Arrays.asList(event_2, event_1)); - //the second (last) event is still expected (independent from the order of these are added) - assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - - } - - @Test - public void testUpdateReadings_with_1_Event() { - NAWelcomeEvent event = new NAWelcomeEvent(); - event.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); - - NAWelcomeHome home = new NAWelcomeHome(); - home.setId(DUMMY_HOME_ID); - home.setEvents(Collections.singletonList(event)); - - NAWelcomeHomeData homeData = new NAWelcomeHomeData(); - homeData.setHomes(Collections.singletonList(home)); - - when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); - - handler.updateReadings(); - - assertEquals(new StringType("person"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } - - @Test - public void testUpdateReadings_no_Events() { - NAWelcomeHome home = new NAWelcomeHome(); - home.setId(DUMMY_HOME_ID); - - NAWelcomeHomeData homeData = new NAWelcomeHomeData(); - homeData.setHomes(Collections.singletonList(home)); - - when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); - - handler.updateReadings(); - - assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } - - @Test - public void testUpdateReadings_empty_HomeData() { - when(bridgeHandlerMock.getWelcomeDataBody(any())).thenReturn(new NAWelcomeHomeData()); - - handler.updateReadings(); - - assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } - - @Test - public void testUpdateReadings_no_HomeData() { - handler.updateReadings(); - - assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } -} +/** + * 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.netatmo.internal.welcome; + +import io.swagger.client.model.NAWelcomeEvent; +import io.swagger.client.model.NAWelcomeHome; +import io.swagger.client.model.NAWelcomeHomeData; +import org.eclipse.smarthome.core.i18n.TimeZoneProvider; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.internal.ThingImpl; +import org.eclipse.smarthome.core.types.UnDefType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.openhab.binding.netatmo.internal.NetatmoBindingConstants; +import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; +import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Sven Strohschein + */ +@RunWith(MockitoJUnitRunner.class) +public class NAWelcomeHomeHandlerTest { + + private static final String DUMMY_HOME_ID = "1"; + + @Mock + private TimeZoneProvider timeZoneProviderMock; + private Thing welcomeHomeThing; + private NAWelcomeHomeHandler handler; + @Mock + private NetatmoBridgeHandler bridgeHandlerMock; + + @Before + public void before() { + welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1"); + handler = new NAWelcomeHomeHandler(welcomeHomeThing, timeZoneProviderMock) { + @Override + protected NetatmoBridgeHandler getBridgeHandler() { + return bridgeHandlerMock; + } + + @Override + protected String getId() { + return DUMMY_HOME_ID; + } + }; + } + + @Test + public void testUpdateReadings_with_Events() { + NAWelcomeEvent event_1 = new NAWelcomeEvent(); + event_1.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + event_1.setTime(1592661881); + + NAWelcomeEvent event_2 = new NAWelcomeEvent(); + event_2.setType(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString()); + event_2.setTime(1592661882); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Arrays.asList(event_1, event_2)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + //the second (last) event is expected + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_2, event_1)); + //the second (last) event is still expected (independent from the order of these are added) + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + } + + @Test + public void testUpdateReadings_with_1_Event() { + NAWelcomeEvent event = new NAWelcomeEvent(); + event.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Collections.singletonList(event)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + assertEquals(new StringType("person"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_no_Events() { + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_empty_HomeData() { + when(bridgeHandlerMock.getWelcomeDataBody(any())).thenReturn(new NAWelcomeHomeData()); + + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + @Test + public void testUpdateReadings_no_HomeData() { + handler.updateReadings(); + + assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } +} From 6bfac2151f440e27cb52bdb69fb8d47a6cb026e1 Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 21 Jun 2020 11:52:12 +0000 Subject: [PATCH 73/83] [unleash-maven-plugin] Preparation for next development cycle. --- bom/openhab-addons/pom.xml | 6 ++---- bom/openhab-core-index/pom.xml | 6 ++---- bom/pom.xml | 15 +++++---------- bom/runtime-index/pom.xml | 6 ++---- bom/test-index/pom.xml | 6 ++---- bundles/org.openhab.binding.adorne/pom.xml | 6 ++---- bundles/org.openhab.binding.airquality/pom.xml | 6 ++---- bundles/org.openhab.binding.airvisualnode/pom.xml | 6 ++---- bundles/org.openhab.binding.alarmdecoder/pom.xml | 6 ++---- bundles/org.openhab.binding.allplay/pom.xml | 6 ++---- .../org.openhab.binding.amazondashbutton/pom.xml | 6 ++---- .../org.openhab.binding.amazonechocontrol/pom.xml | 6 ++---- .../org.openhab.binding.ambientweather/pom.xml | 6 ++---- bundles/org.openhab.binding.astro/pom.xml | 6 ++---- bundles/org.openhab.binding.atlona/pom.xml | 6 ++---- bundles/org.openhab.binding.autelis/pom.xml | 6 ++---- bundles/org.openhab.binding.avmfritz/pom.xml | 6 ++---- bundles/org.openhab.binding.bigassfan/pom.xml | 6 ++---- .../pom.xml | 6 ++---- .../org.openhab.binding.bluetooth.am43/pom.xml | 6 ++---- .../pom.xml | 6 ++---- .../org.openhab.binding.bluetooth.bluez/pom.xml | 6 ++---- .../org.openhab.binding.bluetooth.blukii/pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.binding.bluetooth/pom.xml | 6 ++---- bundles/org.openhab.binding.boschindego/pom.xml | 6 ++---- .../org.openhab.binding.bosesoundtouch/pom.xml | 6 ++---- bundles/org.openhab.binding.bsblan/pom.xml | 6 ++---- bundles/org.openhab.binding.buienradar/pom.xml | 6 ++---- bundles/org.openhab.binding.cbus/pom.xml | 6 ++---- bundles/org.openhab.binding.chromecast/pom.xml | 6 ++---- bundles/org.openhab.binding.cm11a/pom.xml | 6 ++---- bundles/org.openhab.binding.coolmasternet/pom.xml | 6 ++---- bundles/org.openhab.binding.coronastats/pom.xml | 6 ++---- bundles/org.openhab.binding.daikin/pom.xml | 6 ++---- .../org.openhab.binding.danfossairunit/pom.xml | 6 ++---- bundles/org.openhab.binding.darksky/pom.xml | 6 ++---- bundles/org.openhab.binding.deconz/pom.xml | 6 ++---- bundles/org.openhab.binding.denonmarantz/pom.xml | 6 ++---- bundles/org.openhab.binding.digiplex/pom.xml | 6 ++---- bundles/org.openhab.binding.digitalstrom/pom.xml | 6 ++---- .../org.openhab.binding.dlinksmarthome/pom.xml | 6 ++---- bundles/org.openhab.binding.dmx/pom.xml | 6 ++---- bundles/org.openhab.binding.doorbird/pom.xml | 6 ++---- bundles/org.openhab.binding.dscalarm/pom.xml | 6 ++---- bundles/org.openhab.binding.dsmr/pom.xml | 6 ++---- bundles/org.openhab.binding.dwdpollenflug/pom.xml | 6 ++---- bundles/org.openhab.binding.dwdunwetter/pom.xml | 6 ++---- bundles/org.openhab.binding.ecobee/pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.binding.energenie/pom.xml | 6 ++---- bundles/org.openhab.binding.enigma2/pom.xml | 5 ++--- bundles/org.openhab.binding.enocean/pom.xml | 6 ++---- bundles/org.openhab.binding.enturno/pom.xml | 6 ++---- bundles/org.openhab.binding.etherrain/pom.xml | 6 ++---- bundles/org.openhab.binding.evohome/pom.xml | 6 ++---- bundles/org.openhab.binding.exec/pom.xml | 6 ++---- bundles/org.openhab.binding.feed/pom.xml | 6 ++---- bundles/org.openhab.binding.feican/pom.xml | 6 ++---- bundles/org.openhab.binding.fmiweather/pom.xml | 6 ++---- bundles/org.openhab.binding.folding/pom.xml | 6 ++---- bundles/org.openhab.binding.foobot/pom.xml | 6 ++---- bundles/org.openhab.binding.freebox/pom.xml | 6 ++---- bundles/org.openhab.binding.fronius/pom.xml | 6 ++---- .../org.openhab.binding.fsinternetradio/pom.xml | 6 ++---- bundles/org.openhab.binding.ftpupload/pom.xml | 6 ++---- bundles/org.openhab.binding.gardena/pom.xml | 6 ++---- bundles/org.openhab.binding.globalcache/pom.xml | 6 ++---- bundles/org.openhab.binding.goecharger/pom.xml | 6 ++---- bundles/org.openhab.binding.gpstracker/pom.xml | 6 ++---- bundles/org.openhab.binding.groheondus/pom.xml | 6 ++---- bundles/org.openhab.binding.harmonyhub/pom.xml | 6 ++---- bundles/org.openhab.binding.hdanywhere/pom.xml | 6 ++---- bundles/org.openhab.binding.hdpowerview/pom.xml | 6 ++---- bundles/org.openhab.binding.helios/pom.xml | 6 ++---- bundles/org.openhab.binding.heos/pom.xml | 6 ++---- bundles/org.openhab.binding.homematic/pom.xml | 6 ++---- bundles/org.openhab.binding.hpprinter/pom.xml | 6 ++---- bundles/org.openhab.binding.hue/pom.xml | 6 ++---- bundles/org.openhab.binding.hydrawise/pom.xml | 6 ++---- bundles/org.openhab.binding.hyperion/pom.xml | 6 ++---- bundles/org.openhab.binding.iaqualink/pom.xml | 6 ++---- bundles/org.openhab.binding.icalendar/pom.xml | 6 ++---- bundles/org.openhab.binding.icloud/pom.xml | 6 ++---- bundles/org.openhab.binding.ihc/pom.xml | 6 ++---- .../org.openhab.binding.innogysmarthome/pom.xml | 6 ++---- bundles/org.openhab.binding.insteon/pom.xml | 6 ++---- bundles/org.openhab.binding.ipp/pom.xml | 6 ++---- bundles/org.openhab.binding.irtrans/pom.xml | 6 ++---- bundles/org.openhab.binding.jeelink/pom.xml | 6 ++---- bundles/org.openhab.binding.keba/pom.xml | 6 ++---- bundles/org.openhab.binding.km200/pom.xml | 6 ++---- bundles/org.openhab.binding.knx/pom.xml | 6 ++---- bundles/org.openhab.binding.kodi/pom.xml | 6 ++---- bundles/org.openhab.binding.konnected/pom.xml | 6 ++---- .../org.openhab.binding.kostalinverter/pom.xml | 6 ++---- bundles/org.openhab.binding.lametrictime/pom.xml | 6 ++---- bundles/org.openhab.binding.lcn/pom.xml | 6 ++---- bundles/org.openhab.binding.leapmotion/pom.xml | 6 ++---- bundles/org.openhab.binding.lghombot/pom.xml | 6 ++---- bundles/org.openhab.binding.lgtvserial/pom.xml | 6 ++---- bundles/org.openhab.binding.lgwebos/pom.xml | 6 ++---- bundles/org.openhab.binding.lifx/pom.xml | 6 ++---- bundles/org.openhab.binding.linky/pom.xml | 6 ++---- bundles/org.openhab.binding.linuxinput/pom.xml | 6 ++---- bundles/org.openhab.binding.lirc/pom.xml | 6 ++---- bundles/org.openhab.binding.logreader/pom.xml | 6 ++---- bundles/org.openhab.binding.loxone/pom.xml | 6 ++---- bundles/org.openhab.binding.lutron/pom.xml | 6 ++---- bundles/org.openhab.binding.mail/pom.xml | 6 ++---- bundles/org.openhab.binding.max/pom.xml | 6 ++---- bundles/org.openhab.binding.mcp23017/pom.xml | 6 ++---- bundles/org.openhab.binding.melcloud/pom.xml | 6 ++---- bundles/org.openhab.binding.meteoalerte/pom.xml | 2 +- bundles/org.openhab.binding.meteoblue/pom.xml | 6 ++---- bundles/org.openhab.binding.meteostick/pom.xml | 6 ++---- bundles/org.openhab.binding.miele/pom.xml | 6 ++---- bundles/org.openhab.binding.mihome/pom.xml | 6 ++---- bundles/org.openhab.binding.miio/pom.xml | 6 ++---- bundles/org.openhab.binding.milight/pom.xml | 6 ++---- bundles/org.openhab.binding.millheat/pom.xml | 6 ++---- bundles/org.openhab.binding.minecraft/pom.xml | 6 ++---- .../org.openhab.binding.modbus.sunspec/pom.xml | 6 ++---- bundles/org.openhab.binding.modbus/pom.xml | 6 ++---- bundles/org.openhab.binding.mqtt.generic/pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.binding.mqtt.homie/pom.xml | 6 ++---- bundles/org.openhab.binding.mqtt/pom.xml | 6 ++---- bundles/org.openhab.binding.nanoleaf/pom.xml | 6 ++---- bundles/org.openhab.binding.neato/pom.xml | 6 ++---- bundles/org.openhab.binding.neeo/pom.xml | 6 ++---- bundles/org.openhab.binding.neohub/pom.xml | 6 ++---- bundles/org.openhab.binding.nest/pom.xml | 6 ++---- bundles/org.openhab.binding.netatmo/pom.xml | 6 ++---- bundles/org.openhab.binding.network/pom.xml | 6 ++---- .../org.openhab.binding.networkupstools/pom.xml | 6 ++---- bundles/org.openhab.binding.nibeheatpump/pom.xml | 6 ++---- bundles/org.openhab.binding.nibeuplink/pom.xml | 6 ++---- bundles/org.openhab.binding.nikobus/pom.xml | 6 ++---- .../org.openhab.binding.nikohomecontrol/pom.xml | 6 ++---- bundles/org.openhab.binding.novafinedust/pom.xml | 5 ++--- bundles/org.openhab.binding.ntp/pom.xml | 6 ++---- bundles/org.openhab.binding.nuki/pom.xml | 6 ++---- bundles/org.openhab.binding.oceanic/pom.xml | 6 ++---- bundles/org.openhab.binding.omnikinverter/pom.xml | 6 ++---- bundles/org.openhab.binding.onebusaway/pom.xml | 6 ++---- bundles/org.openhab.binding.onewire/pom.xml | 6 ++---- bundles/org.openhab.binding.onewiregpio/pom.xml | 6 ++---- bundles/org.openhab.binding.onkyo/pom.xml | 6 ++---- bundles/org.openhab.binding.opengarage/pom.xml | 6 ++---- bundles/org.openhab.binding.opensprinkler/pom.xml | 6 ++---- .../org.openhab.binding.openthermgateway/pom.xml | 6 ++---- bundles/org.openhab.binding.openuv/pom.xml | 6 ++---- .../org.openhab.binding.openweathermap/pom.xml | 6 ++---- bundles/org.openhab.binding.orvibo/pom.xml | 6 ++---- bundles/org.openhab.binding.paradoxalarm/pom.xml | 6 ++---- bundles/org.openhab.binding.pentair/pom.xml | 6 ++---- bundles/org.openhab.binding.phc/pom.xml | 6 ++---- bundles/org.openhab.binding.pioneeravr/pom.xml | 6 ++---- bundles/org.openhab.binding.pixometer/pom.xml | 6 ++---- bundles/org.openhab.binding.pjlinkdevice/pom.xml | 6 ++---- bundles/org.openhab.binding.plclogo/pom.xml | 6 ++---- bundles/org.openhab.binding.plugwise/pom.xml | 6 ++---- bundles/org.openhab.binding.powermax/pom.xml | 6 ++---- bundles/org.openhab.binding.pulseaudio/pom.xml | 6 ++---- bundles/org.openhab.binding.pushbullet/pom.xml | 6 ++---- bundles/org.openhab.binding.regoheatpump/pom.xml | 6 ++---- bundles/org.openhab.binding.rfxcom/pom.xml | 6 ++---- bundles/org.openhab.binding.rme/pom.xml | 6 ++---- bundles/org.openhab.binding.robonect/pom.xml | 6 ++---- bundles/org.openhab.binding.rotel/pom.xml | 6 ++---- bundles/org.openhab.binding.rotelra1x/pom.xml | 6 ++---- bundles/org.openhab.binding.russound/pom.xml | 6 ++---- bundles/org.openhab.binding.sagercaster/pom.xml | 6 ++---- bundles/org.openhab.binding.samsungtv/pom.xml | 6 ++---- bundles/org.openhab.binding.satel/pom.xml | 6 ++---- bundles/org.openhab.binding.seneye/pom.xml | 6 ++---- bundles/org.openhab.binding.sensebox/pom.xml | 6 ++---- bundles/org.openhab.binding.sensibo/pom.xml | 6 ++---- bundles/org.openhab.binding.serialbutton/pom.xml | 6 ++---- bundles/org.openhab.binding.shelly/pom.xml | 6 ++---- bundles/org.openhab.binding.siemensrds/pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.binding.sinope/pom.xml | 6 ++---- bundles/org.openhab.binding.sleepiq/pom.xml | 6 ++---- .../org.openhab.binding.smaenergymeter/pom.xml | 6 ++---- bundles/org.openhab.binding.smartmeter/pom.xml | 6 ++---- bundles/org.openhab.binding.smhi/pom.xml | 6 ++---- bundles/org.openhab.binding.snmp/pom.xml | 6 ++---- bundles/org.openhab.binding.solaredge/pom.xml | 6 ++---- bundles/org.openhab.binding.solarlog/pom.xml | 6 ++---- bundles/org.openhab.binding.somfymylink/pom.xml | 6 ++---- bundles/org.openhab.binding.somfytahoma/pom.xml | 6 ++---- bundles/org.openhab.binding.sonos/pom.xml | 6 ++---- bundles/org.openhab.binding.sonyaudio/pom.xml | 6 ++---- bundles/org.openhab.binding.sonyprojector/pom.xml | 6 ++---- bundles/org.openhab.binding.spotify/pom.xml | 6 ++---- bundles/org.openhab.binding.squeezebox/pom.xml | 6 ++---- bundles/org.openhab.binding.synopanalyzer/pom.xml | 6 ++---- bundles/org.openhab.binding.systeminfo/pom.xml | 6 ++---- bundles/org.openhab.binding.tado/pom.xml | 6 ++---- bundles/org.openhab.binding.tankerkoenig/pom.xml | 6 ++---- bundles/org.openhab.binding.telegram/pom.xml | 6 ++---- bundles/org.openhab.binding.tellstick/pom.xml | 6 ++---- bundles/org.openhab.binding.tesla/pom.xml | 6 ++---- bundles/org.openhab.binding.tibber/pom.xml | 6 ++---- .../org.openhab.binding.tplinksmarthome/pom.xml | 6 ++---- bundles/org.openhab.binding.tradfri/pom.xml | 6 ++---- bundles/org.openhab.binding.unifi/pom.xml | 6 ++---- bundles/org.openhab.binding.urtsi/pom.xml | 6 ++---- bundles/org.openhab.binding.valloxmv/pom.xml | 6 ++---- bundles/org.openhab.binding.vektiva/pom.xml | 6 ++---- bundles/org.openhab.binding.velbus/pom.xml | 6 ++---- bundles/org.openhab.binding.velux/pom.xml | 6 ++---- bundles/org.openhab.binding.vigicrues/pom.xml | 2 +- bundles/org.openhab.binding.vitotronic/pom.xml | 6 ++---- bundles/org.openhab.binding.volvooncall/pom.xml | 6 ++---- .../org.openhab.binding.weathercompany/pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.binding.wemo/pom.xml | 6 ++---- bundles/org.openhab.binding.wifiled/pom.xml | 6 ++---- bundles/org.openhab.binding.windcentrale/pom.xml | 6 ++---- bundles/org.openhab.binding.xmltv/pom.xml | 6 ++---- bundles/org.openhab.binding.xmppclient/pom.xml | 6 ++---- .../org.openhab.binding.yamahareceiver/pom.xml | 6 ++---- bundles/org.openhab.binding.yeelight/pom.xml | 6 ++---- bundles/org.openhab.binding.zoneminder/pom.xml | 6 ++---- bundles/org.openhab.binding.zway/pom.xml | 6 ++---- .../pom.xml | 6 ++---- .../pom.xml | 6 ++---- bundles/org.openhab.io.homekit/pom.xml | 6 ++---- bundles/org.openhab.io.hueemulation/pom.xml | 6 ++---- bundles/org.openhab.io.imperihome/pom.xml | 6 ++---- bundles/org.openhab.io.javasound/pom.xml | 6 ++---- bundles/org.openhab.io.mqttembeddedbroker/pom.xml | 6 ++---- bundles/org.openhab.io.neeo/pom.xml | 6 ++---- bundles/org.openhab.io.openhabcloud/pom.xml | 6 ++---- bundles/org.openhab.io.transport.modbus/pom.xml | 6 ++---- bundles/org.openhab.io.webaudio/pom.xml | 6 ++---- bundles/org.openhab.transform.bin2json/pom.xml | 6 ++---- bundles/org.openhab.transform.exec/pom.xml | 6 ++---- bundles/org.openhab.transform.javascript/pom.xml | 6 ++---- bundles/org.openhab.transform.jinja/pom.xml | 6 ++---- bundles/org.openhab.transform.jsonpath/pom.xml | 6 ++---- bundles/org.openhab.transform.map/pom.xml | 6 ++---- bundles/org.openhab.transform.regex/pom.xml | 6 ++---- bundles/org.openhab.transform.scale/pom.xml | 6 ++---- bundles/org.openhab.transform.xpath/pom.xml | 6 ++---- bundles/org.openhab.transform.xslt/pom.xml | 6 ++---- bundles/org.openhab.voice.googletts/pom.xml | 6 ++---- bundles/org.openhab.voice.mactts/pom.xml | 6 ++---- bundles/org.openhab.voice.marytts/pom.xml | 6 ++---- bundles/org.openhab.voice.picotts/pom.xml | 6 ++---- bundles/org.openhab.voice.pollytts/pom.xml | 6 ++---- bundles/org.openhab.voice.voicerss/pom.xml | 6 ++---- bundles/pom.xml | 6 ++---- features/openhab-addons-external/pom.xml | 6 ++---- features/openhab-addons/pom.xml | 9 +++------ features/pom.xml | 6 ++---- itests/org.openhab.binding.astro.tests/pom.xml | 6 ++---- itests/org.openhab.binding.avmfritz.tests/pom.xml | 6 ++---- itests/org.openhab.binding.feed.tests/pom.xml | 6 ++---- itests/org.openhab.binding.hue.tests/pom.xml | 6 ++---- itests/org.openhab.binding.max.tests/pom.xml | 6 ++---- itests/org.openhab.binding.modbus.tests/pom.xml | 6 ++---- itests/org.openhab.binding.nest.tests/pom.xml | 6 ++---- itests/org.openhab.binding.ntp.tests/pom.xml | 6 ++---- .../org.openhab.binding.systeminfo.tests/pom.xml | 6 ++---- itests/org.openhab.binding.tradfri.tests/pom.xml | 6 ++---- itests/org.openhab.binding.wemo.tests/pom.xml | 6 ++---- itests/org.openhab.io.hueemulation.tests/pom.xml | 6 ++---- itests/pom.xml | 6 ++---- pom.xml | 6 ++---- 273 files changed, 548 insertions(+), 1092 deletions(-) diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index a66b090c2eaa4..e8eec5fe346bf 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bom org.openhab.addons.reactor.bom - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bom.openhab-addons diff --git a/bom/openhab-core-index/pom.xml b/bom/openhab-core-index/pom.xml index 199d1cdcc071f..92f2e66096255 100644 --- a/bom/openhab-core-index/pom.xml +++ b/bom/openhab-core-index/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bom org.openhab.addons.reactor.bom - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bom.openhab-core-index diff --git a/bom/pom.xml b/bom/pom.xml index 90cffc818e955..bb447b8a77bfb 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons org.openhab.addons.reactor - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bom @@ -39,14 +37,11 @@ - + - + - + header diff --git a/bom/runtime-index/pom.xml b/bom/runtime-index/pom.xml index d9bef01724fba..3429e252925e1 100644 --- a/bom/runtime-index/pom.xml +++ b/bom/runtime-index/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bom org.openhab.addons.reactor.bom - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bom.runtime-index diff --git a/bom/test-index/pom.xml b/bom/test-index/pom.xml index fffcc021d3b9b..202adc22dc62e 100644 --- a/bom/test-index/pom.xml +++ b/bom/test-index/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bom org.openhab.addons.reactor.bom - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bom.test-index diff --git a/bundles/org.openhab.binding.adorne/pom.xml b/bundles/org.openhab.binding.adorne/pom.xml index 2e17c19fa26b3..010f1dffb002d 100644 --- a/bundles/org.openhab.binding.adorne/pom.xml +++ b/bundles/org.openhab.binding.adorne/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.adorne diff --git a/bundles/org.openhab.binding.airquality/pom.xml b/bundles/org.openhab.binding.airquality/pom.xml index 13458aa7f9026..63abd372b04fa 100644 --- a/bundles/org.openhab.binding.airquality/pom.xml +++ b/bundles/org.openhab.binding.airquality/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.airquality diff --git a/bundles/org.openhab.binding.airvisualnode/pom.xml b/bundles/org.openhab.binding.airvisualnode/pom.xml index 0161b95ec84d9..88a532b4ab48f 100644 --- a/bundles/org.openhab.binding.airvisualnode/pom.xml +++ b/bundles/org.openhab.binding.airvisualnode/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.airvisualnode diff --git a/bundles/org.openhab.binding.alarmdecoder/pom.xml b/bundles/org.openhab.binding.alarmdecoder/pom.xml index 32bf1481ecaf0..950bc300b9807 100644 --- a/bundles/org.openhab.binding.alarmdecoder/pom.xml +++ b/bundles/org.openhab.binding.alarmdecoder/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.alarmdecoder diff --git a/bundles/org.openhab.binding.allplay/pom.xml b/bundles/org.openhab.binding.allplay/pom.xml index aae52deff3f24..a153c69217e35 100644 --- a/bundles/org.openhab.binding.allplay/pom.xml +++ b/bundles/org.openhab.binding.allplay/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.allplay diff --git a/bundles/org.openhab.binding.amazondashbutton/pom.xml b/bundles/org.openhab.binding.amazondashbutton/pom.xml index f07d20da3c201..b56fcb1cef1c1 100644 --- a/bundles/org.openhab.binding.amazondashbutton/pom.xml +++ b/bundles/org.openhab.binding.amazondashbutton/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.amazondashbutton diff --git a/bundles/org.openhab.binding.amazonechocontrol/pom.xml b/bundles/org.openhab.binding.amazonechocontrol/pom.xml index 22724d0109799..2eb756df47175 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/pom.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.amazonechocontrol diff --git a/bundles/org.openhab.binding.ambientweather/pom.xml b/bundles/org.openhab.binding.ambientweather/pom.xml index 81935b417c679..718e2dfea25e0 100644 --- a/bundles/org.openhab.binding.ambientweather/pom.xml +++ b/bundles/org.openhab.binding.ambientweather/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ambientweather diff --git a/bundles/org.openhab.binding.astro/pom.xml b/bundles/org.openhab.binding.astro/pom.xml index 823e5cf24e8dc..0108eb2afc91b 100644 --- a/bundles/org.openhab.binding.astro/pom.xml +++ b/bundles/org.openhab.binding.astro/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.astro diff --git a/bundles/org.openhab.binding.atlona/pom.xml b/bundles/org.openhab.binding.atlona/pom.xml index a3a2cda673c22..8f331dc3676c6 100644 --- a/bundles/org.openhab.binding.atlona/pom.xml +++ b/bundles/org.openhab.binding.atlona/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.atlona diff --git a/bundles/org.openhab.binding.autelis/pom.xml b/bundles/org.openhab.binding.autelis/pom.xml index a91d351a8ff7f..9956f5e312401 100644 --- a/bundles/org.openhab.binding.autelis/pom.xml +++ b/bundles/org.openhab.binding.autelis/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.autelis diff --git a/bundles/org.openhab.binding.avmfritz/pom.xml b/bundles/org.openhab.binding.avmfritz/pom.xml index 4b0ab69ea7c68..ff37aa0024edd 100644 --- a/bundles/org.openhab.binding.avmfritz/pom.xml +++ b/bundles/org.openhab.binding.avmfritz/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.avmfritz diff --git a/bundles/org.openhab.binding.bigassfan/pom.xml b/bundles/org.openhab.binding.bigassfan/pom.xml index 3eaab2c4e4c5b..c2b6fd91f9441 100644 --- a/bundles/org.openhab.binding.bigassfan/pom.xml +++ b/bundles/org.openhab.binding.bigassfan/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bigassfan diff --git a/bundles/org.openhab.binding.bluetooth.airthings/pom.xml b/bundles/org.openhab.binding.bluetooth.airthings/pom.xml index 07931a7a5a4d0..8b12d6eb14d22 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.airthings/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.airthings diff --git a/bundles/org.openhab.binding.bluetooth.am43/pom.xml b/bundles/org.openhab.binding.bluetooth.am43/pom.xml index dc8c8c207bbbd..013007527909b 100644 --- a/bundles/org.openhab.binding.bluetooth.am43/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.am43/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.am43 diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/pom.xml b/bundles/org.openhab.binding.bluetooth.bluegiga/pom.xml index 67dda55db3b48..ac17a68067f8e 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.bluegiga diff --git a/bundles/org.openhab.binding.bluetooth.bluez/pom.xml b/bundles/org.openhab.binding.bluetooth.bluez/pom.xml index 8b16b643b7064..beb6749f04ac8 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.bluez/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.bluez diff --git a/bundles/org.openhab.binding.bluetooth.blukii/pom.xml b/bundles/org.openhab.binding.bluetooth.blukii/pom.xml index db48714ecddfe..e4dcb8b876eb0 100644 --- a/bundles/org.openhab.binding.bluetooth.blukii/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.blukii/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.blukii diff --git a/bundles/org.openhab.binding.bluetooth.ruuvitag/pom.xml b/bundles/org.openhab.binding.bluetooth.ruuvitag/pom.xml index dbf861f996c3a..4f40e6a2e1776 100644 --- a/bundles/org.openhab.binding.bluetooth.ruuvitag/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.ruuvitag/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth.ruuvitag diff --git a/bundles/org.openhab.binding.bluetooth/pom.xml b/bundles/org.openhab.binding.bluetooth/pom.xml index 3fa78a6992b9b..79220c998685e 100644 --- a/bundles/org.openhab.binding.bluetooth/pom.xml +++ b/bundles/org.openhab.binding.bluetooth/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bluetooth diff --git a/bundles/org.openhab.binding.boschindego/pom.xml b/bundles/org.openhab.binding.boschindego/pom.xml index 6a1ab1702fcf7..9dca59c07cdb1 100644 --- a/bundles/org.openhab.binding.boschindego/pom.xml +++ b/bundles/org.openhab.binding.boschindego/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.boschindego diff --git a/bundles/org.openhab.binding.bosesoundtouch/pom.xml b/bundles/org.openhab.binding.bosesoundtouch/pom.xml index c4620532fab0f..cada48d0ed878 100644 --- a/bundles/org.openhab.binding.bosesoundtouch/pom.xml +++ b/bundles/org.openhab.binding.bosesoundtouch/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bosesoundtouch diff --git a/bundles/org.openhab.binding.bsblan/pom.xml b/bundles/org.openhab.binding.bsblan/pom.xml index 61bde7a4fb432..7c2f127f06107 100644 --- a/bundles/org.openhab.binding.bsblan/pom.xml +++ b/bundles/org.openhab.binding.bsblan/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.bsblan diff --git a/bundles/org.openhab.binding.buienradar/pom.xml b/bundles/org.openhab.binding.buienradar/pom.xml index 427563120fffc..57cc49a6634b8 100644 --- a/bundles/org.openhab.binding.buienradar/pom.xml +++ b/bundles/org.openhab.binding.buienradar/pom.xml @@ -1,11 +1,9 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.buienradar openHAB Add-ons :: Bundles :: Buienradar Binding diff --git a/bundles/org.openhab.binding.cbus/pom.xml b/bundles/org.openhab.binding.cbus/pom.xml index 4614d6ee86629..8322e90119a5a 100644 --- a/bundles/org.openhab.binding.cbus/pom.xml +++ b/bundles/org.openhab.binding.cbus/pom.xml @@ -1,6 +1,4 @@ - - + 4.0.0 @@ -8,7 +6,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.cbus diff --git a/bundles/org.openhab.binding.chromecast/pom.xml b/bundles/org.openhab.binding.chromecast/pom.xml index 746c6d0c57f3e..50a93f10cd44d 100644 --- a/bundles/org.openhab.binding.chromecast/pom.xml +++ b/bundles/org.openhab.binding.chromecast/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.chromecast diff --git a/bundles/org.openhab.binding.cm11a/pom.xml b/bundles/org.openhab.binding.cm11a/pom.xml index ff59eb2aa57a9..afd4a5a55df12 100644 --- a/bundles/org.openhab.binding.cm11a/pom.xml +++ b/bundles/org.openhab.binding.cm11a/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.cm11a diff --git a/bundles/org.openhab.binding.coolmasternet/pom.xml b/bundles/org.openhab.binding.coolmasternet/pom.xml index 1fe607ded4597..87267f3bc043e 100644 --- a/bundles/org.openhab.binding.coolmasternet/pom.xml +++ b/bundles/org.openhab.binding.coolmasternet/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.coolmasternet diff --git a/bundles/org.openhab.binding.coronastats/pom.xml b/bundles/org.openhab.binding.coronastats/pom.xml index 9d92ab82cc2c4..f6348b1878dc5 100644 --- a/bundles/org.openhab.binding.coronastats/pom.xml +++ b/bundles/org.openhab.binding.coronastats/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.coronastats diff --git a/bundles/org.openhab.binding.daikin/pom.xml b/bundles/org.openhab.binding.daikin/pom.xml index de6bff7437a30..175f8bde29f6b 100644 --- a/bundles/org.openhab.binding.daikin/pom.xml +++ b/bundles/org.openhab.binding.daikin/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.daikin diff --git a/bundles/org.openhab.binding.danfossairunit/pom.xml b/bundles/org.openhab.binding.danfossairunit/pom.xml index 8b4ad17c9196c..5b89503f32e60 100644 --- a/bundles/org.openhab.binding.danfossairunit/pom.xml +++ b/bundles/org.openhab.binding.danfossairunit/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.danfossairunit diff --git a/bundles/org.openhab.binding.darksky/pom.xml b/bundles/org.openhab.binding.darksky/pom.xml index 41653f63d7833..3d15c1c88ae61 100644 --- a/bundles/org.openhab.binding.darksky/pom.xml +++ b/bundles/org.openhab.binding.darksky/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.darksky diff --git a/bundles/org.openhab.binding.deconz/pom.xml b/bundles/org.openhab.binding.deconz/pom.xml index 92038b962ebf6..5c13925635ab7 100644 --- a/bundles/org.openhab.binding.deconz/pom.xml +++ b/bundles/org.openhab.binding.deconz/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.deconz diff --git a/bundles/org.openhab.binding.denonmarantz/pom.xml b/bundles/org.openhab.binding.denonmarantz/pom.xml index e51e6ac402830..894ce99b4c903 100644 --- a/bundles/org.openhab.binding.denonmarantz/pom.xml +++ b/bundles/org.openhab.binding.denonmarantz/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.denonmarantz diff --git a/bundles/org.openhab.binding.digiplex/pom.xml b/bundles/org.openhab.binding.digiplex/pom.xml index de84e49fa0b83..0963420b956ae 100644 --- a/bundles/org.openhab.binding.digiplex/pom.xml +++ b/bundles/org.openhab.binding.digiplex/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.digiplex diff --git a/bundles/org.openhab.binding.digitalstrom/pom.xml b/bundles/org.openhab.binding.digitalstrom/pom.xml index 560769df84afe..a436cd673a79c 100644 --- a/bundles/org.openhab.binding.digitalstrom/pom.xml +++ b/bundles/org.openhab.binding.digitalstrom/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.digitalstrom diff --git a/bundles/org.openhab.binding.dlinksmarthome/pom.xml b/bundles/org.openhab.binding.dlinksmarthome/pom.xml index 905e8daa1fe63..32677d945698c 100644 --- a/bundles/org.openhab.binding.dlinksmarthome/pom.xml +++ b/bundles/org.openhab.binding.dlinksmarthome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dlinksmarthome diff --git a/bundles/org.openhab.binding.dmx/pom.xml b/bundles/org.openhab.binding.dmx/pom.xml index e84933fe3d8eb..ff32f6fcfaa07 100644 --- a/bundles/org.openhab.binding.dmx/pom.xml +++ b/bundles/org.openhab.binding.dmx/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dmx diff --git a/bundles/org.openhab.binding.doorbird/pom.xml b/bundles/org.openhab.binding.doorbird/pom.xml index a54e49ebf914a..dd02f8810ec4f 100644 --- a/bundles/org.openhab.binding.doorbird/pom.xml +++ b/bundles/org.openhab.binding.doorbird/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.doorbird diff --git a/bundles/org.openhab.binding.dscalarm/pom.xml b/bundles/org.openhab.binding.dscalarm/pom.xml index 6ac7ebd7e2ab0..571b3247f9db8 100644 --- a/bundles/org.openhab.binding.dscalarm/pom.xml +++ b/bundles/org.openhab.binding.dscalarm/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dscalarm diff --git a/bundles/org.openhab.binding.dsmr/pom.xml b/bundles/org.openhab.binding.dsmr/pom.xml index 0c398076eaa8f..069d363ace6d1 100644 --- a/bundles/org.openhab.binding.dsmr/pom.xml +++ b/bundles/org.openhab.binding.dsmr/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dsmr diff --git a/bundles/org.openhab.binding.dwdpollenflug/pom.xml b/bundles/org.openhab.binding.dwdpollenflug/pom.xml index e37e8c25a4bd6..d78308d799dff 100644 --- a/bundles/org.openhab.binding.dwdpollenflug/pom.xml +++ b/bundles/org.openhab.binding.dwdpollenflug/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dwdpollenflug diff --git a/bundles/org.openhab.binding.dwdunwetter/pom.xml b/bundles/org.openhab.binding.dwdunwetter/pom.xml index 7ca215409fe23..45800a42ef324 100644 --- a/bundles/org.openhab.binding.dwdunwetter/pom.xml +++ b/bundles/org.openhab.binding.dwdunwetter/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.dwdunwetter diff --git a/bundles/org.openhab.binding.ecobee/pom.xml b/bundles/org.openhab.binding.ecobee/pom.xml index 72c98838edf1a..b6612c2fd98c0 100644 --- a/bundles/org.openhab.binding.ecobee/pom.xml +++ b/bundles/org.openhab.binding.ecobee/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ecobee diff --git a/bundles/org.openhab.binding.elerotransmitterstick/pom.xml b/bundles/org.openhab.binding.elerotransmitterstick/pom.xml index 8f59220d3697f..1ef3270453344 100644 --- a/bundles/org.openhab.binding.elerotransmitterstick/pom.xml +++ b/bundles/org.openhab.binding.elerotransmitterstick/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.elerotransmitterstick diff --git a/bundles/org.openhab.binding.energenie/pom.xml b/bundles/org.openhab.binding.energenie/pom.xml index 0cabd947bc067..f895cfe1e927d 100644 --- a/bundles/org.openhab.binding.energenie/pom.xml +++ b/bundles/org.openhab.binding.energenie/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.energenie diff --git a/bundles/org.openhab.binding.enigma2/pom.xml b/bundles/org.openhab.binding.enigma2/pom.xml index 82f7c91880db7..17cd84b918f0b 100644 --- a/bundles/org.openhab.binding.enigma2/pom.xml +++ b/bundles/org.openhab.binding.enigma2/pom.xml @@ -1,12 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.enigma2 diff --git a/bundles/org.openhab.binding.enocean/pom.xml b/bundles/org.openhab.binding.enocean/pom.xml index a8805b7297c1b..e4d0c6ed77f64 100644 --- a/bundles/org.openhab.binding.enocean/pom.xml +++ b/bundles/org.openhab.binding.enocean/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.enocean diff --git a/bundles/org.openhab.binding.enturno/pom.xml b/bundles/org.openhab.binding.enturno/pom.xml index 4fbf34305054b..a1d41b48d5f07 100644 --- a/bundles/org.openhab.binding.enturno/pom.xml +++ b/bundles/org.openhab.binding.enturno/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.enturno diff --git a/bundles/org.openhab.binding.etherrain/pom.xml b/bundles/org.openhab.binding.etherrain/pom.xml index 698b8f0a86865..857e33fcdf5db 100644 --- a/bundles/org.openhab.binding.etherrain/pom.xml +++ b/bundles/org.openhab.binding.etherrain/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.etherrain diff --git a/bundles/org.openhab.binding.evohome/pom.xml b/bundles/org.openhab.binding.evohome/pom.xml index 246a03a79d214..c608c53e56cf1 100644 --- a/bundles/org.openhab.binding.evohome/pom.xml +++ b/bundles/org.openhab.binding.evohome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.evohome diff --git a/bundles/org.openhab.binding.exec/pom.xml b/bundles/org.openhab.binding.exec/pom.xml index 8829345fcf602..062fcb19f7fdf 100644 --- a/bundles/org.openhab.binding.exec/pom.xml +++ b/bundles/org.openhab.binding.exec/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.exec diff --git a/bundles/org.openhab.binding.feed/pom.xml b/bundles/org.openhab.binding.feed/pom.xml index 8708f7ca719cb..f96cea39750f3 100644 --- a/bundles/org.openhab.binding.feed/pom.xml +++ b/bundles/org.openhab.binding.feed/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.feed diff --git a/bundles/org.openhab.binding.feican/pom.xml b/bundles/org.openhab.binding.feican/pom.xml index 804a2ee78aced..b36b62e9b012e 100644 --- a/bundles/org.openhab.binding.feican/pom.xml +++ b/bundles/org.openhab.binding.feican/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.feican diff --git a/bundles/org.openhab.binding.fmiweather/pom.xml b/bundles/org.openhab.binding.fmiweather/pom.xml index 6d13d2cb60803..3afa2f49a911e 100644 --- a/bundles/org.openhab.binding.fmiweather/pom.xml +++ b/bundles/org.openhab.binding.fmiweather/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.fmiweather diff --git a/bundles/org.openhab.binding.folding/pom.xml b/bundles/org.openhab.binding.folding/pom.xml index ba951caca6c38..d68475a08e7da 100644 --- a/bundles/org.openhab.binding.folding/pom.xml +++ b/bundles/org.openhab.binding.folding/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.folding diff --git a/bundles/org.openhab.binding.foobot/pom.xml b/bundles/org.openhab.binding.foobot/pom.xml index 27e5660ef6ad5..9bacc0a561610 100644 --- a/bundles/org.openhab.binding.foobot/pom.xml +++ b/bundles/org.openhab.binding.foobot/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.foobot diff --git a/bundles/org.openhab.binding.freebox/pom.xml b/bundles/org.openhab.binding.freebox/pom.xml index c40b94117be79..8ae72e026ca28 100644 --- a/bundles/org.openhab.binding.freebox/pom.xml +++ b/bundles/org.openhab.binding.freebox/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.freebox diff --git a/bundles/org.openhab.binding.fronius/pom.xml b/bundles/org.openhab.binding.fronius/pom.xml index 777c3e9c6fcd1..0d50e0acf215e 100644 --- a/bundles/org.openhab.binding.fronius/pom.xml +++ b/bundles/org.openhab.binding.fronius/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.fronius diff --git a/bundles/org.openhab.binding.fsinternetradio/pom.xml b/bundles/org.openhab.binding.fsinternetradio/pom.xml index da8a3f4d2c41c..19f291323692b 100644 --- a/bundles/org.openhab.binding.fsinternetradio/pom.xml +++ b/bundles/org.openhab.binding.fsinternetradio/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.fsinternetradio diff --git a/bundles/org.openhab.binding.ftpupload/pom.xml b/bundles/org.openhab.binding.ftpupload/pom.xml index abcc6576730e8..66e3613d666a6 100644 --- a/bundles/org.openhab.binding.ftpupload/pom.xml +++ b/bundles/org.openhab.binding.ftpupload/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ftpupload diff --git a/bundles/org.openhab.binding.gardena/pom.xml b/bundles/org.openhab.binding.gardena/pom.xml index 30a1abc9d6c3a..4c89d5e4828aa 100644 --- a/bundles/org.openhab.binding.gardena/pom.xml +++ b/bundles/org.openhab.binding.gardena/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.gardena diff --git a/bundles/org.openhab.binding.globalcache/pom.xml b/bundles/org.openhab.binding.globalcache/pom.xml index df8743760466e..54ba9c9ca1dfc 100644 --- a/bundles/org.openhab.binding.globalcache/pom.xml +++ b/bundles/org.openhab.binding.globalcache/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.globalcache diff --git a/bundles/org.openhab.binding.goecharger/pom.xml b/bundles/org.openhab.binding.goecharger/pom.xml index 2b724511c1683..ca935e9693bbb 100644 --- a/bundles/org.openhab.binding.goecharger/pom.xml +++ b/bundles/org.openhab.binding.goecharger/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.goecharger diff --git a/bundles/org.openhab.binding.gpstracker/pom.xml b/bundles/org.openhab.binding.gpstracker/pom.xml index 1d87caa955b58..3d4c6327dd39b 100644 --- a/bundles/org.openhab.binding.gpstracker/pom.xml +++ b/bundles/org.openhab.binding.gpstracker/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.gpstracker diff --git a/bundles/org.openhab.binding.groheondus/pom.xml b/bundles/org.openhab.binding.groheondus/pom.xml index 00bfda7d0a809..0530478a245d9 100644 --- a/bundles/org.openhab.binding.groheondus/pom.xml +++ b/bundles/org.openhab.binding.groheondus/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.groheondus diff --git a/bundles/org.openhab.binding.harmonyhub/pom.xml b/bundles/org.openhab.binding.harmonyhub/pom.xml index 141d90293bd56..20e5cd281b6a5 100644 --- a/bundles/org.openhab.binding.harmonyhub/pom.xml +++ b/bundles/org.openhab.binding.harmonyhub/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.harmonyhub diff --git a/bundles/org.openhab.binding.hdanywhere/pom.xml b/bundles/org.openhab.binding.hdanywhere/pom.xml index 7be57b324dd58..f3463004c1bfb 100644 --- a/bundles/org.openhab.binding.hdanywhere/pom.xml +++ b/bundles/org.openhab.binding.hdanywhere/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hdanywhere diff --git a/bundles/org.openhab.binding.hdpowerview/pom.xml b/bundles/org.openhab.binding.hdpowerview/pom.xml index 88a1877a5f269..c39aeb64fd33e 100644 --- a/bundles/org.openhab.binding.hdpowerview/pom.xml +++ b/bundles/org.openhab.binding.hdpowerview/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hdpowerview diff --git a/bundles/org.openhab.binding.helios/pom.xml b/bundles/org.openhab.binding.helios/pom.xml index 244b8b147fc3a..0dd0ef673beca 100644 --- a/bundles/org.openhab.binding.helios/pom.xml +++ b/bundles/org.openhab.binding.helios/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.helios diff --git a/bundles/org.openhab.binding.heos/pom.xml b/bundles/org.openhab.binding.heos/pom.xml index e2d8406a61341..eb8029d468b11 100644 --- a/bundles/org.openhab.binding.heos/pom.xml +++ b/bundles/org.openhab.binding.heos/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.heos diff --git a/bundles/org.openhab.binding.homematic/pom.xml b/bundles/org.openhab.binding.homematic/pom.xml index 66fc62816e8b5..026f824f08382 100644 --- a/bundles/org.openhab.binding.homematic/pom.xml +++ b/bundles/org.openhab.binding.homematic/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.homematic diff --git a/bundles/org.openhab.binding.hpprinter/pom.xml b/bundles/org.openhab.binding.hpprinter/pom.xml index 9b52c752e4e42..0ccf8de1703f0 100644 --- a/bundles/org.openhab.binding.hpprinter/pom.xml +++ b/bundles/org.openhab.binding.hpprinter/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hpprinter diff --git a/bundles/org.openhab.binding.hue/pom.xml b/bundles/org.openhab.binding.hue/pom.xml index 5b723be42088c..810e523cbd064 100644 --- a/bundles/org.openhab.binding.hue/pom.xml +++ b/bundles/org.openhab.binding.hue/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hue diff --git a/bundles/org.openhab.binding.hydrawise/pom.xml b/bundles/org.openhab.binding.hydrawise/pom.xml index 4bbebad4ba07c..43c7588f169e4 100644 --- a/bundles/org.openhab.binding.hydrawise/pom.xml +++ b/bundles/org.openhab.binding.hydrawise/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hydrawise diff --git a/bundles/org.openhab.binding.hyperion/pom.xml b/bundles/org.openhab.binding.hyperion/pom.xml index 4f518ffacf91a..2b359356a1ec6 100644 --- a/bundles/org.openhab.binding.hyperion/pom.xml +++ b/bundles/org.openhab.binding.hyperion/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.hyperion diff --git a/bundles/org.openhab.binding.iaqualink/pom.xml b/bundles/org.openhab.binding.iaqualink/pom.xml index d0af430f127d0..3204fb692ca93 100644 --- a/bundles/org.openhab.binding.iaqualink/pom.xml +++ b/bundles/org.openhab.binding.iaqualink/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.iaqualink diff --git a/bundles/org.openhab.binding.icalendar/pom.xml b/bundles/org.openhab.binding.icalendar/pom.xml index 4a8e747b35ff2..182a702f2707b 100644 --- a/bundles/org.openhab.binding.icalendar/pom.xml +++ b/bundles/org.openhab.binding.icalendar/pom.xml @@ -1,11 +1,9 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.icalendar openHAB Add-ons :: Bundles :: iCalendar Binding diff --git a/bundles/org.openhab.binding.icloud/pom.xml b/bundles/org.openhab.binding.icloud/pom.xml index d04ddeb6b92a6..dfad1654bf4ee 100644 --- a/bundles/org.openhab.binding.icloud/pom.xml +++ b/bundles/org.openhab.binding.icloud/pom.xml @@ -1,12 +1,10 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.icloud diff --git a/bundles/org.openhab.binding.ihc/pom.xml b/bundles/org.openhab.binding.ihc/pom.xml index 17f9ee40f4326..152d926090f3d 100644 --- a/bundles/org.openhab.binding.ihc/pom.xml +++ b/bundles/org.openhab.binding.ihc/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ihc diff --git a/bundles/org.openhab.binding.innogysmarthome/pom.xml b/bundles/org.openhab.binding.innogysmarthome/pom.xml index 9ab188ac6a568..9eeebbb89e409 100644 --- a/bundles/org.openhab.binding.innogysmarthome/pom.xml +++ b/bundles/org.openhab.binding.innogysmarthome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.innogysmarthome diff --git a/bundles/org.openhab.binding.insteon/pom.xml b/bundles/org.openhab.binding.insteon/pom.xml index 76d29a978790e..e8ce56c33fa8c 100644 --- a/bundles/org.openhab.binding.insteon/pom.xml +++ b/bundles/org.openhab.binding.insteon/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.insteon diff --git a/bundles/org.openhab.binding.ipp/pom.xml b/bundles/org.openhab.binding.ipp/pom.xml index 7d099b64db127..266ffe8298475 100644 --- a/bundles/org.openhab.binding.ipp/pom.xml +++ b/bundles/org.openhab.binding.ipp/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ipp diff --git a/bundles/org.openhab.binding.irtrans/pom.xml b/bundles/org.openhab.binding.irtrans/pom.xml index 2e3f65b2cd1a8..8f509b4390fd0 100644 --- a/bundles/org.openhab.binding.irtrans/pom.xml +++ b/bundles/org.openhab.binding.irtrans/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.irtrans diff --git a/bundles/org.openhab.binding.jeelink/pom.xml b/bundles/org.openhab.binding.jeelink/pom.xml index 5d1b819a35789..08315df0a7bb0 100644 --- a/bundles/org.openhab.binding.jeelink/pom.xml +++ b/bundles/org.openhab.binding.jeelink/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.jeelink diff --git a/bundles/org.openhab.binding.keba/pom.xml b/bundles/org.openhab.binding.keba/pom.xml index 60a4da11e5cae..5ac4a1a44c25a 100644 --- a/bundles/org.openhab.binding.keba/pom.xml +++ b/bundles/org.openhab.binding.keba/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.keba diff --git a/bundles/org.openhab.binding.km200/pom.xml b/bundles/org.openhab.binding.km200/pom.xml index fe59c134864ec..3880d38e283e1 100644 --- a/bundles/org.openhab.binding.km200/pom.xml +++ b/bundles/org.openhab.binding.km200/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.km200 diff --git a/bundles/org.openhab.binding.knx/pom.xml b/bundles/org.openhab.binding.knx/pom.xml index c9f46849bdeee..108269cbe5408 100644 --- a/bundles/org.openhab.binding.knx/pom.xml +++ b/bundles/org.openhab.binding.knx/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.knx diff --git a/bundles/org.openhab.binding.kodi/pom.xml b/bundles/org.openhab.binding.kodi/pom.xml index f3c90bc07f701..c9e7d9fff6d85 100644 --- a/bundles/org.openhab.binding.kodi/pom.xml +++ b/bundles/org.openhab.binding.kodi/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.kodi diff --git a/bundles/org.openhab.binding.konnected/pom.xml b/bundles/org.openhab.binding.konnected/pom.xml index fa2b3fd0acfc8..7bca10799c0ed 100644 --- a/bundles/org.openhab.binding.konnected/pom.xml +++ b/bundles/org.openhab.binding.konnected/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.konnected diff --git a/bundles/org.openhab.binding.kostalinverter/pom.xml b/bundles/org.openhab.binding.kostalinverter/pom.xml index 3b7208726427d..735f4b0e53e3f 100644 --- a/bundles/org.openhab.binding.kostalinverter/pom.xml +++ b/bundles/org.openhab.binding.kostalinverter/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.kostalinverter diff --git a/bundles/org.openhab.binding.lametrictime/pom.xml b/bundles/org.openhab.binding.lametrictime/pom.xml index 6840d097f9b0d..83a00b4fb9fac 100644 --- a/bundles/org.openhab.binding.lametrictime/pom.xml +++ b/bundles/org.openhab.binding.lametrictime/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lametrictime diff --git a/bundles/org.openhab.binding.lcn/pom.xml b/bundles/org.openhab.binding.lcn/pom.xml index eccef96b93fe3..9819d1149049b 100644 --- a/bundles/org.openhab.binding.lcn/pom.xml +++ b/bundles/org.openhab.binding.lcn/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lcn diff --git a/bundles/org.openhab.binding.leapmotion/pom.xml b/bundles/org.openhab.binding.leapmotion/pom.xml index 5beef321ada5c..4d40ae43cc2c8 100644 --- a/bundles/org.openhab.binding.leapmotion/pom.xml +++ b/bundles/org.openhab.binding.leapmotion/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.leapmotion diff --git a/bundles/org.openhab.binding.lghombot/pom.xml b/bundles/org.openhab.binding.lghombot/pom.xml index 051d33220ec6c..cbaee24e24b4c 100644 --- a/bundles/org.openhab.binding.lghombot/pom.xml +++ b/bundles/org.openhab.binding.lghombot/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lghombot diff --git a/bundles/org.openhab.binding.lgtvserial/pom.xml b/bundles/org.openhab.binding.lgtvserial/pom.xml index bf86c7b21678f..39b127fbf1ea6 100644 --- a/bundles/org.openhab.binding.lgtvserial/pom.xml +++ b/bundles/org.openhab.binding.lgtvserial/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lgtvserial diff --git a/bundles/org.openhab.binding.lgwebos/pom.xml b/bundles/org.openhab.binding.lgwebos/pom.xml index 9f6896e096a18..81801cbc36fdc 100644 --- a/bundles/org.openhab.binding.lgwebos/pom.xml +++ b/bundles/org.openhab.binding.lgwebos/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lgwebos diff --git a/bundles/org.openhab.binding.lifx/pom.xml b/bundles/org.openhab.binding.lifx/pom.xml index d5cce570edc5b..b1e77fb22ffd4 100644 --- a/bundles/org.openhab.binding.lifx/pom.xml +++ b/bundles/org.openhab.binding.lifx/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lifx diff --git a/bundles/org.openhab.binding.linky/pom.xml b/bundles/org.openhab.binding.linky/pom.xml index b5264d0789a69..e1458ac1ec5fe 100644 --- a/bundles/org.openhab.binding.linky/pom.xml +++ b/bundles/org.openhab.binding.linky/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.linky diff --git a/bundles/org.openhab.binding.linuxinput/pom.xml b/bundles/org.openhab.binding.linuxinput/pom.xml index 0e48db86d38b8..4ec1aef7fb65d 100644 --- a/bundles/org.openhab.binding.linuxinput/pom.xml +++ b/bundles/org.openhab.binding.linuxinput/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.linuxinput diff --git a/bundles/org.openhab.binding.lirc/pom.xml b/bundles/org.openhab.binding.lirc/pom.xml index 7fc79d0aadf1f..943e8f52988b7 100644 --- a/bundles/org.openhab.binding.lirc/pom.xml +++ b/bundles/org.openhab.binding.lirc/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lirc diff --git a/bundles/org.openhab.binding.logreader/pom.xml b/bundles/org.openhab.binding.logreader/pom.xml index 0601e304eb1c0..9e5ec8791d1c7 100644 --- a/bundles/org.openhab.binding.logreader/pom.xml +++ b/bundles/org.openhab.binding.logreader/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.logreader diff --git a/bundles/org.openhab.binding.loxone/pom.xml b/bundles/org.openhab.binding.loxone/pom.xml index d7d656250c2d7..33627469aee64 100644 --- a/bundles/org.openhab.binding.loxone/pom.xml +++ b/bundles/org.openhab.binding.loxone/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.loxone diff --git a/bundles/org.openhab.binding.lutron/pom.xml b/bundles/org.openhab.binding.lutron/pom.xml index 49e12db5f1836..392b133e3f719 100644 --- a/bundles/org.openhab.binding.lutron/pom.xml +++ b/bundles/org.openhab.binding.lutron/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.lutron diff --git a/bundles/org.openhab.binding.mail/pom.xml b/bundles/org.openhab.binding.mail/pom.xml index f77b912a9cbae..a679e50b54c05 100644 --- a/bundles/org.openhab.binding.mail/pom.xml +++ b/bundles/org.openhab.binding.mail/pom.xml @@ -1,12 +1,10 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mail diff --git a/bundles/org.openhab.binding.max/pom.xml b/bundles/org.openhab.binding.max/pom.xml index 00c90e86cb8c1..dba7f22f868a3 100644 --- a/bundles/org.openhab.binding.max/pom.xml +++ b/bundles/org.openhab.binding.max/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.max diff --git a/bundles/org.openhab.binding.mcp23017/pom.xml b/bundles/org.openhab.binding.mcp23017/pom.xml index 0c0ff3543e27a..25accad4fefdd 100644 --- a/bundles/org.openhab.binding.mcp23017/pom.xml +++ b/bundles/org.openhab.binding.mcp23017/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mcp23017 diff --git a/bundles/org.openhab.binding.melcloud/pom.xml b/bundles/org.openhab.binding.melcloud/pom.xml index fb8f1f2dd67f3..f1d577575029e 100644 --- a/bundles/org.openhab.binding.melcloud/pom.xml +++ b/bundles/org.openhab.binding.melcloud/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.melcloud diff --git a/bundles/org.openhab.binding.meteoalerte/pom.xml b/bundles/org.openhab.binding.meteoalerte/pom.xml index 837d1f841f230..a865566c83a21 100644 --- a/bundles/org.openhab.binding.meteoalerte/pom.xml +++ b/bundles/org.openhab.binding.meteoalerte/pom.xml @@ -5,7 +5,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.meteoalerte diff --git a/bundles/org.openhab.binding.meteoblue/pom.xml b/bundles/org.openhab.binding.meteoblue/pom.xml index 5c03d196408f4..5960ac8e19a35 100644 --- a/bundles/org.openhab.binding.meteoblue/pom.xml +++ b/bundles/org.openhab.binding.meteoblue/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.meteoblue diff --git a/bundles/org.openhab.binding.meteostick/pom.xml b/bundles/org.openhab.binding.meteostick/pom.xml index a07652c4f93de..61bfa2f1610e4 100644 --- a/bundles/org.openhab.binding.meteostick/pom.xml +++ b/bundles/org.openhab.binding.meteostick/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.meteostick diff --git a/bundles/org.openhab.binding.miele/pom.xml b/bundles/org.openhab.binding.miele/pom.xml index e918cdc1e8d77..08cd204072e68 100644 --- a/bundles/org.openhab.binding.miele/pom.xml +++ b/bundles/org.openhab.binding.miele/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.miele diff --git a/bundles/org.openhab.binding.mihome/pom.xml b/bundles/org.openhab.binding.mihome/pom.xml index ec2a204818009..254a58499b3c4 100644 --- a/bundles/org.openhab.binding.mihome/pom.xml +++ b/bundles/org.openhab.binding.mihome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mihome diff --git a/bundles/org.openhab.binding.miio/pom.xml b/bundles/org.openhab.binding.miio/pom.xml index 4548505002ab1..fca6b1e77a6ff 100644 --- a/bundles/org.openhab.binding.miio/pom.xml +++ b/bundles/org.openhab.binding.miio/pom.xml @@ -1,11 +1,9 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.miio openHAB Add-ons :: Bundles :: Xiaomi Mi IO Binding diff --git a/bundles/org.openhab.binding.milight/pom.xml b/bundles/org.openhab.binding.milight/pom.xml index 077b40b58f46f..eae1044f01a4e 100644 --- a/bundles/org.openhab.binding.milight/pom.xml +++ b/bundles/org.openhab.binding.milight/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.milight diff --git a/bundles/org.openhab.binding.millheat/pom.xml b/bundles/org.openhab.binding.millheat/pom.xml index ead6bde13cb14..bef30f86fb682 100644 --- a/bundles/org.openhab.binding.millheat/pom.xml +++ b/bundles/org.openhab.binding.millheat/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.millheat diff --git a/bundles/org.openhab.binding.minecraft/pom.xml b/bundles/org.openhab.binding.minecraft/pom.xml index c22eaa9c52470..33e3cee19606c 100644 --- a/bundles/org.openhab.binding.minecraft/pom.xml +++ b/bundles/org.openhab.binding.minecraft/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.minecraft diff --git a/bundles/org.openhab.binding.modbus.sunspec/pom.xml b/bundles/org.openhab.binding.modbus.sunspec/pom.xml index 1cbc06c957183..218e1e8643c30 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/pom.xml +++ b/bundles/org.openhab.binding.modbus.sunspec/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.modbus.sunspec diff --git a/bundles/org.openhab.binding.modbus/pom.xml b/bundles/org.openhab.binding.modbus/pom.xml index 14321f10feaaa..2de8a8f2898a2 100644 --- a/bundles/org.openhab.binding.modbus/pom.xml +++ b/bundles/org.openhab.binding.modbus/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.modbus diff --git a/bundles/org.openhab.binding.mqtt.generic/pom.xml b/bundles/org.openhab.binding.mqtt.generic/pom.xml index 6196c943fe999..7bb2ba6a16cb7 100644 --- a/bundles/org.openhab.binding.mqtt.generic/pom.xml +++ b/bundles/org.openhab.binding.mqtt.generic/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mqtt.generic diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml index 077af82e9ae13..bfa06cef7dee8 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml +++ b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mqtt.homeassistant diff --git a/bundles/org.openhab.binding.mqtt.homie/pom.xml b/bundles/org.openhab.binding.mqtt.homie/pom.xml index 8037f1e6cdedd..dcebad45e9f24 100644 --- a/bundles/org.openhab.binding.mqtt.homie/pom.xml +++ b/bundles/org.openhab.binding.mqtt.homie/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mqtt.homie diff --git a/bundles/org.openhab.binding.mqtt/pom.xml b/bundles/org.openhab.binding.mqtt/pom.xml index cd6336b2ca2d1..4fbbe89cd3e53 100644 --- a/bundles/org.openhab.binding.mqtt/pom.xml +++ b/bundles/org.openhab.binding.mqtt/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.mqtt diff --git a/bundles/org.openhab.binding.nanoleaf/pom.xml b/bundles/org.openhab.binding.nanoleaf/pom.xml index 440b314f1b332..821b212618a54 100644 --- a/bundles/org.openhab.binding.nanoleaf/pom.xml +++ b/bundles/org.openhab.binding.nanoleaf/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nanoleaf diff --git a/bundles/org.openhab.binding.neato/pom.xml b/bundles/org.openhab.binding.neato/pom.xml index 4c2d2ae5adf85..4c46731ac5651 100644 --- a/bundles/org.openhab.binding.neato/pom.xml +++ b/bundles/org.openhab.binding.neato/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.neato diff --git a/bundles/org.openhab.binding.neeo/pom.xml b/bundles/org.openhab.binding.neeo/pom.xml index 76ff35251b899..cfd2c494dc839 100644 --- a/bundles/org.openhab.binding.neeo/pom.xml +++ b/bundles/org.openhab.binding.neeo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.neeo diff --git a/bundles/org.openhab.binding.neohub/pom.xml b/bundles/org.openhab.binding.neohub/pom.xml index eb1609b309c56..828cbdfec102a 100644 --- a/bundles/org.openhab.binding.neohub/pom.xml +++ b/bundles/org.openhab.binding.neohub/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.neohub diff --git a/bundles/org.openhab.binding.nest/pom.xml b/bundles/org.openhab.binding.nest/pom.xml index b5c7c6e4a6f49..4f26614d22bd2 100644 --- a/bundles/org.openhab.binding.nest/pom.xml +++ b/bundles/org.openhab.binding.nest/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nest diff --git a/bundles/org.openhab.binding.netatmo/pom.xml b/bundles/org.openhab.binding.netatmo/pom.xml index edc5c5ec82c12..86ae9367f04f0 100644 --- a/bundles/org.openhab.binding.netatmo/pom.xml +++ b/bundles/org.openhab.binding.netatmo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.netatmo diff --git a/bundles/org.openhab.binding.network/pom.xml b/bundles/org.openhab.binding.network/pom.xml index 5c9141b23079d..e34852ec5ebc1 100644 --- a/bundles/org.openhab.binding.network/pom.xml +++ b/bundles/org.openhab.binding.network/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.network diff --git a/bundles/org.openhab.binding.networkupstools/pom.xml b/bundles/org.openhab.binding.networkupstools/pom.xml index 0d0797bd0d703..fe322fea33b2e 100644 --- a/bundles/org.openhab.binding.networkupstools/pom.xml +++ b/bundles/org.openhab.binding.networkupstools/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.networkupstools diff --git a/bundles/org.openhab.binding.nibeheatpump/pom.xml b/bundles/org.openhab.binding.nibeheatpump/pom.xml index 1a05d8c994c19..9c6a982254f52 100644 --- a/bundles/org.openhab.binding.nibeheatpump/pom.xml +++ b/bundles/org.openhab.binding.nibeheatpump/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nibeheatpump diff --git a/bundles/org.openhab.binding.nibeuplink/pom.xml b/bundles/org.openhab.binding.nibeuplink/pom.xml index 38f2d43033e23..94743f9ea220a 100644 --- a/bundles/org.openhab.binding.nibeuplink/pom.xml +++ b/bundles/org.openhab.binding.nibeuplink/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nibeuplink diff --git a/bundles/org.openhab.binding.nikobus/pom.xml b/bundles/org.openhab.binding.nikobus/pom.xml index f935ec87e7e07..27d88af5d1b3d 100644 --- a/bundles/org.openhab.binding.nikobus/pom.xml +++ b/bundles/org.openhab.binding.nikobus/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nikobus diff --git a/bundles/org.openhab.binding.nikohomecontrol/pom.xml b/bundles/org.openhab.binding.nikohomecontrol/pom.xml index ac0f66ae38c2f..24b8c4f9c722f 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/pom.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nikohomecontrol diff --git a/bundles/org.openhab.binding.novafinedust/pom.xml b/bundles/org.openhab.binding.novafinedust/pom.xml index 06f7b28af079a..3d540d095168b 100644 --- a/bundles/org.openhab.binding.novafinedust/pom.xml +++ b/bundles/org.openhab.binding.novafinedust/pom.xml @@ -1,12 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.novafinedust diff --git a/bundles/org.openhab.binding.ntp/pom.xml b/bundles/org.openhab.binding.ntp/pom.xml index 9f8e3fd47e9f7..d831b0bb4760b 100644 --- a/bundles/org.openhab.binding.ntp/pom.xml +++ b/bundles/org.openhab.binding.ntp/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.ntp diff --git a/bundles/org.openhab.binding.nuki/pom.xml b/bundles/org.openhab.binding.nuki/pom.xml index 9fa0d7369ae80..f9a7c98248927 100644 --- a/bundles/org.openhab.binding.nuki/pom.xml +++ b/bundles/org.openhab.binding.nuki/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.nuki diff --git a/bundles/org.openhab.binding.oceanic/pom.xml b/bundles/org.openhab.binding.oceanic/pom.xml index aa4155bd92205..929b164797dc8 100644 --- a/bundles/org.openhab.binding.oceanic/pom.xml +++ b/bundles/org.openhab.binding.oceanic/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.oceanic diff --git a/bundles/org.openhab.binding.omnikinverter/pom.xml b/bundles/org.openhab.binding.omnikinverter/pom.xml index ffb66c989e7f7..98dc7517876c8 100644 --- a/bundles/org.openhab.binding.omnikinverter/pom.xml +++ b/bundles/org.openhab.binding.omnikinverter/pom.xml @@ -1,12 +1,10 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.omnikinverter diff --git a/bundles/org.openhab.binding.onebusaway/pom.xml b/bundles/org.openhab.binding.onebusaway/pom.xml index 84ca3e58a50a5..9ab9c2dfb3177 100644 --- a/bundles/org.openhab.binding.onebusaway/pom.xml +++ b/bundles/org.openhab.binding.onebusaway/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.onebusaway diff --git a/bundles/org.openhab.binding.onewire/pom.xml b/bundles/org.openhab.binding.onewire/pom.xml index e8190cd88296c..339c536478183 100644 --- a/bundles/org.openhab.binding.onewire/pom.xml +++ b/bundles/org.openhab.binding.onewire/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.onewire diff --git a/bundles/org.openhab.binding.onewiregpio/pom.xml b/bundles/org.openhab.binding.onewiregpio/pom.xml index 98802d98bde26..646ffa252643c 100644 --- a/bundles/org.openhab.binding.onewiregpio/pom.xml +++ b/bundles/org.openhab.binding.onewiregpio/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.onewiregpio diff --git a/bundles/org.openhab.binding.onkyo/pom.xml b/bundles/org.openhab.binding.onkyo/pom.xml index 6bc133235b644..bb6fc5e7e63f8 100644 --- a/bundles/org.openhab.binding.onkyo/pom.xml +++ b/bundles/org.openhab.binding.onkyo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.onkyo diff --git a/bundles/org.openhab.binding.opengarage/pom.xml b/bundles/org.openhab.binding.opengarage/pom.xml index bb09c84478678..89b80ae6d7620 100644 --- a/bundles/org.openhab.binding.opengarage/pom.xml +++ b/bundles/org.openhab.binding.opengarage/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.opengarage diff --git a/bundles/org.openhab.binding.opensprinkler/pom.xml b/bundles/org.openhab.binding.opensprinkler/pom.xml index 8ef8e463ea022..0f12738b1c56d 100644 --- a/bundles/org.openhab.binding.opensprinkler/pom.xml +++ b/bundles/org.openhab.binding.opensprinkler/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.opensprinkler diff --git a/bundles/org.openhab.binding.openthermgateway/pom.xml b/bundles/org.openhab.binding.openthermgateway/pom.xml index 6d91464ad3633..8c31229e2bee5 100644 --- a/bundles/org.openhab.binding.openthermgateway/pom.xml +++ b/bundles/org.openhab.binding.openthermgateway/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.openthermgateway diff --git a/bundles/org.openhab.binding.openuv/pom.xml b/bundles/org.openhab.binding.openuv/pom.xml index 91697115ac9fc..49d079baca284 100644 --- a/bundles/org.openhab.binding.openuv/pom.xml +++ b/bundles/org.openhab.binding.openuv/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.openuv diff --git a/bundles/org.openhab.binding.openweathermap/pom.xml b/bundles/org.openhab.binding.openweathermap/pom.xml index 19414a2e952b7..9fa0e807c7837 100644 --- a/bundles/org.openhab.binding.openweathermap/pom.xml +++ b/bundles/org.openhab.binding.openweathermap/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.openweathermap diff --git a/bundles/org.openhab.binding.orvibo/pom.xml b/bundles/org.openhab.binding.orvibo/pom.xml index b62794e9ca280..bf5cbfc144a62 100644 --- a/bundles/org.openhab.binding.orvibo/pom.xml +++ b/bundles/org.openhab.binding.orvibo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.orvibo diff --git a/bundles/org.openhab.binding.paradoxalarm/pom.xml b/bundles/org.openhab.binding.paradoxalarm/pom.xml index c15ca6b44f7ca..234997b938b95 100644 --- a/bundles/org.openhab.binding.paradoxalarm/pom.xml +++ b/bundles/org.openhab.binding.paradoxalarm/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.reactor.bundles org.openhab.addons.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.paradoxalarm diff --git a/bundles/org.openhab.binding.pentair/pom.xml b/bundles/org.openhab.binding.pentair/pom.xml index efe3c32439ee1..8967fafe839c5 100644 --- a/bundles/org.openhab.binding.pentair/pom.xml +++ b/bundles/org.openhab.binding.pentair/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pentair diff --git a/bundles/org.openhab.binding.phc/pom.xml b/bundles/org.openhab.binding.phc/pom.xml index 6e4a03f77959d..51db6027d7de7 100644 --- a/bundles/org.openhab.binding.phc/pom.xml +++ b/bundles/org.openhab.binding.phc/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.phc diff --git a/bundles/org.openhab.binding.pioneeravr/pom.xml b/bundles/org.openhab.binding.pioneeravr/pom.xml index b080179704c78..fdc2801aea4d9 100644 --- a/bundles/org.openhab.binding.pioneeravr/pom.xml +++ b/bundles/org.openhab.binding.pioneeravr/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pioneeravr diff --git a/bundles/org.openhab.binding.pixometer/pom.xml b/bundles/org.openhab.binding.pixometer/pom.xml index 67f1f859f13bd..6639963c6e1fd 100644 --- a/bundles/org.openhab.binding.pixometer/pom.xml +++ b/bundles/org.openhab.binding.pixometer/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pixometer diff --git a/bundles/org.openhab.binding.pjlinkdevice/pom.xml b/bundles/org.openhab.binding.pjlinkdevice/pom.xml index 17b87df05f91d..b9f0296bd2689 100644 --- a/bundles/org.openhab.binding.pjlinkdevice/pom.xml +++ b/bundles/org.openhab.binding.pjlinkdevice/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pjlinkdevice diff --git a/bundles/org.openhab.binding.plclogo/pom.xml b/bundles/org.openhab.binding.plclogo/pom.xml index e101548a91627..340d628cc8a31 100644 --- a/bundles/org.openhab.binding.plclogo/pom.xml +++ b/bundles/org.openhab.binding.plclogo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.plclogo diff --git a/bundles/org.openhab.binding.plugwise/pom.xml b/bundles/org.openhab.binding.plugwise/pom.xml index 4bc78cae141ba..5d4d7b3c86955 100644 --- a/bundles/org.openhab.binding.plugwise/pom.xml +++ b/bundles/org.openhab.binding.plugwise/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.plugwise diff --git a/bundles/org.openhab.binding.powermax/pom.xml b/bundles/org.openhab.binding.powermax/pom.xml index 464e3666a7340..673fa4744458b 100644 --- a/bundles/org.openhab.binding.powermax/pom.xml +++ b/bundles/org.openhab.binding.powermax/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.powermax diff --git a/bundles/org.openhab.binding.pulseaudio/pom.xml b/bundles/org.openhab.binding.pulseaudio/pom.xml index d6a32c5ed0540..c2f34f116a234 100644 --- a/bundles/org.openhab.binding.pulseaudio/pom.xml +++ b/bundles/org.openhab.binding.pulseaudio/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pulseaudio diff --git a/bundles/org.openhab.binding.pushbullet/pom.xml b/bundles/org.openhab.binding.pushbullet/pom.xml index 92df7c43b5fb7..12d20f879914c 100644 --- a/bundles/org.openhab.binding.pushbullet/pom.xml +++ b/bundles/org.openhab.binding.pushbullet/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.pushbullet diff --git a/bundles/org.openhab.binding.regoheatpump/pom.xml b/bundles/org.openhab.binding.regoheatpump/pom.xml index d4d8817ea710e..ec26a2e99d007 100644 --- a/bundles/org.openhab.binding.regoheatpump/pom.xml +++ b/bundles/org.openhab.binding.regoheatpump/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.regoheatpump diff --git a/bundles/org.openhab.binding.rfxcom/pom.xml b/bundles/org.openhab.binding.rfxcom/pom.xml index 1f319bda17e57..f32fa1bea2e41 100644 --- a/bundles/org.openhab.binding.rfxcom/pom.xml +++ b/bundles/org.openhab.binding.rfxcom/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.rfxcom diff --git a/bundles/org.openhab.binding.rme/pom.xml b/bundles/org.openhab.binding.rme/pom.xml index c62db87d48f95..b9f15739021ab 100644 --- a/bundles/org.openhab.binding.rme/pom.xml +++ b/bundles/org.openhab.binding.rme/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.rme diff --git a/bundles/org.openhab.binding.robonect/pom.xml b/bundles/org.openhab.binding.robonect/pom.xml index 45d38ea44389c..35aed480cf30b 100644 --- a/bundles/org.openhab.binding.robonect/pom.xml +++ b/bundles/org.openhab.binding.robonect/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.robonect diff --git a/bundles/org.openhab.binding.rotel/pom.xml b/bundles/org.openhab.binding.rotel/pom.xml index 169a49143254d..32f241b4c3a45 100644 --- a/bundles/org.openhab.binding.rotel/pom.xml +++ b/bundles/org.openhab.binding.rotel/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.rotel diff --git a/bundles/org.openhab.binding.rotelra1x/pom.xml b/bundles/org.openhab.binding.rotelra1x/pom.xml index 8b84e8a91f390..72fcf3bec4a66 100644 --- a/bundles/org.openhab.binding.rotelra1x/pom.xml +++ b/bundles/org.openhab.binding.rotelra1x/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.rotelra1x diff --git a/bundles/org.openhab.binding.russound/pom.xml b/bundles/org.openhab.binding.russound/pom.xml index dd9d39b42305f..f7654601537a3 100644 --- a/bundles/org.openhab.binding.russound/pom.xml +++ b/bundles/org.openhab.binding.russound/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.russound diff --git a/bundles/org.openhab.binding.sagercaster/pom.xml b/bundles/org.openhab.binding.sagercaster/pom.xml index b2ac472971648..c4c80c8227ec6 100644 --- a/bundles/org.openhab.binding.sagercaster/pom.xml +++ b/bundles/org.openhab.binding.sagercaster/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sagercaster diff --git a/bundles/org.openhab.binding.samsungtv/pom.xml b/bundles/org.openhab.binding.samsungtv/pom.xml index 75cdacf1b5df1..a9e4d6200955d 100644 --- a/bundles/org.openhab.binding.samsungtv/pom.xml +++ b/bundles/org.openhab.binding.samsungtv/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.samsungtv diff --git a/bundles/org.openhab.binding.satel/pom.xml b/bundles/org.openhab.binding.satel/pom.xml index a20f9832c482e..39b433fe196a8 100644 --- a/bundles/org.openhab.binding.satel/pom.xml +++ b/bundles/org.openhab.binding.satel/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.satel diff --git a/bundles/org.openhab.binding.seneye/pom.xml b/bundles/org.openhab.binding.seneye/pom.xml index 954610f025f66..2467bbfc8b57a 100644 --- a/bundles/org.openhab.binding.seneye/pom.xml +++ b/bundles/org.openhab.binding.seneye/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.seneye diff --git a/bundles/org.openhab.binding.sensebox/pom.xml b/bundles/org.openhab.binding.sensebox/pom.xml index 7b51bf991b376..515b204355519 100644 --- a/bundles/org.openhab.binding.sensebox/pom.xml +++ b/bundles/org.openhab.binding.sensebox/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sensebox diff --git a/bundles/org.openhab.binding.sensibo/pom.xml b/bundles/org.openhab.binding.sensibo/pom.xml index c440bf9ea90fc..bf913b92b35c4 100644 --- a/bundles/org.openhab.binding.sensibo/pom.xml +++ b/bundles/org.openhab.binding.sensibo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sensibo diff --git a/bundles/org.openhab.binding.serialbutton/pom.xml b/bundles/org.openhab.binding.serialbutton/pom.xml index 7550c80c990bf..a6a1645b5a485 100644 --- a/bundles/org.openhab.binding.serialbutton/pom.xml +++ b/bundles/org.openhab.binding.serialbutton/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.serialbutton diff --git a/bundles/org.openhab.binding.shelly/pom.xml b/bundles/org.openhab.binding.shelly/pom.xml index 25080f23ef528..8802b3dc8b65c 100644 --- a/bundles/org.openhab.binding.shelly/pom.xml +++ b/bundles/org.openhab.binding.shelly/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT diff --git a/bundles/org.openhab.binding.siemensrds/pom.xml b/bundles/org.openhab.binding.siemensrds/pom.xml index cdb857d2dc1e8..bcd3e785abf4f 100644 --- a/bundles/org.openhab.binding.siemensrds/pom.xml +++ b/bundles/org.openhab.binding.siemensrds/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.siemensrds diff --git a/bundles/org.openhab.binding.silvercrestwifisocket/pom.xml b/bundles/org.openhab.binding.silvercrestwifisocket/pom.xml index ae4e3e0e67b3c..ed72764202e32 100644 --- a/bundles/org.openhab.binding.silvercrestwifisocket/pom.xml +++ b/bundles/org.openhab.binding.silvercrestwifisocket/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.silvercrestwifisocket diff --git a/bundles/org.openhab.binding.sinope/pom.xml b/bundles/org.openhab.binding.sinope/pom.xml index 69c4e2641ace6..88fe54b9251c7 100644 --- a/bundles/org.openhab.binding.sinope/pom.xml +++ b/bundles/org.openhab.binding.sinope/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sinope diff --git a/bundles/org.openhab.binding.sleepiq/pom.xml b/bundles/org.openhab.binding.sleepiq/pom.xml index 6cff18e971fe3..70152067fb64d 100644 --- a/bundles/org.openhab.binding.sleepiq/pom.xml +++ b/bundles/org.openhab.binding.sleepiq/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sleepiq diff --git a/bundles/org.openhab.binding.smaenergymeter/pom.xml b/bundles/org.openhab.binding.smaenergymeter/pom.xml index 53c440f387e36..c15292c9b0111 100644 --- a/bundles/org.openhab.binding.smaenergymeter/pom.xml +++ b/bundles/org.openhab.binding.smaenergymeter/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.smaenergymeter diff --git a/bundles/org.openhab.binding.smartmeter/pom.xml b/bundles/org.openhab.binding.smartmeter/pom.xml index a0d64d669a05a..3c645b5ca8732 100644 --- a/bundles/org.openhab.binding.smartmeter/pom.xml +++ b/bundles/org.openhab.binding.smartmeter/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.smartmeter diff --git a/bundles/org.openhab.binding.smhi/pom.xml b/bundles/org.openhab.binding.smhi/pom.xml index d813dce2f265b..196d676ad3ffb 100644 --- a/bundles/org.openhab.binding.smhi/pom.xml +++ b/bundles/org.openhab.binding.smhi/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.smhi diff --git a/bundles/org.openhab.binding.snmp/pom.xml b/bundles/org.openhab.binding.snmp/pom.xml index 6d7ff9fa1389d..bdf5a4b2d76af 100644 --- a/bundles/org.openhab.binding.snmp/pom.xml +++ b/bundles/org.openhab.binding.snmp/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.snmp diff --git a/bundles/org.openhab.binding.solaredge/pom.xml b/bundles/org.openhab.binding.solaredge/pom.xml index 0b409730317d6..43fd0e68b8622 100644 --- a/bundles/org.openhab.binding.solaredge/pom.xml +++ b/bundles/org.openhab.binding.solaredge/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.solaredge diff --git a/bundles/org.openhab.binding.solarlog/pom.xml b/bundles/org.openhab.binding.solarlog/pom.xml index bb40792f28639..4b9a4b94b6271 100644 --- a/bundles/org.openhab.binding.solarlog/pom.xml +++ b/bundles/org.openhab.binding.solarlog/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.solarlog diff --git a/bundles/org.openhab.binding.somfymylink/pom.xml b/bundles/org.openhab.binding.somfymylink/pom.xml index fa97d7feeef9c..2beadfb1f6d9e 100644 --- a/bundles/org.openhab.binding.somfymylink/pom.xml +++ b/bundles/org.openhab.binding.somfymylink/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.somfymylink diff --git a/bundles/org.openhab.binding.somfytahoma/pom.xml b/bundles/org.openhab.binding.somfytahoma/pom.xml index c8134098b1443..d72c334c043b1 100644 --- a/bundles/org.openhab.binding.somfytahoma/pom.xml +++ b/bundles/org.openhab.binding.somfytahoma/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.somfytahoma diff --git a/bundles/org.openhab.binding.sonos/pom.xml b/bundles/org.openhab.binding.sonos/pom.xml index 84d7e284397d6..c7a016cdecb94 100644 --- a/bundles/org.openhab.binding.sonos/pom.xml +++ b/bundles/org.openhab.binding.sonos/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sonos diff --git a/bundles/org.openhab.binding.sonyaudio/pom.xml b/bundles/org.openhab.binding.sonyaudio/pom.xml index 7048463a347de..dcb5a80b482fc 100644 --- a/bundles/org.openhab.binding.sonyaudio/pom.xml +++ b/bundles/org.openhab.binding.sonyaudio/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sonyaudio diff --git a/bundles/org.openhab.binding.sonyprojector/pom.xml b/bundles/org.openhab.binding.sonyprojector/pom.xml index 4e02dcac466d5..0c86cd3d63738 100644 --- a/bundles/org.openhab.binding.sonyprojector/pom.xml +++ b/bundles/org.openhab.binding.sonyprojector/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.sonyprojector diff --git a/bundles/org.openhab.binding.spotify/pom.xml b/bundles/org.openhab.binding.spotify/pom.xml index 8e58cb6e38d0c..14b6a3fd36ee6 100644 --- a/bundles/org.openhab.binding.spotify/pom.xml +++ b/bundles/org.openhab.binding.spotify/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.spotify diff --git a/bundles/org.openhab.binding.squeezebox/pom.xml b/bundles/org.openhab.binding.squeezebox/pom.xml index f20a6fc0f99b7..3082d1c775217 100644 --- a/bundles/org.openhab.binding.squeezebox/pom.xml +++ b/bundles/org.openhab.binding.squeezebox/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.squeezebox diff --git a/bundles/org.openhab.binding.synopanalyzer/pom.xml b/bundles/org.openhab.binding.synopanalyzer/pom.xml index ab51d4e2243e9..1e3cae495a169 100644 --- a/bundles/org.openhab.binding.synopanalyzer/pom.xml +++ b/bundles/org.openhab.binding.synopanalyzer/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.synopanalyzer diff --git a/bundles/org.openhab.binding.systeminfo/pom.xml b/bundles/org.openhab.binding.systeminfo/pom.xml index 2f27f2b6c47a7..4d55a1a15ce5d 100644 --- a/bundles/org.openhab.binding.systeminfo/pom.xml +++ b/bundles/org.openhab.binding.systeminfo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.systeminfo diff --git a/bundles/org.openhab.binding.tado/pom.xml b/bundles/org.openhab.binding.tado/pom.xml index 7595f12c0829b..9f929ca0fba2b 100644 --- a/bundles/org.openhab.binding.tado/pom.xml +++ b/bundles/org.openhab.binding.tado/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tado diff --git a/bundles/org.openhab.binding.tankerkoenig/pom.xml b/bundles/org.openhab.binding.tankerkoenig/pom.xml index 8bfce1aa46680..6bc102707f73d 100644 --- a/bundles/org.openhab.binding.tankerkoenig/pom.xml +++ b/bundles/org.openhab.binding.tankerkoenig/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tankerkoenig diff --git a/bundles/org.openhab.binding.telegram/pom.xml b/bundles/org.openhab.binding.telegram/pom.xml index 35bfb4c30dc64..f9182e6820dc3 100644 --- a/bundles/org.openhab.binding.telegram/pom.xml +++ b/bundles/org.openhab.binding.telegram/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.telegram diff --git a/bundles/org.openhab.binding.tellstick/pom.xml b/bundles/org.openhab.binding.tellstick/pom.xml index 94fd74411c598..f835c9845361b 100644 --- a/bundles/org.openhab.binding.tellstick/pom.xml +++ b/bundles/org.openhab.binding.tellstick/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tellstick diff --git a/bundles/org.openhab.binding.tesla/pom.xml b/bundles/org.openhab.binding.tesla/pom.xml index 70e1ed58374ad..9c70a8d174a20 100644 --- a/bundles/org.openhab.binding.tesla/pom.xml +++ b/bundles/org.openhab.binding.tesla/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tesla diff --git a/bundles/org.openhab.binding.tibber/pom.xml b/bundles/org.openhab.binding.tibber/pom.xml index 0355d82c97d74..6b6b76244dbb7 100644 --- a/bundles/org.openhab.binding.tibber/pom.xml +++ b/bundles/org.openhab.binding.tibber/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tibber diff --git a/bundles/org.openhab.binding.tplinksmarthome/pom.xml b/bundles/org.openhab.binding.tplinksmarthome/pom.xml index d9ad26e2d4995..226bdcc3beb55 100644 --- a/bundles/org.openhab.binding.tplinksmarthome/pom.xml +++ b/bundles/org.openhab.binding.tplinksmarthome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tplinksmarthome diff --git a/bundles/org.openhab.binding.tradfri/pom.xml b/bundles/org.openhab.binding.tradfri/pom.xml index 375773f8238d1..585bef88ef528 100644 --- a/bundles/org.openhab.binding.tradfri/pom.xml +++ b/bundles/org.openhab.binding.tradfri/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.tradfri diff --git a/bundles/org.openhab.binding.unifi/pom.xml b/bundles/org.openhab.binding.unifi/pom.xml index 4a5fb1cc4c0b4..4c1039ca2eeaf 100644 --- a/bundles/org.openhab.binding.unifi/pom.xml +++ b/bundles/org.openhab.binding.unifi/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.unifi diff --git a/bundles/org.openhab.binding.urtsi/pom.xml b/bundles/org.openhab.binding.urtsi/pom.xml index 6bf9467f38317..e376dc5353d21 100644 --- a/bundles/org.openhab.binding.urtsi/pom.xml +++ b/bundles/org.openhab.binding.urtsi/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.urtsi diff --git a/bundles/org.openhab.binding.valloxmv/pom.xml b/bundles/org.openhab.binding.valloxmv/pom.xml index 56bfce35f5d58..81ff314ef2258 100644 --- a/bundles/org.openhab.binding.valloxmv/pom.xml +++ b/bundles/org.openhab.binding.valloxmv/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.valloxmv diff --git a/bundles/org.openhab.binding.vektiva/pom.xml b/bundles/org.openhab.binding.vektiva/pom.xml index 0b3625ec21fef..23f205c10764b 100644 --- a/bundles/org.openhab.binding.vektiva/pom.xml +++ b/bundles/org.openhab.binding.vektiva/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.reactor.bundles org.openhab.addons.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.vektiva diff --git a/bundles/org.openhab.binding.velbus/pom.xml b/bundles/org.openhab.binding.velbus/pom.xml index 26c28580f3533..617619d53de6e 100644 --- a/bundles/org.openhab.binding.velbus/pom.xml +++ b/bundles/org.openhab.binding.velbus/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.velbus diff --git a/bundles/org.openhab.binding.velux/pom.xml b/bundles/org.openhab.binding.velux/pom.xml index b59b8040830f0..c0d76026df271 100644 --- a/bundles/org.openhab.binding.velux/pom.xml +++ b/bundles/org.openhab.binding.velux/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.velux diff --git a/bundles/org.openhab.binding.vigicrues/pom.xml b/bundles/org.openhab.binding.vigicrues/pom.xml index 9ec44633068b6..39eb5dc7a416f 100644 --- a/bundles/org.openhab.binding.vigicrues/pom.xml +++ b/bundles/org.openhab.binding.vigicrues/pom.xml @@ -5,7 +5,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.vigicrues diff --git a/bundles/org.openhab.binding.vitotronic/pom.xml b/bundles/org.openhab.binding.vitotronic/pom.xml index bb6c52be7e8a8..9226a20faec24 100644 --- a/bundles/org.openhab.binding.vitotronic/pom.xml +++ b/bundles/org.openhab.binding.vitotronic/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.vitotronic diff --git a/bundles/org.openhab.binding.volvooncall/pom.xml b/bundles/org.openhab.binding.volvooncall/pom.xml index 5138152e8acb8..8a7054c0d2fb5 100644 --- a/bundles/org.openhab.binding.volvooncall/pom.xml +++ b/bundles/org.openhab.binding.volvooncall/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.volvooncall diff --git a/bundles/org.openhab.binding.weathercompany/pom.xml b/bundles/org.openhab.binding.weathercompany/pom.xml index f48ebdca9f29d..5f61fcff956d4 100644 --- a/bundles/org.openhab.binding.weathercompany/pom.xml +++ b/bundles/org.openhab.binding.weathercompany/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.weathercompany diff --git a/bundles/org.openhab.binding.weatherunderground/pom.xml b/bundles/org.openhab.binding.weatherunderground/pom.xml index 415132cf7e0db..8b5ba63e9b071 100644 --- a/bundles/org.openhab.binding.weatherunderground/pom.xml +++ b/bundles/org.openhab.binding.weatherunderground/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.weatherunderground diff --git a/bundles/org.openhab.binding.wemo/pom.xml b/bundles/org.openhab.binding.wemo/pom.xml index 0e9508543e51d..1367afcf759e7 100644 --- a/bundles/org.openhab.binding.wemo/pom.xml +++ b/bundles/org.openhab.binding.wemo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.wemo diff --git a/bundles/org.openhab.binding.wifiled/pom.xml b/bundles/org.openhab.binding.wifiled/pom.xml index b015648616b76..f128a350ac693 100644 --- a/bundles/org.openhab.binding.wifiled/pom.xml +++ b/bundles/org.openhab.binding.wifiled/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.wifiled diff --git a/bundles/org.openhab.binding.windcentrale/pom.xml b/bundles/org.openhab.binding.windcentrale/pom.xml index 4fc5b6e4f1467..9070c682f33cd 100644 --- a/bundles/org.openhab.binding.windcentrale/pom.xml +++ b/bundles/org.openhab.binding.windcentrale/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.windcentrale diff --git a/bundles/org.openhab.binding.xmltv/pom.xml b/bundles/org.openhab.binding.xmltv/pom.xml index a732ed98f9952..eba619c04984b 100644 --- a/bundles/org.openhab.binding.xmltv/pom.xml +++ b/bundles/org.openhab.binding.xmltv/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.xmltv diff --git a/bundles/org.openhab.binding.xmppclient/pom.xml b/bundles/org.openhab.binding.xmppclient/pom.xml index 2457aae37f655..8b54dee827368 100644 --- a/bundles/org.openhab.binding.xmppclient/pom.xml +++ b/bundles/org.openhab.binding.xmppclient/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.xmppclient diff --git a/bundles/org.openhab.binding.yamahareceiver/pom.xml b/bundles/org.openhab.binding.yamahareceiver/pom.xml index 3f92a5a4f3049..2321de8209863 100644 --- a/bundles/org.openhab.binding.yamahareceiver/pom.xml +++ b/bundles/org.openhab.binding.yamahareceiver/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.yamahareceiver diff --git a/bundles/org.openhab.binding.yeelight/pom.xml b/bundles/org.openhab.binding.yeelight/pom.xml index 48abd25eb985a..0ae2cbe4884ae 100644 --- a/bundles/org.openhab.binding.yeelight/pom.xml +++ b/bundles/org.openhab.binding.yeelight/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.yeelight diff --git a/bundles/org.openhab.binding.zoneminder/pom.xml b/bundles/org.openhab.binding.zoneminder/pom.xml index 6a0f4acbcb362..e54ee4504e719 100644 --- a/bundles/org.openhab.binding.zoneminder/pom.xml +++ b/bundles/org.openhab.binding.zoneminder/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.zoneminder diff --git a/bundles/org.openhab.binding.zway/pom.xml b/bundles/org.openhab.binding.zway/pom.xml index 2460befbed0bc..6755a542fe463 100644 --- a/bundles/org.openhab.binding.zway/pom.xml +++ b/bundles/org.openhab.binding.zway/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.binding.zway diff --git a/bundles/org.openhab.extensionservice.marketplace.automation/pom.xml b/bundles/org.openhab.extensionservice.marketplace.automation/pom.xml index e0c6f5c41a649..4059eb431e04c 100644 --- a/bundles/org.openhab.extensionservice.marketplace.automation/pom.xml +++ b/bundles/org.openhab.extensionservice.marketplace.automation/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.extensionservice.marketplace.automation diff --git a/bundles/org.openhab.extensionservice.marketplace/pom.xml b/bundles/org.openhab.extensionservice.marketplace/pom.xml index 85be7dad37ac3..3a0443e9f4a3d 100644 --- a/bundles/org.openhab.extensionservice.marketplace/pom.xml +++ b/bundles/org.openhab.extensionservice.marketplace/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.extensionservice.marketplace diff --git a/bundles/org.openhab.io.homekit/pom.xml b/bundles/org.openhab.io.homekit/pom.xml index ac6a9765a44a2..307beabf38a9a 100644 --- a/bundles/org.openhab.io.homekit/pom.xml +++ b/bundles/org.openhab.io.homekit/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.homekit diff --git a/bundles/org.openhab.io.hueemulation/pom.xml b/bundles/org.openhab.io.hueemulation/pom.xml index d3d20faf5611d..3cbd45afb0b7e 100644 --- a/bundles/org.openhab.io.hueemulation/pom.xml +++ b/bundles/org.openhab.io.hueemulation/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.hueemulation diff --git a/bundles/org.openhab.io.imperihome/pom.xml b/bundles/org.openhab.io.imperihome/pom.xml index 4c3d8f3cd59de..f386607882579 100644 --- a/bundles/org.openhab.io.imperihome/pom.xml +++ b/bundles/org.openhab.io.imperihome/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.imperihome diff --git a/bundles/org.openhab.io.javasound/pom.xml b/bundles/org.openhab.io.javasound/pom.xml index 0099a025c6c36..709222adc78c2 100644 --- a/bundles/org.openhab.io.javasound/pom.xml +++ b/bundles/org.openhab.io.javasound/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.javasound diff --git a/bundles/org.openhab.io.mqttembeddedbroker/pom.xml b/bundles/org.openhab.io.mqttembeddedbroker/pom.xml index 46d5abe3ace44..ff53ed81f1819 100644 --- a/bundles/org.openhab.io.mqttembeddedbroker/pom.xml +++ b/bundles/org.openhab.io.mqttembeddedbroker/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.mqttembeddedbroker diff --git a/bundles/org.openhab.io.neeo/pom.xml b/bundles/org.openhab.io.neeo/pom.xml index fb0aa578f51ef..539b837266433 100644 --- a/bundles/org.openhab.io.neeo/pom.xml +++ b/bundles/org.openhab.io.neeo/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.neeo diff --git a/bundles/org.openhab.io.openhabcloud/pom.xml b/bundles/org.openhab.io.openhabcloud/pom.xml index d69d6ce1db8c0..85422cbaf4576 100644 --- a/bundles/org.openhab.io.openhabcloud/pom.xml +++ b/bundles/org.openhab.io.openhabcloud/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.openhabcloud diff --git a/bundles/org.openhab.io.transport.modbus/pom.xml b/bundles/org.openhab.io.transport.modbus/pom.xml index 1eeefc0887c2b..63f9ee5aa0efa 100644 --- a/bundles/org.openhab.io.transport.modbus/pom.xml +++ b/bundles/org.openhab.io.transport.modbus/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.transport.modbus diff --git a/bundles/org.openhab.io.webaudio/pom.xml b/bundles/org.openhab.io.webaudio/pom.xml index a50ab906b936a..94678aa52edc3 100644 --- a/bundles/org.openhab.io.webaudio/pom.xml +++ b/bundles/org.openhab.io.webaudio/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.io.webaudio diff --git a/bundles/org.openhab.transform.bin2json/pom.xml b/bundles/org.openhab.transform.bin2json/pom.xml index 8ec75731c4c86..4a0f0c16b4dba 100644 --- a/bundles/org.openhab.transform.bin2json/pom.xml +++ b/bundles/org.openhab.transform.bin2json/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.bin2json diff --git a/bundles/org.openhab.transform.exec/pom.xml b/bundles/org.openhab.transform.exec/pom.xml index 666dd7953bd3b..e74d90b24ec95 100644 --- a/bundles/org.openhab.transform.exec/pom.xml +++ b/bundles/org.openhab.transform.exec/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.exec diff --git a/bundles/org.openhab.transform.javascript/pom.xml b/bundles/org.openhab.transform.javascript/pom.xml index 2ccaadb8d3407..6b9708bbdf5a2 100644 --- a/bundles/org.openhab.transform.javascript/pom.xml +++ b/bundles/org.openhab.transform.javascript/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.javascript diff --git a/bundles/org.openhab.transform.jinja/pom.xml b/bundles/org.openhab.transform.jinja/pom.xml index d8b78feb0afab..99a5412065730 100644 --- a/bundles/org.openhab.transform.jinja/pom.xml +++ b/bundles/org.openhab.transform.jinja/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.jinja diff --git a/bundles/org.openhab.transform.jsonpath/pom.xml b/bundles/org.openhab.transform.jsonpath/pom.xml index 558ba2f858011..118304b887e86 100644 --- a/bundles/org.openhab.transform.jsonpath/pom.xml +++ b/bundles/org.openhab.transform.jsonpath/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.jsonpath diff --git a/bundles/org.openhab.transform.map/pom.xml b/bundles/org.openhab.transform.map/pom.xml index 1fc57495407c4..53c7d1e5fd1ce 100644 --- a/bundles/org.openhab.transform.map/pom.xml +++ b/bundles/org.openhab.transform.map/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.map diff --git a/bundles/org.openhab.transform.regex/pom.xml b/bundles/org.openhab.transform.regex/pom.xml index f51dd7f92e4f4..4cfccae1b346d 100644 --- a/bundles/org.openhab.transform.regex/pom.xml +++ b/bundles/org.openhab.transform.regex/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.regex diff --git a/bundles/org.openhab.transform.scale/pom.xml b/bundles/org.openhab.transform.scale/pom.xml index 0ec61059aef2d..d9270ef8fb470 100644 --- a/bundles/org.openhab.transform.scale/pom.xml +++ b/bundles/org.openhab.transform.scale/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.scale diff --git a/bundles/org.openhab.transform.xpath/pom.xml b/bundles/org.openhab.transform.xpath/pom.xml index c3da60f00c381..aa8e1aa7a7f93 100644 --- a/bundles/org.openhab.transform.xpath/pom.xml +++ b/bundles/org.openhab.transform.xpath/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.xpath diff --git a/bundles/org.openhab.transform.xslt/pom.xml b/bundles/org.openhab.transform.xslt/pom.xml index af93f1f664452..4fe7a62089b28 100644 --- a/bundles/org.openhab.transform.xslt/pom.xml +++ b/bundles/org.openhab.transform.xslt/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.transform.xslt diff --git a/bundles/org.openhab.voice.googletts/pom.xml b/bundles/org.openhab.voice.googletts/pom.xml index 5796766d546ad..9b4db9537a40c 100644 --- a/bundles/org.openhab.voice.googletts/pom.xml +++ b/bundles/org.openhab.voice.googletts/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.googletts diff --git a/bundles/org.openhab.voice.mactts/pom.xml b/bundles/org.openhab.voice.mactts/pom.xml index 071f2c72bdf4e..5429ad031e9f1 100644 --- a/bundles/org.openhab.voice.mactts/pom.xml +++ b/bundles/org.openhab.voice.mactts/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.mactts diff --git a/bundles/org.openhab.voice.marytts/pom.xml b/bundles/org.openhab.voice.marytts/pom.xml index d73add22999cf..6dd1906ea4292 100644 --- a/bundles/org.openhab.voice.marytts/pom.xml +++ b/bundles/org.openhab.voice.marytts/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.marytts diff --git a/bundles/org.openhab.voice.picotts/pom.xml b/bundles/org.openhab.voice.picotts/pom.xml index e9e0aa7ed1591..17c675d82755d 100644 --- a/bundles/org.openhab.voice.picotts/pom.xml +++ b/bundles/org.openhab.voice.picotts/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.picotts diff --git a/bundles/org.openhab.voice.pollytts/pom.xml b/bundles/org.openhab.voice.pollytts/pom.xml index e871fad6bf3d0..bb4d2aee63de1 100644 --- a/bundles/org.openhab.voice.pollytts/pom.xml +++ b/bundles/org.openhab.voice.pollytts/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.pollytts diff --git a/bundles/org.openhab.voice.voicerss/pom.xml b/bundles/org.openhab.voice.voicerss/pom.xml index c526e218d3ea0..144e41ddcbc58 100644 --- a/bundles/org.openhab.voice.voicerss/pom.xml +++ b/bundles/org.openhab.voice.voicerss/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.voice.voicerss diff --git a/bundles/pom.xml b/bundles/pom.xml index 7259eabff90db..de8830d0a7978 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons org.openhab.addons.reactor - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.bundles diff --git a/features/openhab-addons-external/pom.xml b/features/openhab-addons-external/pom.xml index a24267f046a7d..4f2dbfd0d327f 100644 --- a/features/openhab-addons-external/pom.xml +++ b/features/openhab-addons-external/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.features.karaf org.openhab.addons.reactor.features.karaf - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT openhab-addons-external diff --git a/features/openhab-addons/pom.xml b/features/openhab-addons/pom.xml index e489705893a89..49f151cee7afc 100644 --- a/features/openhab-addons/pom.xml +++ b/features/openhab-addons/pom.xml @@ -1,13 +1,11 @@ - - + 4.0.0 org.openhab.addons.features.karaf org.openhab.addons.reactor.features.karaf - 2.5.6-SNAPSHOT + 2.5.7-SNAPSHOT org.openhab.addons.features.karaf.openhab-addons @@ -54,8 +52,7 @@ - +